Android Framework-specific rules [FW]

These rules are about APIs, patterns, and data structures that are specific to APIs and functionality built into the Android framework (Bundles, Parcelables, etc.).

Intent builders should use the create*Intent() pattern

Creators for intents should use methods named createFooIntent().

Use Bundles instead of creating new general-purpose data structures

Avoid creating new general-purpose data structures to represent arbitrary key to typed value mappings. Instead, consider using Bundle.

This typically comes up when writing platform APIs that serve as communication channels between non-platform apps and services, where the platform does not read the data sent across the channel and the API contract may be partially defined outside of the platform (ex. in a Jetpack library).

In cases where the platform does read the data, avoid using Bundle and prefer a strongly-typed data class.

Parcelable implementations must have public CREATOR field

Parcelable inflation is exposed through CREATOR, not raw constructors. If a class implements Parcelable, then its CREATOR field must also public API and the class constructor taking a Parcel argument must be private.

Use CharSequence for UI strings

When a string will be presented in a user interface, use CharSequence to allow for Spannables.

If it’s just a key or some other non-user-visible label or value, String is fine.

Avoid using Enums

IntDefs must be used over enums in all platform APIs, and should be strongly considered in unbundled, library APIs. Only use enums when you are certain new values will not be added.

Benefits ofIntDef

  • Enables adding values over time
    • Kotlin when statements can fail at runtime if they become no-longer-exhaustive due to an added enum value in platform.
  • No class/objects used at runtime, only primitive
    • While R8 / Minfication can avoid this cost for unbundled library APIs, this optimization cannot affect platform API classes.

Benefits of Enum

  • Idiomatic language feature of Java, Kotlin
  • Enables exhaustive switch, when statement usage
    • Note - values must not change over time, see above
  • Clearly scoped, and discoverable naming
  • Enables compile time verification
    • e.g. a when statement in kotlin that returns a value
  • Is a functioning class that can implement interfaces, have static helpers, expose member/extension methods, fields.

Follow Android package layering hierarchy

The android.* package hierarchy has an implicit ordering, where lower-level packages cannot depend on higher-level packages.

Avoid referring to Google, other companies, and their products

The Android platform is an open-source project and aims to be vendor neutral. The API should be generic and equally usable by system integrators or applications with the requisite permissions.

Parcelable implementations should be final

Parcelable classes defined by the platform are always loaded from framework.jar, so it’s invalid for an app to try overriding a Parcelable implementation.

If the sending app extends a Parcelable, the receiving app won’t have the sender’s custom implementation to unpack with. Note about backward compatibility: if your class historically wasn’t final, but didn’t have a publicly available constructor, you still can mark it final.

Methods calling into system process should rethrow RemoteException as RuntimeException

RemoteException is typically thrown by internal AIDL, and indicates that the system process has died, or the app is trying to send too much data. In both cases, public API should rethrow as a RuntimeException to ensure that apps don’t accidentally persist security or policy decisions.

If you know the other side of a Binder call is the system process, this simple boilerplate code is the best-practice:

try {
    ...
} catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
}

Throw specific exceptions for API changes

Public API behaviors might change across API levels and cause app crashes (for instance to enforce new security policies).

When the API needs to throw for a request that was previously valid, throw a new specific exception instead of a generic one. For instance, ExportedFlagRequired instead of SecurityException (and ExportedFlagRequired can extend SecurityException).

It will help app developers and tools to more easily detect these API behavior changes.

Implement copy constructor instead of clone()

Use of the Java clone() method is strongly discouraged due to the lack of API guarantees provided by the Object class and difficulties inherent in extending classes that use clone(). Instead, use a copy constructor that takes an object of the same type.

/**
 * Constructs a shallow copy of {@code other}.
 */
public Foo(Foo other)

Classes that rely on a Builder for construction should consider adding a Builder copy constructor to allow modifications to the copy.

public class Foo {
    public static final class Builder {
        /**
         * Constructs a Foo builder using data from {@code other}.
         */
        public Builder(Foo other)

Use ParcelFileDescriptor over FileDescriptor.

The java.io.FileDescriptor object has a poor definition of ownership, which can result in obscure use-after-close bugs. Instead, APIs should return or accept ParcelFileDescriptor instances. Legacy code can convert between PFD and FD if needed using dup() or getFileDescriptor().

Avoid using odd-sized numerical values.

Avoid using short or byte values directly, since they often limit how you might be able to evolve the API in the future.

Avoid using BitSet.

java.util.BitSet is great for implementation but not for public API. It's mutable, requires an allocation for high-frequency method calls, and does not provide semantic meaning for what each bit represents.

For high-performance scenarios, use an int or long with @IntDef. For low-performance scenarios, consider a Set<EnumType>. For raw binary data, use byte[].

Prefer android.net.Uri.

android.net.Uri is the preferred encapsulation for URIs in Android APIs.

Avoid java.net.URI, because it is overly strict in parsing URIs, and never use java.net.URL, because its definition of equality is severely broken.

Hide annotations marked as @IntDef, @LongDef or @StringDef

Annotations marked as @IntDef, @LongDef or @StringDef denote a set of valid constants that can be passed to an API. However, when they are exported as APIs themselves, the compiler inlines the constants and only the (now useless) values remain in the annotation's API stub (for the platform) or JAR (for libraries).

As such, usages of these annotations must be marked @hide in the platform or @hide and RestrictTo.Scope.LIBRARY) in libraries. They must be marked @Retention(RetentionPolicy.SOURCE) in both cases to ensure they do not appear in API stubs or JARs.

/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
  STREAM_TYPE_FULL_IMAGE_DATA,
  STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}

