blob: ca1d9903236d339b8b457755d1f790fdb36f5bbd [file] [log] [blame] [view]
## Implementing compatibility {#compat}
### Referencing new APIs {#compat-newapi}
Generally, methods on library classes should be available to all devices above
the library's `minSdkVersion`; however, the behavior of the method may vary
based on platform API availability.
For example, a method may delegate to a platform API on SDKs where the API is
available, backport a subset of behavior on earlier SDKs, and no-op on very old
SDKs.
#### Checking device SDK version {#compat-sdk}
The most common way of delegating to platform or backport implementations is to
compare the device's `Build.VERSION.SDK_INT` field to a known-good SDK version;
for example, the SDK in which a method first appeared or in which a critical bug
was first fixed.
Non-reflective calls to new APIs gated on `SDK_INT` **must** be made from
version-specific static inner classes to avoid verification errors that
negatively affect run-time performance. This is enforced at build time by the
`ClassVerificationFailure` lint check, which offers auto-fixes in Java sources.
For more information, see Chromium's guide to
[Class Verification Failures](https://chromium.googlesource.com/chromium/src/+/HEAD/build/android/docs/class_verification_failures.md).
Methods in implementation-specific classes **must** be paired with the
`@DoNotInline` annotation to prevent them from being inlined.
```java {.good}
public static void saveAttributeDataForStyleable(@NonNull View view, ...) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.saveAttributeDataForStyleable(view, ...);
}
}
@RequiresApi(29)
private static class Api29Impl {
@DoNotInline
static void saveAttributeDataForStyleable(@NonNull View view, ...) {
view.saveAttributeDataForStyleable(...);
}
}
```
Alternatively, in Kotlin sources:
```kotlin {.good}
@RequiresApi(29)
private object Api29Impl {
@JvmStatic
@DoNotInline
fun saveAttributeDataForStyleable(view: View, ...) { ... }
}
```
When developing against pre-release SDKs where the `SDK_INT` has not been
finalized, SDK checks **must** use `BuildCompat.isAtLeastX()` methods and
**must** use a tip-of-tree `project` dependency to ensure that the
implementation of `BuildCompat` stays up-to-date when the SDK is finalized.
**Do not** assume that the next SDK release's `SDK_INT` will be N+1. The value
is not finalized until SDK finalization happens, at which point the `isAtLeast`
check will be updated. **Never** write your own check for a pre-release SDK.
```java {.good}
@NonNull
public static List<Window> getAllWindows() {
if (BuildCompat.isAtLeastR()) {
return ApiRImpl.getAllWindows();
}
return Collections.emptyList();
}
```
```kotlin {.good}
dependencies {
api(project(":core:core"))
}
```
##### Preventing invalid casting {#compat-casting}
Even when a call to a new API is moved to a version-specific class, a class
verification failure is still possible when referencing types introduced in new
APIs.
When a type does not exist on a device, the verifier treats the type as
`Object`. This is a problem if the new type is implicitly cast to a different
type which does exist on the device.
In general, if `A extends B`, using an `A` as a `B` without an explicit cast is
fine. However, if `A` was introduced at a later API level than `B`, on devices
below that API level, `A` will be seen as `Object`. An `Object` cannot be used
as a `B` without an explicit cast. However, adding an explicit cast to `B` won't
fix this, because the compiler will see the cast as redundant (as it normally
would be). So, implicit casts between types introduced at different API levels
should be moved out to version-specific static inner classes, as described
[above](#compat-sdk).
The `ImplicitCastClassVerificationFailure` lint check detects and provides
autofixes for instances of invalid implicit casts.
For instance, the following would **not** be valid, because it implicitly casts
an `AdaptiveIconDrawable` (new in API level 26, `Object` on lower API levels) to
`Drawable`. Instead, the method inside of `Api26Impl` could return `Drawable`,
or the cast could be moved into a version-specific static inner class.
```java {.bad}
private Drawable methodReturnsDrawable() {
if (Build.VERSION.SDK_INT >= 26) {
// Implicitly casts the returned AdaptiveIconDrawable to Drawable
return Api26Impl.createAdaptiveIconDrawable(null, null);
} else {
return null;
}
}
@RequiresApi(26)
static class Api26Impl {
// Returns AdaptiveIconDrawable, introduced in API level 26
@DoNotInline
static AdaptiveIconDrawable createAdaptiveIconDrawable(Drawable backgroundDrawable, Drawable foregroundDrawable) {
return new AdaptiveIconDrawable(backgroundDrawable, foregroundDrawable);
}
}
```
The version-specific static inner class solution would look like this:
```java {.good}
private Drawable methodReturnsDrawable() {
if (Build.VERSION.SDK_INT >= 26) {
return Api26Impl.castToDrawable(Api26Impl.createAdaptiveIconDrawable(null, null));
} else {
return null;
}
}
@RequiresApi(26)
static class Api26Impl {
// Returns AdaptiveIconDrawable, introduced in API level 26
@DoNotInline
static AdaptiveIconDrawable createAdaptiveIconDrawable(Drawable backgroundDrawable, Drawable foregroundDrawable) {
return new AdaptiveIconDrawable(backgroundDrawable, foregroundDrawable);
}
// Method which performs the implicit cast from AdaptiveIconDrawable to Drawable
@DoNotInline
static Drawable castToDrawable(AdaptiveIconDrawable adaptiveIconDrawable) {
return adaptiveIconDrawable;
}
}
```
The following would also **not** be valid, because it implicitly casts a
`Notification.MessagingStyle` (new in API level 24, `Object` on lower API
levels) to `Notification.Style`. Instead, `Api24Impl` could have a `setBuilder`
method which takes `Notification.MessagingStyle` as a parameter, or the cast
could be moved into a version-specific static inner class.
```java {.bad}
public void methodUsesStyle(Notification.MessagingStyle style, Notification.Builder builder) {
if (Build.VERSION.SDK_INT >= 24) {
Api16Impl.setBuilder(
// Implicitly casts the style to Notification.Style (added in API level 16)
// when it is a Notification.MessagingStyle (added in API level 24)
style, builder
);
}
}
@RequiresApi(16)
static class Api16Impl {
private Api16Impl() { }
@DoNotInline
static void setBuilder(Notification.Style style, Notification.Builder builder) {
style.setBuilder(builder);
}
}
```
The version-specific static inner class solution would look like this:
```java {.good}
public void methodUsesStyle(Notification.MessagingStyle style, Notification.Builder builder) {
if (Build.VERSION.SDK_INT >= 24) {
Api16Impl.setBuilder(
Api24Impl.castToStyle(style), builder
);
}
}
@RequiresApi(16)
static class Api16Impl {
private Api16Impl() { }
@DoNotInline
static void setBuilder(Notification.Style style, Notification.Builder builder) {
style.setBuilder(builder);
}
}
@RequiresApi(24)
static class Api24Impl {
private Api24Impl() { }
// Performs the implicit cast from Notification.MessagingStyle to Notification.Style
@DoNotInline
static Notification.Style castToStyle(Notification.MessagingStyle messagingStyle) {
return messagingStyle;
}
}
```
#### Validating class verification
To verify that your library does not raise class verification failures, look for
`dex2oat` output during install time.
You can generate class verification logs from test APKs. Simply call the
class/method that should generate a class verification failure in a test.
The test APK will generate class verification logs on install.
```bash
# Enable ART logging (requires root). Note the 2 pairs of quotes!
adb root
adb shell setprop dalvik.vm.dex2oat-flags '"--runtime-arg -verbose:verifier"'
# Restart Android services to pick up the settings
adb shell stop && adb shell start
# Optional: clear logs which aren't relevant
adb logcat -c
# Install the app and check for ART logs
# This line is what triggers log lines, and can be repeated
adb install -d -r someApk.apk
# it's useful to run this _during_ install in another shell
adb logcat | grep 'dex2oat'
...
... I dex2oat : Soft verification failures in
```
#### Device-specific issues {#compat-oem}
Library code may work around device- or manufacturer-specific issues -- issues
not present in AOSP builds of Android -- *only* if a corresponding CTS test
and/or CDD policy is added to the next revision of the Android platform. Doing
so ensures that such issues can be detected and fixed by OEMs.
#### Handling `minSdkVersion` disparity {#compat-minsdk}
Methods that only need to be accessible on newer devices, including
`to<PlatformClass>()` methods, may be annotated with `@RequiresApi(<sdk>)` to
indicate they must not be called when running on older SDKs. This annotation is
enforced at build time by the `NewApi` lint check.
#### Handling `targetSdkVersion` behavior changes {#compat-targetsdk}
To preserve application functionality, device behavior at a given API level may
change based on an application's `targetSdkVersion`. For example, if an app with
`targetSdkVersion` set to API level 22 runs on a device with API level 29, all
required permissions will be granted at installation time and the run-time
permissions framework will emulate earlier device behavior.
Libraries do not have control over the app's `targetSdkVersion` and -- in rare
cases -- may need to handle variations in platform behavior. Refer to the
following pages for version-specific behavior changes:
* Android 14,
[API level 34](https://developer.android.com/about/versions/14/behavior-changes-14)
* Android 13,
[API level 33](https://developer.android.com/about/versions/13/behavior-changes-13)
* Android 12,
[API level 31](https://developer.android.com/about/versions/12/behavior-changes-12)
* Android 11,
[API level 30](https://developer.android.com/about/versions/11/behavior-changes-11)
* Android 10,
[API level 29](https://developer.android.com/about/versions/10/behavior-changes-10)
* Android Pie (9.0),
[API level 28](https://developer.android.com/about/versions/pie/android-9.0-changes-28)
* Android Oreo (8.0),
[API level 26](https://developer.android.com/about/versions/oreo/android-8.0-changes)
* Android Nougat(7.0),
[API level 24](https://developer.android.com/about/versions/nougat/android-7.0-changes)
* Android Lollipop (5.0),
[API level 21](https://developer.android.com/about/versions/lollipop/android-5.0-changes)
* Android KitKat (4.4),
[API level 19](https://developer.android.com/about/versions/kitkat/android-4.4#Behaviors)
#### Working around Lint issues {#compat-lint}
In rare cases, Lint may fail to interpret API usages and yield a `NewApi` error
and require the use of `@TargetApi` or `@SuppressLint('NewApi')` annotations.
Both of these annotations are strongly discouraged and may only be used
temporarily. They **must never** be used in a stable release. Any usage of these
annotation **must** be associated with an active bug, and the usage must be
removed when the bug is resolved.
#### Java 8+ APIs and core library desugaring {#compat-desugar}
While the DEX compiler (D8) supports
[API desugaring](https://developer.android.com/studio/write/java8-support-table)
to enable usage of Java 8+ APIs on a broader range of platform API levels, there
is currently no way for a library to express the toolchain requirements
necessary for desugaring to work as intended.
As of 2023-05-11, there is still a
[pending feature request](https://issuetracker.google.com/203113147) to allow
Android libraries to express these requirements.
Libraries **must not** rely on `coreLibraryDesugaring` to access Java language
APIs on earlier platform API levels. For example, `java.time.*` may only be used
in code paths targeting API level 26 and above.
### Delegating to API-specific implementations {#delegating-to-api-specific-implementations}
#### SDK-dependent reflection {#sdk-reflection}
Note: The
[BanUncheckedReflection](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lint-checks/src/main/java/androidx/build/lint/BanUncheckedReflection.kt)
lint check detects disallowed usages of reflection.
Starting in API level 28, the platform restricts which
[non-SDK interfaces](https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces)
can be accessed via reflection by apps and libraries. As a general rule, you
will **not** be able to use reflection to access hidden APIs on devices with
`SDK_INT` greater than `Build.VERSION_CODES.P` (28).
In cases where a hidden API is a constant value, **do not** inline the value.
Hidden APIs cannot be tested by CTS and carry no stability guarantees.
On earlier devices or in cases where an API is marked with
`@UnsupportedAppUsage`, reflection on hidden platform APIs is allowed **only**
when an alternative public platform API exists in a later revision of the
Android SDK. For example, the following implementation is allowed:
```java
public AccessibilityDelegate getAccessibilityDelegate(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Retrieve the delegate using a public API.
return v.getAccessibilityDelegate();
} else if (Build.VERSION.SDK_INT >= 11) {
// Retrieve the delegate by reflecting on a private field. If the
// field does not exist or cannot be accessed, this will no-op.
if (sAccessibilityDelegateField == null) {
try {
sAccessibilityDelegateField = View.class
.getDeclaredField("mAccessibilityDelegate");
sAccessibilityDelegateField.setAccessible(true);
} catch (Throwable t) {
sAccessibilityDelegateCheckFailed = true;
return null;
}
}
try {
Object o = sAccessibilityDelegateField.get(v);
if (o instanceof View.AccessibilityDelegate) {
return (View.AccessibilityDelegate) o;
}
return null;
} catch (Throwable t) {
sAccessibilityDelegateCheckFailed = true;
return null;
}
} else {
// There is no way to retrieve the delegate, even via reflection.
return null;
}
```
Calls to public APIs added in pre-release revisions *must* be gated using
`BuildCompat`:
```java
if (BuildCompat.isAtLeastQ()) {
// call new API added in Q
} else if (Build.SDK_INT.VERSION >= 23) {
// make a best-effort using APIs that we expect to be available
} else {
// no-op or best-effort given no information
}
```
### Inter-process communication {#ipc}
Protocols and data structures used for IPC must support interoperability between
different versions of libraries and should be treated similarly to public API;
however, AndroidX does not currently implement compatibility tracking for IPC.
We recommend the following, in order of preference:
1. Stable AIDL if (1) your project lives partially in the Android platform and
has access to Stable AIDL build rules and (2) you need to support Android's
`Parcelable` data types. The AndroidX workflow **does not** provide Stable
AIDL compilation or compatibility checks, so these would need to happen in
the platform build and the resulting `.java` files would need to be copied
out.
2. Protobuf if (1) your project needs to persist data to disk or (2) you need
interoperability with systems already using Protobuf. Similar to Stable
AIDL, the AndroidX workflow **does not** provide built-in support Protobuf
compilation or compatibility checks. It is possible to use a Proto plug-in,
but you will be responsible for bundling the runtime and maintaining
compatibility on your own.
3. `Bundle` if you have a very simple data model that is unlikely to change in
the future. `Bundle` has the weakest type safety and compatibility
guarantees of any recommendation, and it has many caveats that make it a
poor choice.
4. `VersionedParcelable` if your project is already using Versioned Parcelable
and is aware of its compatibility constraints.
We are currently evaluating Square's [Wire](https://github.com/square/wire) and
Google's [gRPC](https://grpc.io/) libraries for recommendation. If either of
these libraries meets your team's needs based on your own research, feel free to
use them.
**Do not** design your own serialization mechanism or wire format for disk
storage or inter-process communication. Preserving and verifying compatibility
is difficult and error-prone.
In all cases, **do not** expose your serialization mechanism in your API
surface. Neither Stable AIDL nor Protobuf generate stable language APIs.
#### Annotating unstable IPC
Once an API that relies on an IPC contract ships to production in an app, the
contract is locked in and must maintain compatibility to prevent crashing either
end of an inter-process communication channel.
Developers **should** annotate unstable IPC classes with a `@RequiresOptIn`
annotation explaining that they must not be used in production code. Libraries
**must not** opt-in to these annotations when such classes are referenced
internally, and should instead propagate the annotations to public API surfaces.
A single annotation for this purpose may be defined per library or atomic group:
```java
/**
* Parcelables and AIDL-generated classes bearing this annotation are not
* guaranteed to be stable and must not be used for inter-process communication
* in production.
*/
@RequiresOptIn
public @interface UnstableAidlDefinition {}
```
Generally speaking, at this point in time no libraries should have unstable
`Parcelable` classes defined in source code, but for completeness:
```java
@UnstableAidlDefinition
public class ResultReceiver implements Parcelable { ... }
```
AIDL definition files under `src/aidl` should use `@JavaPassthrough` with a
fully-qualified class name to annotate generated classes:
```java
@JavaPassthrough(annotation="@androidx.core.util.UnstableAidlDefinition")
oneway interface IResultReceiver {
void send(int resultCode, in Bundle resultData);
}
```
For Stable AIDL, the build system enforces per-CL compatibility guarantees. No
annotations are required for Stable AIDL definition files under
`src/stableAidl`.
#### Parcelable {#ipc-parcelable}
**Do not** implement `Parcelable` for any class that may be used for IPC or
otherwise exposed as public API. By default, `Parcelable` does not provide any
compatibility guarantees and will result in crashes if fields are added or
removed between library versions. If you are using Stable AIDL, you *may* use
AIDL-defined parcelables for IPC but not public API.
NOTE As of 2022/12/16, we are working on experimental support for compiling and
tracking Stable AIDL definitions within the AndroidX workflow.
#### Protobuf {#ipc-protobuf}
Developers **should** use protocol buffers for most cases. See
[Protobuf](#dependencies-protobuf) for more information on using protocol
buffers in your library. **Do** use protocol buffers if your data structure is
complex and likely to change over time. If your data includes `FileDescriptor`s,
`Binder`s, or other platform-defined `Parcelable` data structures, they will
need to be stored alongside the protobuf bytes in a `Bundle`.
NOTE We are currently investigating the suitability of Square's
[`wire` library](https://github.com/square/wire) for handling protocol buffers
in Android libraries. If adopted, it will replace `proto` library dependencies.
Libraries that expose their serialization mechanism in their API surface *will
not be able to migrate*.
#### Bundle {#ipc-bundle}
Developers **may** use `Bundle` in simple cases that require sending `Binder`s,
`FileDescriptor`s, or platform `Parcelable`s across IPC
([example](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java;l=820)).
Note that `Bundle` has several caveats:
- When running on Android S and below, accessing *any* entry in a `Bundle`
will result in the platform attempting to deserialize *every* entry. This
has been fixed in Android T and later with "lazy" bundles, but developers
should be careful when accessing `Bundle` on earlier platforms. If a single
entry cannot be loaded -- for example if a developer added a custom
`Parcelable` that doesn't exist in the receiver's classpath -- an exception
will be thrown when accessing *any* entry.
- On all platforms, library code that receives `Bundle`s data from outside the
process **must** read the data defensively. See previous note regarding
additional concerns for Android S and below.
- On all platforms, library code that sends `Bundle`s outside the process
*should* discourage clients from passing custom `Parcelable`s.
- `Bundle` provides no versioning and Jetpack provides no affordances for
tracking the keys or value types associated with a `Bundle`. Library owners
are responsible for providing their own system for guaranteeing wire format
compatibility between versions.
#### Communication protocols {#ipc-protocol}
Any communication prototcol, handshake, etc. must maintain compatibility
consistent with SemVer guidelines. Consider how your protocol will handle
addition and removal of operations or constants, compatibility-breaking changes,
and other modifications without crashing either the host or client process.