When building the platform SDK and library AARs, a tool extracts the annotations and bundles them separately from the compiled sources. Android Studio reads this bundled format and enforces the type definitions.

Do not add new setting provider keys

Do not expose new keys from Settings.Global, Settings.System, and/or Settings.Secure.

Instead, add a proper getter and setter Java API in a relevant class, which is typically a “manager” class. Add a listener mechanism or a broadcast to notify clients of changes as needed.

SettingsProvider settings have a number of problems compared to getters/setters:

  • No type safety.
  • No unified way to provide a default value.
  • No proper way to customize permissions.
    • For example, it's not possible to protect your setting with a custom permission.
  • No proper way to add custom logic properly.
    • For example, it‘s not possible to change setting A’s value depending on setting B's value.

Example: Settings.Secure.LOCATION_MODE has existed for a long time, but the location team has deprecated it for a proper Java API LocationManager.isLocationEnabled() and the MODE_CHANGED_ACTION broadcast, which gave the team a lot more flexibility, and the semantics of the APIs are a lot clearer now.

Do not extend Activity and AsyncTask

AsyncTask is an implementation detail. Instead, expose a listener or, in androidx, a ListenableFuture API instead.

Activity subclasses are impossible to compose. Extending activity for your feature makes it incompatible with other features that require users to do the same. Instead, rely on composition by using tools such as LifecycleObserver.

Use the Context's getUser()

Classes bound to a Context, such as anything returned from Context.getSystemService() should use the user bound to the Context instead of exposing members that target specific users.

class FooManager {
  Context mContext;

  void fooBar() {
    mIFooBar.fooBarForUser(mContext.getUser());
  }
}
class FooManager {
  Context mContext;

  Foobar getFoobar() {
    // Bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBarForUser(Process.myUserHandle());
  }

  Foobar getFoobar() {
    // Also bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBar();
  }

  Foobar getFoobarForUser(UserHandle user) {
    mIFooBar.fooBarForUser(user);
  }
}

Exception: A method may accept a user argument if it accepts values that don't represent a single user, such as UserHandle.ALL.

Use UserHandle instead of plain ints

UserHandle is preferred to ensure type safety and avoid conflating user IDs with uids.

Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);

Where unavoidable, ints representing a user ID must be annotated with @UserIdInt.

Foobar getFoobarForUser(@UserIdInt int user);

Prefer listeners/callbacks to broadcast intents

Broadcast intents are very powerful, but they've resulted in emergent behaviors that can negatively impact system health, and so new broadcast intents should be added judiciously.

Here are some specific concerns which result in us discouraging the introduction of new broadcast intents:

  • When sending broadcasts without the FLAG_RECEIVER_REGISTERED_ONLY flag, they will force-start any applications which aren‘t already running. While this can sometimes be a desired outcome, it can result in stampeding of dozens of apps, negatively impacting system health. We’d recommend using alternative strategies, such as JobScheduler, to better coordinate when various preconditions are met.

  • When sending broadcasts, there is little ability to filter or adjust the content delivered to apps. This makes it difficult or impossible to respond to future privacy concerns, or introduce behavior changes based on the target SDK of the receiving app.

  • Since broadcast queues are a shared resource, they can become overloaded and may not result in timely delivery of your event. We've observed several broadcast queues in the wild which have an end-to-end latency of 10 minutes or longer.

For these reasons, we encourage new features to consider using listeners/callbacks or other facilities such as JobScheduler instead of broadcast intents.

In cases where broadcast intents still remain the ideal design, here are some best-practices that should be considered:

  • If possible, use Intent.FLAG_RECEIVER_REGISTERED_ONLY to limit your broadcast to apps that are already running. For example, ACTION_SCREEN_ON uses this design to avoid waking up apps.
  • If possible, use Intent.setPackage() or Intent.setComponent() to target the broadcast at a specific app of interest. For example, ACTION_MEDIA_BUTTON uses this design to focus on the current app handling playback controls.
  • If possible, define your broadcast as a <protected-broadcast> to ensure that malicious apps can't impersonate the OS.