Skip to content

Chapter 3: Feature Flags and aconfig

Large-scale software projects face an inherent contradiction: developers need to commit code to the mainline branch frequently to reduce merge conflicts, yet half-finished features must never reach end users. For over a decade, Android OEMs addressed this tension through long-lived release branches, cherry-pick marathons, and #ifdef-like compile-time switches scattered across thousands of files. The result was predictable -- merge debt, stale branches, and an ever-growing distance between what developers tested and what shipped.

Starting with Android 14 (API 34) and maturing significantly in Android 15 (API 35), the aconfig system introduces a unified, build-and-runtime feature flag infrastructure. It sits at the intersection of build policy, runtime configuration, code generation, and testing, touching nearly every layer of the platform. As of the Android 17 (API 37) tree, there are nearly 500 .aconfig declaration files spanning frameworks, system services, HALs, Mainline modules, and vendor partitions.

Android 17 advances the system on several fronts that this chapter covers in detail: a version-4 storage format that lays the groundwork for integer-valued flags (the flag_type field and value_int plumbing in the proto schema), a read-only Java optimization path that lets R8 collapse a flag package down to a single class, the removal of the standalone DeviceConfig code-generation template, and the migration of the runtime daemon (aconfigd-system) to a pure-Rust binary with an earlier init entry point. These changes are surfaced in their respective sections rather than collected in a single place, with a consolidated tour in section 3.9.

This chapter traces the entire feature flag pipeline: from the policy motivation behind trunk-stable development, through the .aconfig declaration format and the Soong module types that wire declarations into the build, to the Rust-based aconfig tool that generates type-safe Java, C++, and Rust accessor code, into the runtime flag resolution system backed by aconfigd and memory-mapped storage files, and finally through the testing infrastructure that lets engineers exercise every flag combination in unit and integration tests.


3.1 Feature Flag Architecture

3.1.1 Why Feature Flags?

The motivation for feature flags in AOSP is captured in a single phrase: trunk-stable development. Instead of isolating unreleased features on long-lived branches, all code lives on the mainline trunk, guarded by flags that can be flipped at build time or at runtime. This approach yields several benefits:

  1. Reduced merge conflicts. Every engineer works against the same tree. Features-in-progress are committed behind disabled flags, eliminating the need for feature branches that diverge over months.

  2. Gradual rollouts. A feature can be enabled for dogfood builds, then beta users, then a staged production rollout -- all without code changes. If a problem is detected, the flag is disabled server-side; no OTA required.

  3. Release decoupling. The release train can cut at any point on trunk. Features not ready for a particular release remain behind disabled flags; their code is present but inert.

  4. Consistent testing. CI can run the full test suite with flags in multiple combinations -- enabled, disabled, and mixed -- catching interactions that branch-based development misses entirely.

  5. Flag-guarded APIs. New public APIs can be annotated with @FlaggedApi, allowing the API surface to be conditionally visible depending on flag state. This is critical for Mainline modules that ship across multiple Android versions.

3.1.2 Trunk-Stable Development Model

The trunk-stable model changes the mental model for platform engineers:

Traditional Model:
  main ─── feature-branch-A ──┐
       ├── feature-branch-B ──┤── merge → release-branch
       └── feature-branch-C ──┘

Trunk-Stable Model:
  main ──── all features committed (behind flags) ──── cut release
            flag_a=disabled  flag_b=enabled  flag_c=disabled

In the trunk-stable model, the main branch is always in a releasable state. Features are enabled or disabled through flag configuration that is orthogonal to the code itself. The release configuration selects which flags are enabled for a given build target (e.g., bp4a, ap3a, trunk_staging).

3.1.3 Flag Types and Permissions

Flags in aconfig have two orthogonal dimensions:

State (enabled or disabled):

State Meaning
ENABLED The feature behind this flag is active.
DISABLED The feature behind this flag is inactive.

Permission (who can change the flag):

Permission Meaning
READ_ONLY The flag value is baked at build time. Cannot be overridden at runtime.
READ_WRITE The flag can be overridden at runtime via DeviceConfig or aconfigd.

Additionally, flags may be marked as is_fixed_read_only in their declaration. This is a stronger guarantee: the flag can never be changed from its declared default, not even by release configuration. The build system uses this to enable compile-time optimizations -- the R8 optimizer can completely eliminate dead code branches behind fixed read-only flags.

Until Android 17, every flag was implicitly boolean. Android 17 adds a flag type dimension to the declaration schema (FLAG_TYPE_BOOLEAN versus FLAG_TYPE_INTEGER), so that a flag can carry an integer payload rather than a mere on/off state. This is groundwork: the proto schema, the cache, the v4 storage format, and the parser all carry the integer plumbing, and declaring an integer flag is gated behind the RELEASE_ACONFIG_ENABLE_INT_FLAG build flag, but accessor code generation for integer flags is not yet wired. Sections 3.2.3 and 3.9 cover the type field in detail.

3.1.4 High-Level Architecture

The aconfig system spans build time and runtime:

flowchart TB
    subgraph "Build Time"
        A[".aconfig declarations"] --> B["aconfig create-cache"]
        C[".values overrides"] --> B
        B --> D["Cache (.pb)"]
        D --> E["aconfig create-java-lib"]
        D --> F["aconfig create-cpp-lib"]
        D --> G["aconfig create-rust-lib"]
        E --> H["Flags.java / FeatureFlags.java<br/>FeatureFlagsImpl.java"]
        F --> I["package.h / package.cc"]
        G --> J["lib.rs"]
        D --> K["aconfig create-storage"]
        K --> L["package_map / flag_map<br/>flag_val / flag_info"]
    end

    subgraph "Runtime"
        L --> M["aconfigd"]
        M --> N["/metadata/aconfig/"]
        N --> O["Memory-mapped<br/>storage files"]
        O --> P["FeatureFlagsImpl<br/>(generated code)"]
        P --> Q["Application code"]
    end

    style A fill:#e1f5fe
    style C fill:#e1f5fe
    style H fill:#c8e6c9
    style I fill:#c8e6c9
    style J fill:#c8e6c9
    style Q fill:#fff3e0

3.1.5 Containers

A container is a unit of software that is built and installed as a single artifact. The container concept is central to aconfig because flag storage files are organized per container. The main containers are:

  • system -- the system partition
  • system_ext -- the system_ext partition
  • vendor -- the vendor partition
  • product -- the product partition
  • APEX modules -- each APEX (e.g., com.android.configinfrastructure, com.android.wifi) is its own container
  • APKs -- independently released APKs are their own containers

The container determines where storage files are placed and how flag values are resolved at boot. A flag declared in one container cannot be read by code running in a different container without explicit export.


3.2 The aconfig System

3.2.1 The aconfig Tool

The aconfig binary is a Rust tool located at:

build/make/tools/aconfig/aconfig/

It provides several subcommands that form the backbone of the build-time pipeline:

Subcommand Purpose
create-cache Parse .aconfig declarations and .values overrides into a binary protobuf cache
create-java-lib Generate Java source from the cache
create-cpp-lib Generate C++ source from the cache
create-rust-lib Generate Rust source from the cache
create-storage Generate binary storage files (package_map, flag_map, flag_val, flag_info)
dump-cache Dump cache contents in various formats (text, protobuf, custom)

The tool is registered as a host binary in the Soong build system. The Go variable that downstream build rules reference is assigned with Aconfig = pctx.HostTool("aconfig") in build/soong/aconfig/init.go; the package's init() function separately calls pctx.HostBinToolVariable("aconfig", "aconfig") to publish the corresponding Ninja variable.

3.2.2 The .aconfig File Format

Flag declarations use a text-protobuf format defined by the flag_declarations message in build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto. Each .aconfig file declares a package, a container, and one or more flags:

// File: system/apex/apexd/apexd.aconfig

package: "com.android.apex.flags"
container: "system"

flag {
  name: "mount_before_data"
  namespace: "treble"
  description: "This flag controls if allowing mounting APEXes
                before the data partition"
  bug: "361701397"
  is_fixed_read_only: true
}

A more complex example from the ConfigInfrastructure module:

// File: packages/modules/ConfigInfrastructure/framework/flags.aconfig

package: "android.provider.flags"
container: "com.android.configinfrastructure"

flag {
  name: "new_storage_writer_system_api"
  namespace: "core_experiments_team_internal"
  description: "API flag for writing new storage"
  bug: "367765164"
  is_fixed_read_only: true
  is_exported: true
}

flag {
  name: "dump_improvements"
  namespace: "core_experiments_team_internal"
  description: "Added more information on dumpsys device_config"
  bug: "364399200"
  is_exported: true
}

flag {
  name: "enable_immediate_clear_override_bugfix"
  namespace: "core_experiments_team_internal"
  description: "Bugfix flag to allow clearing a local override
                immediately"
  bug: "387316969"
  metadata {
    purpose: PURPOSE_BUGFIX
  }
}

3.2.3 Declaration Fields

Each flag_declaration message supports these fields, as defined by the flag_declaration message in aconfig.proto (lines 80-110):

Field Type Required Description
name string Yes Snake_case identifier (e.g., mount_before_data)
namespace string Yes Organizational grouping for server-side management
description string Yes Human-readable purpose of the flag
bug string Yes Bug tracker ID (can be repeated)
is_fixed_read_only bool No If true, value cannot change at runtime or via release config
is_exported bool No If true, flag is accessible outside its container
metadata message No Additional metadata (purpose, storage backend)
type flag_type No Value type; defaults to FLAG_TYPE_UNSPECIFIED (treated as boolean). Added in Android 17

The type field (field 8) is new in Android 17. It is an enum:

enum flag_type {
  FLAG_TYPE_UNSPECIFIED = 0;  // assume boolean for backward compatibility
  FLAG_TYPE_BOOLEAN = 1;
  FLAG_TYPE_INTEGER = 2;
}

When a flag is FLAG_TYPE_INTEGER, its value is carried by the new value_int field on flag_value (field 5) and parsed_flag (field 14) rather than by the boolean state. Section 3.9 covers integer flags and their current build-flag gating in more depth.

The metadata message supports:

message flag_metadata {
  enum flag_purpose {
    PURPOSE_UNSPECIFIED = 0;
    PURPOSE_FEATURE = 1;
    PURPOSE_BUGFIX = 2;
  }

  enum flag_storage_backend {
    UNSPECIFIED = 0;
    ACONFIGD = 1;
    DEVICE_CONFIG = 2;
    NONE = 3;
  }

  optional flag_purpose purpose = 1;
  optional flag_storage_backend storage = 2;
}

The purpose field distinguishes between feature flags (which gate new functionality) and bugfix flags (which gate correctness fixes). The storage field selects the runtime backend: the new ACONFIGD-based memory-mapped storage or the legacy DEVICE_CONFIG (Settings-based) storage.

3.2.4 Naming Conventions

The aconfig system enforces strict naming rules (see aconfig.proto lines 26-57):

  • Flag names: lowercase snake_case, no consecutive underscores, no leading digits (e.g., adjust_rate is valid; AdjustRate and adjust__rate are not)
  • Package names: dot-delimited lowercase snake_case segments, each segment following the same rules (e.g., com.android.mypackage)
  • Namespaces: lowercase snake_case (e.g., core_experiments_team_internal)
  • Containers: lowercase, dot-delimited for APEX names (e.g., system, com.android.configinfrastructure)

3.2.5 Namespaces

Namespaces serve as the organizational unit for server-side flag management. They group related flags that are typically owned by the same team and managed through the same rollout pipeline. On the server side (Google's internal "Gantry" system), namespaces map to individual configuration surfaces that teams can independently manage.

A namespace does not correspond one-to-one with a package; multiple packages can share a namespace, and a package can contain flags in different namespaces. The runtime DeviceConfig system (legacy storage) uses the namespace as the property namespace for flag lookups:

DeviceConfig.getProperties("core_experiments_team_internal");

In the new aconfigd storage system, namespaces are still tracked in the metadata but are less central to the lookup path, since flags are indexed by package and name rather than namespace.

3.2.6 The Flag Values File

Flag values override the default state and permission of declared flags. They use the flag_value protobuf message format:

// File: build/make/tools/aconfig/aconfig/tests/first.values

flag_value {
    package: "com.android.aconfig.test"
    name: "disabled_ro"
    state: DISABLED
    permission: READ_ONLY
}
flag_value {
    package: "com.android.aconfig.test"
    name: "enabled_rw"
    state: ENABLED
    permission: READ_WRITE
}
flag_value {
    package: "com.android.aconfig.test"
    name: "enabled_fixed_ro"
    state: ENABLED
    permission: READ_ONLY
}

Real release configurations store values as .textproto files:

// File: build/release/aconfig/bp1a/com.android.internal.camera.flags/
//       single_thread_executor_flag_values.textproto

flag_value {
  package: "com.android.internal.camera.flags"
  name: "single_thread_executor"
  state: ENABLED
  permission: READ_ONLY
}

A flag_value may now also carry an integer payload through the value_int field (Android 17), used when the corresponding declaration is FLAG_TYPE_INTEGER.

3.2.7 Value Resolution Order

When aconfig create-cache processes a flag, it applies values in order:

flowchart LR
    A["Declaration default<br/>(DISABLED, READ_WRITE)"] --> B["Values file 1"]
    B --> C["Values file 2"]
    C --> D["Values file N"]
    D --> E["Final state<br/>in cache"]

    style A fill:#e1f5fe
    style E fill:#c8e6c9
  1. Declaration default: All flags start as DISABLED with READ_WRITE permission (defined in commands.rs line 74-75).
  2. Values files are applied in order. Later values override earlier ones.
  3. Build-time permission enforcement: If RELEASE_ACONFIG_REQUIRE_ALL_READ_ONLY is set, all flags are forced to READ_ONLY regardless of their declared permission.
  4. Fixed read-only enforcement: Flags with is_fixed_read_only: true cannot have their state overridden by values files.

Each value application is recorded as a tracepoint in the cache, allowing developers to trace exactly which file set each flag's final value:

message tracepoint {
  optional string source = 1;
  optional flag_state state = 2;
  optional flag_permission permission = 3;
}

3.3 Flag Code Generation

3.3.1 Generated File Structure

The aconfig tool generates type-safe accessor code in three languages. For each package, the generated code follows a consistent pattern across all languages: a public accessor facade, a runtime implementation, and a testing interface.

Java (generated by aconfig create-java-lib):

File Purpose
Flags.java Static accessor methods, one per flag
FeatureFlags.java Interface declaring all flag methods
FeatureFlagsImpl.java Runtime implementation (reads from storage)
CustomFeatureFlags.java Delegation wrapper for custom flag resolution
FakeFeatureFlagsImpl.java Test double for unit testing

C++ (generated by aconfig create-cpp-lib):

File Purpose
<package>.h Header with inline accessors and C-linkage functions
<package>.cc Implementation reading from storage at runtime

Rust (generated by aconfig create-rust-lib):

File Purpose
src/lib.rs Module with flag accessor functions

3.3.2 Code Generation Modes

The aconfig tool supports four code generation modes, controlled by the --mode parameter (defined in codegen/mod.rs lines 58-64):

pub enum CodegenMode {
    Exported,       // For flags visible outside their container
    ForceReadOnly,  // All flags treated as read-only
    Production,     // Normal production mode
    Test,           // Test mode with mutable flag state
}

These modes are selected via the mode property on java_aconfig_library, cc_aconfig_library, and rust_aconfig_library modules. The supported mode strings are (from codegen/java_aconfig_library.go line 31):

var aconfigSupportedModes = []string{
    "production", "test", "exported", "force-read-only",
}

3.3.3 Flags.java -- The Public API Surface

The generated Flags.java is the primary entry point for flag checks. It provides static methods that delegate to an internal FeatureFlags implementation.

Looking at the template in build/make/tools/aconfig/aconfig/templates/Flags.java.template:

// Generated code for package com.android.apex.flags

package com.android.apex.flags;

public final class Flags {

    /** @hide */
    public static final String FLAG_MOUNT_BEFORE_DATA =
        "com.android.apex.flags.mount_before_data";

    @com.android.aconfig.annotations.AssumeTrueForR8
    @com.android.aconfig.annotations.AconfigFlagAccessor
    public static boolean mountBeforeData() {
        return FEATURE_FLAGS.mountBeforeData();
    }

    private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
}

Key observations:

  1. Flag name constants follow the pattern FLAG_<UPPER_SNAKE_CASE> and store the fully-qualified flag name (package.flag_name).

  2. R8 annotations enable the R8 optimizer to assume a flag's value at compile time:

  3. @AssumeTrueForR8 -- on flags that are ENABLED and READ_ONLY
  4. @AssumeFalseForR8 -- on flags that are DISABLED and READ_ONLY
  5. Read-write flags have no such annotation, since their value can change at runtime.

  6. Method naming converts snake_case flag names to camelCase method names (e.g., mount_before_data becomes mountBeforeData()).

  7. In test mode, Flags.java additionally exposes setFeatureFlags() and unsetFeatureFlags() methods for injecting test doubles.

3.3.4 FeatureFlags.java -- The Interface

The generated interface declares one boolean method per flag:

package com.android.apex.flags;

/** @hide */
public interface FeatureFlags {
    @com.android.aconfig.annotations.AssumeTrueForR8
    @com.android.aconfig.annotations.AconfigFlagAccessor
    boolean mountBeforeData();
}

This interface is the contract that both the production and test implementations must satisfy.

3.3.5 FeatureFlagsImpl.java -- Runtime Resolution

The runtime implementation varies based on the storage backend. The aconfig tool selects among several templates:

New storage (aconfigd-based) -- template FeatureFlagsImpl.new_storage.java.template:

package com.example.flags;

import android.os.flagging.PlatformAconfigPackageInternal;
import android.util.Log;

/** @hide */
public final class FeatureFlagsImpl implements FeatureFlags {
    private static final String TAG = "FeatureFlagsImpl";
    private static volatile boolean isCached = false;
    private static boolean myReadWriteFlag = false;

    private void init() {
        try {
            PlatformAconfigPackageInternal reader =
                PlatformAconfigPackageInternal.load(
                    "com.example.flags", 0xABCD1234L);
            myReadWriteFlag = reader.getBooleanFlagValue(0);
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        } catch (LinkageError e) {
            // For mainline modules on older devices
            Log.e(TAG, e.toString());
        }
        isCached = true;
    }

    @Override
    public boolean myReadWriteFlag() {
        if (!isCached) {
            init();
        }
        return myReadWriteFlag;
    }

    @Override
    public boolean myReadOnlyFlag() {
        return true;  // Baked at build time
    }
}

For platform containers (system, system_ext, product, vendor), the implementation uses PlatformAconfigPackageInternal. For non-platform containers (APEX modules), it uses AconfigPackageInternal. Both read flag values from memory-mapped storage files under /metadata/aconfig/.

The package fingerprint (0xABCD1234L) is a SipHash13 of the package name, used to verify that the correct storage file is being read.

Legacy DeviceConfig storage -- template FeatureFlagsImpl.legacy_flag.internal.java.template. (Through Android 16 this path used a separate FeatureFlagsImpl.deviceConfig.java.template; Android 17 removed that file and folded the DeviceConfig runtime read into the legacy_flag.internal template -- see section 3.3.9.)

package com.example.flags;

import android.os.Binder;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;

/** @hide */
public final class FeatureFlagsImpl implements FeatureFlags {
    private static volatile boolean my_namespace_is_cached = false;
    private static boolean myReadWriteFlag = false;

    private void load_overrides_my_namespace() {
        final long ident = Binder.clearCallingIdentity();
        try {
            Properties properties =
                DeviceConfig.getProperties("my_namespace");
            myReadWriteFlag =
                properties.getBoolean(
                    Flags.FLAG_MY_READ_WRITE_FLAG, false);
        } catch (NullPointerException e) {
            throw new RuntimeException(
                "Cannot read value from namespace my_namespace "
                + "from DeviceConfig. It could be that the code "
                + "using flag executed before SettingsProvider "
                + "initialization. Please use fixed read-only flag "
                + "by adding is_fixed_read_only: true in flag "
                + "declaration.", e);
        } catch (SecurityException e) {
            // Skip loading for isolated processes
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
        my_namespace_is_cached = true;
    }

    @Override
    public boolean myReadWriteFlag() {
        if (!my_namespace_is_cached) {
            load_overrides_my_namespace();
        }
        return myReadWriteFlag;
    }

    @Override
    public boolean myReadOnlyFlag() {
        return true;  // Baked at build time
    }
}

The DeviceConfig-based implementation groups flag reads by namespace, performing a bulk getProperties() call to avoid per-flag IPC overhead.

Test mode -- template FeatureFlagsImpl.test_mode.java.template:

package com.example.flags;

/** @hide */
public final class FeatureFlagsImpl implements FeatureFlags {
    @Override
    public boolean myFlag() {
        throw new UnsupportedOperationException(
            "Method is not implemented.");
    }
}

In test mode, the real implementation throws on every access, forcing tests to explicitly set flag values through the fake implementation. This ensures tests never accidentally depend on production flag values.

3.3.6 FakeFeatureFlagsImpl.java -- Test Double

The FakeFeatureFlagsImpl is generated for non-exported libraries and provides a map-backed implementation for testing:

package com.example.flags;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;

/** @hide */
public class FakeFeatureFlagsImpl extends CustomFeatureFlags {
    private final Map<String, Boolean> mFlagMap = new HashMap<>();
    private final FeatureFlags mDefaults;

    public FakeFeatureFlagsImpl() {
        this(null);
    }

    public FakeFeatureFlagsImpl(FeatureFlags defaults) {
        super(null);
        mDefaults = defaults;
        for (String flagName : getFlagNames()) {
            mFlagMap.put(flagName, null);
        }
    }

    @Override
    protected boolean getValue(String flagName,
                               Predicate<FeatureFlags> getter) {
        Boolean value = this.mFlagMap.get(flagName);
        if (value != null) {
            return value;
        }
        if (mDefaults != null) {
            return getter.test(mDefaults);
        }
        throw new IllegalArgumentException(flagName + " is not set");
    }

    public void setFlag(String flagName, boolean value) {
        if (!this.mFlagMap.containsKey(flagName)) {
            throw new IllegalArgumentException(
                "no such flag " + flagName);
        }
        this.mFlagMap.put(flagName, value);
    }

    public void resetAll() {
        for (Map.Entry entry : mFlagMap.entrySet()) {
            entry.setValue(null);
        }
    }
}

This class enables unit tests to set specific flag values without requiring a running system or DeviceConfig provider.

3.3.7 CustomFeatureFlags.java -- Delegation Wrapper

The CustomFeatureFlags class provides a delegation pattern allowing callers to inject custom flag resolution logic:

package com.example.flags;

import java.util.function.BiPredicate;
import java.util.function.Predicate;

/** @hide */
public class CustomFeatureFlags implements FeatureFlags {
    private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl;

    public CustomFeatureFlags(
            BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) {
        mGetValueImpl = getValueImpl;
    }

    @Override
    public boolean myFlag() {
        return getValue(Flags.FLAG_MY_FLAG,
            FeatureFlags::myFlag);
    }

    public boolean isFlagReadOnlyOptimized(String flagName) {
        if (mReadOnlyFlagsSet.contains(flagName) &&
            isOptimizationEnabled()) {
            return true;
        }
        return false;
    }

    @com.android.aconfig.annotations.AssumeTrueForR8
    private boolean isOptimizationEnabled() {
        return false;
    }

    protected boolean getValue(String flagName,
                               Predicate<FeatureFlags> getter) {
        return mGetValueImpl.test(flagName, getter);
    }
}

The isOptimizationEnabled() method is marked @AssumeTrueForR8 but returns false. This is an intentional pattern: R8 can assume this returns true, enabling it to optimize away the isFlagReadOnlyOptimized checks for read-only flags in release builds, while the actual runtime behavior preserves the dynamic check.

3.3.8 ExportedFlags.java -- Simplified External API

For exported flag libraries (mode: "exported" with single_exported_file: true), the aconfig tool generates an additional ExportedFlags.java that provides a simplified API for external consumers (apps built outside the platform):

// Generated: ExportedFlags.java
package com.example.flags;

import android.os.Build;

public class ExportedFlags {

    public static boolean myExportedFlag() {
        if (Build.VERSION.SDK_INT >= 36) {
            return true;  // Finalized at API level 36
        }
        return Flags.myExportedFlag();
    }
}

This class provides stable flag accessors that include SDK version checks for finalized flags, ensuring backward compatibility when apps target multiple Android versions. The @Deprecated annotation is applied to the original Flags, FeatureFlags, CustomFeatureFlags, and FakeFeatureFlagsImpl classes to encourage migration to ExportedFlags.

The SDK level baked into the check is not a constant -- it is the API level at which the flag was actually finalized. The condition is produced by ApiLevel::conditional() in build/make/tools/aconfig/convert_finalized_flags/src/lib.rs, which reads the finalized-flags records (e.g. prebuilts/sdk/<N>/finalized-flags.txt). Android 17 extends this for minor SDK versions: for levels at or above Baklava the generated condition becomes a dual check against both the major and minor SDK, Build.VERSION.SDK_INT >= 36 && Build.VERSION.SDK_INT_FULL >= <level>, where SDK_INT_FULL encodes the minor version (the multiplier is 100000). This path is gated by the RELEASE_ACONFIG_SUPPORT_MINOR_SDK build flag. Independently, RELEASE_ACONFIG_GENERATE_CHECKS_SDK_ANNOTATION makes the generator emit an @androidx.annotation.ChecksSdkIntAtLeast annotation on each finalized exported getter so static analysis tools understand the version gate.

3.3.9 FeatureFlagsImpl Template Selection

The aconfig Java codegen selects from four FeatureFlagsImpl templates based on the code generation mode, whether the library is exported, and which storage backend the package uses. In Android 17 the selection logic in the add_feature_flags_impl_template function (codegen/java.rs) is:

  1. Test mode (checked first, overrides everything else) -- uses FeatureFlagsImpl.test_mode.java.template (throws on every access).
  2. Exported library -- uses FeatureFlagsImpl.exported.java.template. Exported codegen always relies on new storage; the generator asserts that exported flags do not use the DeviceConfig backend.
  3. DeviceConfig storage (use_device_config, non-exported) -- uses FeatureFlagsImpl.legacy_flag.internal.java.template, which reads each flag via DeviceConfig.getProperties() / getBoolean().
  4. New aconfigd storage (the default, non-exported) -- uses FeatureFlagsImpl.new_storage.java.template, which reads from memory-mapped files via PlatformAconfigPackageInternal / AconfigPackageInternal.

Android 17 removed the previously separate FeatureFlagsImpl.deviceConfig.java.template; the DeviceConfig path now shares the legacy_flag.internal template. The complete template inventory in build/make/tools/aconfig/aconfig/templates/ (13 files) is:

CustomFeatureFlags.java.template
ExportedFlags.java.template
FakeFeatureFlagsImpl.java.template
FeatureFlags.java.template
FeatureFlagsImpl.exported.java.template
FeatureFlagsImpl.legacy_flag.internal.java.template
FeatureFlagsImpl.new_storage.java.template
FeatureFlagsImpl.test_mode.java.template
Flags.java.template
cpp_exported_header.template
cpp_source_file.template
rust.template
rust_test.template

The template engine used is TinyTemplate (a Rust crate), with template directives like {{ if condition }}, {{ for item in list }}, and {variable} substitution.

When the read-only Java optimization is active (Android 17, governed by the RELEASE_ACONFIG_OPTIMIZE_READ_ONLY_JAVA build flag) the generator can take an even more aggressive shortcut: read-only getters in Flags.java return their default value directly, and when impl-interface removal is also allowed (see section 3.6.6) the FeatureFlags, FeatureFlagsImpl, CustomFeatureFlags, and FakeFeatureFlagsImpl classes can be dropped entirely, collapsing a package down to a single Flags class. Section 3.9 traces this path.

3.3.10 C++ Code Generation

For C++, the generated code follows a provider pattern. The header declares an abstract flag_provider_interface with virtual methods for each flag:

// Generated: com_android_aconfig_test.h

#pragma once

#ifndef COM_ANDROID_ACONFIG_TEST
#define COM_ANDROID_ACONFIG_TEST(FLAG) \
    COM_ANDROID_ACONFIG_TEST_##FLAG
#endif

#ifndef COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO
#define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO true
#endif

#ifdef __cplusplus

#include <memory>

namespace com::android::aconfig::test {

class flag_provider_interface {
public:
    virtual ~flag_provider_interface() = default;
    virtual bool enabled_fixed_ro() = 0;
    virtual bool disabled_rw() = 0;
};

extern std::unique_ptr<flag_provider_interface> provider_;

// Fixed read-only: resolved at compile time via macro
constexpr inline bool enabled_fixed_ro() {
    return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
}

// Read-write: delegates to provider at runtime
inline bool disabled_rw() {
    return provider_->disabled_rw();
}

}  // namespace com::android::aconfig::test

extern "C" {
#endif

bool com_android_aconfig_test_enabled_fixed_ro();
bool com_android_aconfig_test_disabled_rw();

#ifdef __cplusplus
}
#endif

Key design decisions in the C++ codegen:

  1. Fixed read-only flags become constexpr inline functions that return a preprocessor macro value. This enables the compiler to eliminate dead code at compile time.

  2. Read-write flags go through a provider_ pointer that is initialized at runtime from memory-mapped storage.

  3. C-linkage functions (extern "C") are provided for consumption from C code and JNI.

  4. [[clang::no_destroy]] annotation is applied to the provider pointer to avoid destruction-order issues in thread-safe contexts.

  5. In test mode, each flag also gets a setter function (void disabled_rw(bool val)) and a reset_flags() function for test cleanup.

3.3.11 Rust Code Generation

For Rust, the generated code uses a provider trait pattern similar to C++:

// Generated: src/lib.rs

pub fn enabled_fixed_ro() -> bool {
    true  // Fixed read-only
}

pub fn disabled_rw() -> bool {
    // Read from storage via provider
    PROVIDER.disabled_rw()
}

In test mode, Rust flags use a mutable static (behind a mutex) that tests can set and reset. The generated test code uses a thread-local provider to avoid interference between parallel tests.

3.3.12 The Code Generation Pipeline

The complete pipeline from declaration to usable library:

flowchart TB
    A[".aconfig file"] --> B["aconfig_declarations<br/>(Soong module)"]
    V[".values overrides"] --> VS["aconfig_values<br/>(Soong module)"]
    VS --> VSet["aconfig_value_set<br/>(Soong module)"]
    VSet --> RC["RELEASE_ACONFIG_VALUE_SETS<br/>(release config)"]
    RC --> B

    B -->|"create-cache"| Cache["aconfig-cache.pb"]

    Cache --> JL["java_aconfig_library<br/>(Soong module)"]
    Cache --> CL["cc_aconfig_library<br/>(Soong module)"]
    Cache --> RL["rust_aconfig_library<br/>(Soong module)"]

    JL -->|"create-java-lib"| JSrc["Java source .jar"]
    CL -->|"create-cpp-lib"| CSrc[".h + .cc files"]
    RL -->|"create-rust-lib"| RSrc["lib.rs"]

    JSrc --> App["Your Java/Kotlin module"]
    CSrc --> NApp["Your C++ module"]
    RSrc --> RApp["Your Rust module"]

    style A fill:#e1f5fe
    style V fill:#e1f5fe
    style Cache fill:#fff9c4
    style JSrc fill:#c8e6c9
    style CSrc fill:#c8e6c9
    style RSrc fill:#c8e6c9

3.4 Flag Storage and Runtime

3.4.1 Storage Architecture Overview

The aconfig system supports two storage backends for runtime flag resolution, selected per-flag through the metadata.storage field in declarations:

flowchart TB
    subgraph "New Storage (aconfigd)"
        A1["Build: aconfig create-storage"] --> A2["Binary files:<br/>package_map, flag_map,<br/>flag_val, flag_info"]
        A2 --> A3["aconfigd-system<br/>(init service)"]
        A3 --> A4["/metadata/aconfig/<br/>mmap'd files"]
        A4 --> A5["Generated code<br/>(PlatformAconfigPackageInternal)"]
    end

    subgraph "Legacy Storage (DeviceConfig)"
        B1["SettingsProvider<br/>database"] --> B2["DeviceConfig API"]
        B2 --> B3["Generated code<br/>(DeviceConfig.getProperties)"]
    end

    style A4 fill:#c8e6c9
    style B1 fill:#fff9c4

3.4.2 New Storage: aconfigd and Memory-Mapped Files

The new storage system was introduced to address performance and boot-time limitations of the DeviceConfig approach. It consists of four binary file types, generated at build time by aconfig create-storage:

File Type Content
package_map Maps package names to their offset ranges in the flag files
flag_map Maps flag names (within a package) to offsets in flag_val
flag_val Compact array of boolean flag values
flag_info Metadata about each flag (permissions, attributes)

These files are defined by the storage_file_info proto in build/make/tools/aconfig/aconfig_storage_file/protos/aconfig_storage_metadata.proto:

message storage_file_info {
  optional uint32 version = 1;
  optional string container = 2;
  optional string package_map = 3;
  optional string flag_map = 4;
  optional string flag_val = 5;
  optional string flag_info = 6;
  optional int64 timestamp = 7;
}

At boot time, the aconfigd-system service initializes the storage. In Android 17 aconfigd-system is a pure-Rust binary (a rust_binary Soong module in system/server_configurable_flags/aconfigd/Android.bp); the earlier enable_full_rust_system_aconfigd migration flag has been removed now that the Rust daemon is the only implementation.

# From system/server_configurable_flags/aconfigd/aconfigd.rc

service early_system_aconfigd_platform_init
    /system/bin/aconfigd-system early-platform-init
    class core
    user system
    group system
    oneshot
    disabled
    file /dev/kmsg w

on early-init
    mkdir /metadata/aconfig 0775 root system
    mkdir /metadata/aconfig/flags 0770 root system
    mkdir /metadata/aconfig/maps 0775 root system
    mkdir /metadata/aconfig/boot 0775 root system
    exec_start early_system_aconfigd_platform_init

The same mkdir block also runs under an on post-fs trigger, which then exec_starts the system_aconfigd_platform_init service. The early-platform-init entry point is gated behind a runtime check (enable_earlier_aconfigd()) and writes an /metadata/aconfig/early_init_done marker once it has run, so platform storage can be available earlier in boot than before.

The storage files are memory-mapped read-only by client processes. The constant STORAGE_LOCATION in aconfig_storage_read_api/src/lib.rs (line 62) defines the root path:

pub const STORAGE_LOCATION: &str = "/metadata/aconfig";

3.4.3 Storage Read API

The aconfig_storage_read_api crate provides four core functions for reading from the memory-mapped storage files:

// 1. Get package read context (package offset info)
pub fn get_package_read_context(
    container: &str, package: &str
) -> Result<Option<PackageReadContext>>

// 2. Get flag read context (flag offset within package)
pub fn get_flag_read_context(
    container: &str, package_id: u32, flag: &str
) -> Result<Option<FlagReadContext>>

// 3. Read a boolean flag value at a global offset
pub fn get_boolean_flag_value(
    container: &str, offset: u32
) -> Result<bool>

// 4. Get storage file version
pub fn get_storage_file_version(
    file_path: &str
) -> Result<u32>

Android 17 adds a fifth core reader, get_int64_flag_value(file, index) -> Result<i64>, for the new integer flags, alongside get_flag_attribute for reading a flag's info bits. The integer reader is currently Rust-only; there is no corresponding cxx::bridge query for it yet, so C++ generated code still reads only booleans.

These are low-level APIs intended only for use by generated code. Application developers should never call them directly.

The read path for a single flag:

sequenceDiagram
    participant App as Application Code
    participant Flags as Flags.java
    participant Impl as FeatureFlagsImpl
    participant Reader as AconfigPackageInternal
    participant Storage as /metadata/aconfig/

    App->>Flags: Flags.myFlag()
    Flags->>Impl: FEATURE_FLAGS.myFlag()

    alt First access (not cached)
        Impl->>Reader: load(package, fingerprint)
        Reader->>Storage: mmap(package_map)
        Reader->>Storage: mmap(flag_map)
        Reader->>Storage: mmap(flag_val)
        Reader-->>Impl: flag values
        Impl->>Impl: Cache values locally
    end

    Impl-->>Flags: true/false
    Flags-->>App: true/false

3.4.4 Storage File Internals

The four binary storage files use a versioned format with hash-table-based lookups. The file format is defined in build/make/tools/aconfig/aconfig_storage_file/src/lib.rs.

Package Map (package_map):

The package map uses a hash table to map package names to their metadata. Each entry contains:

pub struct PackageTableNode {
    pub package_name: String,        // e.g., "com.android.apex.flags"
    pub package_id: u32,             // Unique ID within this container
    pub fingerprint: u64,            // SipHash13 of flag names (v2+)
    pub redact_exported_reads: bool, // v3: redact exported-flag reads
    pub boolean_start_index: u32,    // Offset into flag_val for this package
    pub int_start_index: u32,        // v4: offset of this package's int flags
    pub next_offset: Option<u32>,    // Hash collision chain
}

The int_start_index field is new in Android 17's version-4 format (it is only serialized when the v4 writer is selected); it gives the offset of the package's first integer flag, mirroring boolean_start_index for booleans.

The hash table size is chosen from a set of prime numbers (HASH_PRIMES array) to minimize collisions:

pub(crate) const HASH_PRIMES: [u32; 29] = [
    7, 17, 29, 53, 97, 193, 389, 769, 1543, 3079,
    6151, 12289, 24593, 49157, 98317, 196613, ...
];

Flag Map (flag_map):

The flag map uses a separate hash table to map (package_id, flag_name) pairs to flag metadata:

pub struct FlagTableNode {
    pub package_id: u32,
    pub flag_name: String,
    pub flag_type: StoredFlagType,  // ReadOnlyBoolean, ReadWriteBoolean,
                                     // FixedReadOnlyBoolean
    pub flag_index: u16,            // Index within the package's range
    pub next_offset: Option<u32>,
}

The flag_type distinguishes between:

  • ReadOnlyBoolean -- value set at build time, not overridable at runtime
  • ReadWriteBoolean -- value can be overridden at runtime
  • FixedReadOnlyBoolean -- value permanently fixed, enables compiler optimizations

Android 17's version-4 format adds three integer counterparts to the StoredFlagType enum -- ReadWriteInt64, ReadOnlyInt64, and FixedReadOnlyInt64 -- alongside a FlagValueType enum (Boolean, Int64) that classifies how the value is stored. These variants are only used when the v4 parser is enabled.

Flag Value (flag_val):

The flag value file is a compact array of boolean values. Each flag occupies one byte (not one bit) for efficient random access without bit manipulation. The offset for a specific flag is:

offset = package.boolean_start_index + flag.flag_index

Flag Info (flag_info):

The flag info file stores attribute bitmasks for each flag:

pub enum FlagInfoBit {
    HasServerOverride = 1 << 0,  // 0x01
    IsReadWrite       = 1 << 1,  // 0x02
    HasLocalOverride  = 1 << 2,  // 0x04
}

These bits track whether a flag is read-write and whether it has been overridden by server-side configuration or local aflags commands.

Storage file versions are encoded as the first four bytes of each file. The current version scheme:

Version Features
1 Basic package/flag maps and value storage
2 Added package fingerprints (SipHash13 of flag names)
3 Added exported read redaction support
4 Added integer flags (Android 17)

The default write version is 2 (DEFAULT_FILE_VERSION). The maximum supported read version is conditional in Android 17: MAX_SUPPORTED_FILE_VERSION = if cfg!(enable_parse_v4) { 4 } else { 3 }. The v4 format adds the integer-flag storage discussed above -- the package node's int_start_index, the Int64 StoredFlagType variants, and the flag-info header's num_int_flags / int_flag_offset fields plus the int_nodes list. Whether v4 is written and parsed is driven by the RELEASE_ACONFIG_PARSE_V4 build flag (which sets the enable_parse_v4 Rust cfg).

3.4.5 Package Fingerprint

Each package has a fingerprint computed using SipHash13 (from aconfig_storage_file/src/sip_hasher13.rs). The fingerprint is computed from the sorted list of flag names within the package. It serves two purposes:

  1. Integrity verification: generated code includes the fingerprint and verifies it at runtime against the storage file, detecting mismatches between code and storage.

  2. Cache invalidation: if a flag is added or removed from a package, the fingerprint changes, ensuring the generated code re-reads from storage.

The fingerprint appears in generated code as a hex literal:

PlatformAconfigPackageInternal reader =
    PlatformAconfigPackageInternal.load(
        "com.example.flags", 0xABCD1234L);

3.4.6 CXX Interop Layer

The storage read API is implemented in Rust but needs to be callable from C++ (for cc_aconfig_library generated code). The aconfig_storage_read_api crate uses cxx::bridge to generate C++ bindings:

#[cxx::bridge]
mod ffi {
    pub struct PackageReadContextQueryCXX {
        pub query_success: bool,
        pub error_message: String,
        pub package_exists: bool,
        pub package_id: u32,
        pub boolean_start_index: u32,
        pub fingerprint: u64,
    }

    pub struct FlagReadContextQueryCXX {
        pub query_success: bool,
        pub error_message: String,
        pub flag_exists: bool,
        pub flag_type: u16,
        pub flag_index: u16,
    }

    pub struct BooleanFlagValueQueryCXX {
        pub query_success: bool,
        pub error_message: String,
        pub flag_value: bool,
    }

    extern "Rust" {
        pub fn get_package_read_context_cxx(
            file: &[u8], package: &str,
        ) -> PackageReadContextQueryCXX;

        pub fn get_flag_read_context_cxx(
            file: &[u8], package_id: u32, flag: &str,
        ) -> FlagReadContextQueryCXX;

        pub fn get_boolean_flag_value_cxx(
            file: &[u8], offset: u32,
        ) -> BooleanFlagValueQueryCXX;
    }
}

Each query returns a result struct with an explicit query_success field and error_message, avoiding Rust's Result type which does not translate directly across the FFI boundary. The flag_type is encoded as a u16 for C++ compatibility.

3.4.7 The aconfigd Service Architecture

The aconfigd service is split into two binaries for security and updatability:

Binary Location Purpose
aconfigd-system /system/bin/aconfigd-system Platform flag initialization
aconfigd-mainline /apex/com.android.configinfrastructure/bin/ Mainline module flag handling

The system instance runs as three separate one-shot services defined in system/server_configurable_flags/aconfigd/aconfigd.rc:

service early_system_aconfigd_platform_init
    /system/bin/aconfigd-system early-platform-init
    class core
    user system
    group system
    oneshot
    disabled

service system_aconfigd_platform_init
    /system/bin/aconfigd-system platform-init
    class core
    user system
    group system
    oneshot
    disabled

service system_aconfigd_socket_service
    /system/bin/aconfigd-system start-socket
    class core
    user system
    group system
    oneshot
    disabled
    socket aconfigd_system stream 666 system system

Boot sequence:

sequenceDiagram
    participant Init as init
    participant Early as early_system_aconfigd
    participant AconfigD as system_aconfigd
    participant Socket as aconfigd_socket

    Note over Init: early-init phase
    Init->>Init: mkdir /metadata/aconfig/*
    Init->>Early: exec_start early-platform-init
    Early->>Early: Load boot storage records
    Early-->>Init: Done

    Note over Init: post-fs phase
    Init->>AconfigD: exec_start platform-init
    AconfigD->>AconfigD: Initialize platform<br/>storage files
    AconfigD-->>Init: Done

    Note over Init: Later (socket service)
    Init->>Socket: start-socket
    Socket->>Socket: Listen on<br/>aconfigd_system socket
    Socket->>Socket: Handle override requests

The socket service handles runtime flag override requests. Internally (from system/server_configurable_flags/aconfigd/src/aconfigd_commands.rs), it creates an Aconfigd instance and processes requests through a Unix domain socket:

const ACONFIGD_SOCKET: &str = "aconfigd_system";
const ACONFIGD_ROOT_DIR: &str = "/metadata/aconfig";
const STORAGE_RECORDS: &str =
    "/metadata/aconfig/storage_records.pb";
const PLATFORM_STORAGE_RECORDS: &str =
    "/metadata/aconfig/platform_storage_records.pb";

pub fn start_socket() -> Result<()> {
    let fd = rustutils::sockets::
        android_get_control_socket(ACONFIGD_SOCKET)?;
    let listener = UnixListener::from(fd);
    // Android 17 selects the records file at runtime:
    let records = if enable_aconfigd_from_mainline() {
        PLATFORM_STORAGE_RECORDS
    } else {
        STORAGE_RECORDS
    };
    let mut aconfigd = Aconfigd::new(
        Path::new(ACONFIGD_ROOT_DIR),
        Path::new(records));
    aconfigd.initialize_from_storage_record()?;

    for stream in listener.incoming() {
        match stream {
            Ok(mut stream) => {
                aconfigd.handle_socket_request_from_stream(
                    &mut stream)?;
            }
            Err(errmsg) => {
                error!("failed to listen: {:?}", errmsg);
            }
        }
    }
    Ok(())
}

The new platform_storage_records.pb (and the enable_aconfigd_from_mainline() switch that selects it) reflect Android 17's split between platform-owned storage records and the records the Mainline aconfigd-mainline daemon manages.

The /metadata/aconfig/ directory structure at runtime:

/metadata/aconfig/
    storage_records.pb          # Index of all storage files
    platform_storage_records.pb # Platform-only records
    maps/
        system.package.map      # Per-container package maps
        system.flag.map
        com.android.wifi.package.map
        com.android.wifi.flag.map
        ...
    flags/
        system.val              # Per-container flag values
        system.info
        com.android.wifi.val
        com.android.wifi.info
        ...
    boot/
        system.val              # Boot-time snapshots
        system.info
        ...

3.4.8 Legacy Storage: DeviceConfig and Settings.Global

Before the aconfigd system, flags were stored in Android's DeviceConfig framework, which ultimately reads from the settings_config table in the Settings.Global content provider. This approach has several limitations:

  1. Boot ordering dependency: DeviceConfig requires SettingsProvider to be running. Flags needed before SettingsProvider initialization cannot use this backend.

  2. IPC overhead: Each DeviceConfig.getProperties() call involves a Binder IPC to the SettingsProvider process.

  3. No atomic multi-flag reads: While getProperties() returns all flags in a namespace atomically, cross-namespace reads are not atomic.

  4. Permission model: DeviceConfig access requires specific SELinux permissions that not all processes have.

The generated code for DeviceConfig storage includes explicit error handling for these cases:

try {
    Properties properties =
        DeviceConfig.getProperties("my_namespace");
    myFlag = properties.getBoolean(
        Flags.FLAG_MY_FLAG, false);
} catch (NullPointerException e) {
    throw new RuntimeException(
        "Cannot read value from namespace my_namespace "
        + "from DeviceConfig. It could be that the code "
        + "using flag executed before SettingsProvider "
        + "initialization. Please use fixed read-only "
        + "flag by adding is_fixed_read_only: true in "
        + "flag declaration.", e);
} catch (SecurityException e) {
    // For isolated process case, skip loading
}

3.4.9 Flag Value Resolution at Runtime

The complete resolution chain for a flag's value at runtime:

flowchart TB
    A["Build-time default<br/>(from .aconfig declaration)"] --> B{"Release config<br/>override?"}
    B -->|Yes| C["Override value from<br/>.values files"]
    B -->|No| A2["Default value"]
    C --> D{"is_fixed_read_only?"}
    A2 --> D
    D -->|Yes| E["Value frozen in<br/>binary image"]
    D -->|No| F{"READ_WRITE?"}
    F -->|No| E
    F -->|Yes| G{"Runtime override<br/>from aconfigd?"}
    G -->|Yes| H["Runtime override value"]
    G -->|No| I["Build-time value<br/>(READ_WRITE default)"]
    E --> J["Final value returned<br/>to caller"]
    H --> J
    I --> J

    style E fill:#c8e6c9
    style J fill:#fff3e0

Read-only flags (including is_fixed_read_only) are fully resolved at build time. The generated code returns a constant:

public boolean myFixedFlag() {
    return true;  // Baked at build time, never changes
}

Read-write flags require a runtime lookup. Their build-time value serves as the default if no runtime override is present.

3.4.10 The aflags CLI Tool

The aflags binary is a device-side tool for inspecting and manipulating flag values. The on-device aflags (build/make/tools/aconfig/aflags/src/main.rs) is a thin shim that delegates to the updatable aflags_updatable binary in the ConfigInfrastructure APEX, where the real subcommand logic lives (packages/modules/ConfigInfrastructure/aflags/src/main.rs):

// From build/make/tools/aconfig/aflags/src/main.rs

fn invoke_updatable_aflags() {
    let updatable_command =
        "/apex/com.android.configinfrastructure/bin/aflags_updatable";
    // ... delegate all arguments to updatable binary
}

Common aflags commands:

# List all flags and their values
adb shell aflags list

# Filter the list to a container
adb shell aflags list --container system

# Override a flag value (read-write flags only)
adb shell aflags enable com.android.apex.flags.mount_before_data

# Clear an override (the subcommand is "unset", not "clear")
adb shell aflags unset com.android.apex.flags.mount_before_data

The enable, disable, and unset subcommands accept an -i/--immediate flag. Android 17 adds two listing capabilities: aflags list --format proto emits a Base64-encoded ProtoFlagList (gated by the android.provider.flags.aflags_list_proto flag), and, when the aflags_list_mainline_beta flag is set, aflags list also merges Mainline Beta flags read from device_config storage.


3.5 Flag Lifecycle

3.5.1 Lifecycle Phases

Every flag follows a predictable lifecycle from creation to cleanup:

stateDiagram-v2
    [*] --> Development: Declare flag
    Development --> Testing: Feature complete
    Testing --> Launch: Approved
    Launch --> Cleanup: Feature stable
    Cleanup --> [*]: Flag removed

    state Development {
        [*] --> CodingBehindFlag
        CodingBehindFlag --> LocalTesting
        LocalTesting --> CodingBehindFlag
    }

    state Testing {
        [*] --> DogfoodEnabled
        DogfoodEnabled --> BetaEnabled
        BetaEnabled --> StagedRollout
    }

    state Launch {
        [*] --> EnabledForAll
        EnabledForAll --> MonitorMetrics
    }

    state Cleanup {
        [*] --> RemoveChecks
        RemoveChecks --> DeleteDeclaration
        DeleteDeclaration --> RemoveDead
    }

3.5.2 Phase 1: Development

During development, a flag is:

  • Declared in a .aconfig file with DISABLED default state
  • Guarded in code with if (Flags.myNewFeature()) { ... }
  • Tested locally by overriding with aflags or build config

The developer's Android.bp adds the flag declaration and codegen library:

aconfig_declarations {
    name: "my-feature-flags",
    package: "com.android.myfeature.flags",
    container: "system",
    srcs: ["my_feature.aconfig"],
}

java_aconfig_library {
    name: "my-feature-flags-java",
    aconfig_declarations: "my-feature-flags",
}

3.5.3 Phase 2: Testing

As the feature matures:

  • The release configuration for dogfood/beta builds sets the flag to ENABLED with READ_WRITE permission
  • Server-side configuration can enable/disable the flag for specific user populations
  • CI runs tests with the flag in both states

3.5.4 Phase 3: Launch

At launch:

  • The flag is set to ENABLED and READ_ONLY in the release config
  • For API-guarded features, the flag is finalized for the target SDK level
  • The flag's value is baked into the binary and cannot be overridden

3.5.5 Phase 4: Cleanup

After the feature has been stable for a release cycle:

  • The flag declaration is removed from the .aconfig file
  • All if (Flags.myFlag()) checks are replaced with the enabled branch
  • Dead code from the disabled branch is removed
  • The flag's codegen library dependency is removed

Cleanup is critical -- stale flags accumulate technical debt. The aconfig system assists cleanup by providing the dump-cache command to identify flags that have been in a fixed state across all release configurations.

3.5.6 Bugfix Flags

Flags with purpose: PURPOSE_BUGFIX follow an accelerated lifecycle. They are typically:

  • Created as READ_WRITE to allow quick rollback if the fix causes regression
  • Promoted to READ_ONLY after one release cycle with the fix active
  • Cleaned up in the following release
flag {
  name: "enable_immediate_clear_override_bugfix"
  namespace: "core_experiments_team_internal"
  description: "Bugfix flag to allow clearing a local
                override immediately"
  bug: "387316969"
  metadata {
    purpose: PURPOSE_BUGFIX
  }
}

3.5.7 Exported and Finalized Flags

Flags marked is_exported: true are visible to code outside their originating container. This is essential for Mainline modules that expose APIs consumed by apps built outside the platform.

When an API guarded by a flag is finalized for a given SDK level, the flag enters the finalized flags system. The finalized_flag proto in aconfig_protos/protos/aconfig_internal.proto records:

message finalized_flag {
  optional string name = 1;
  optional string package = 2;
  optional int32 min_sdk = 3;
}

In the generated exported code, finalized flags include an SDK version check:

public static boolean myExportedFlag() {
    if (Build.VERSION.SDK_INT >= 36) {
        return true;  // Finalized at SDK 36
    }
    return FEATURE_FLAGS.myExportedFlag();
}

This ensures that apps running on SDK 36+ always see the flag as enabled, regardless of the runtime flag state.


3.6 Build System Integration

3.6.1 Soong Module Types

The aconfig build integration registers its module types through two packages:

From build/soong/aconfig/init.go (RegisterBuildComponents, lines 148-156):

func RegisterBuildComponents(ctx android.RegistrationContext) {
    ctx.RegisterModuleType("aconfig_declarations",
        DeclarationsFactory)
    ctx.RegisterModuleType("aconfig_values",
        ValuesFactory)
    ctx.RegisterModuleType("aconfig_value_set",
        ValueSetFactory)
    ctx.RegisterModuleType("all_aconfig_declarations",
        AllAconfigDeclarationsFactory)
    ctx.RegisterParallelSingletonType("all_aconfig_declarations",
        AllAconfigDeclarationsSingletonFactory)
    ctx.RegisterParallelSingletonType("exported_java_aconfig_library",
        ExportedJavaDeclarationsLibraryFactory)
    ctx.RegisterModuleType("all_aconfig_declarations_extension",
        AllAconfigDeclarationsExtensionFactory)
}

A change worth noting for Android 17: all_aconfig_declarations is now registered twice -- once as an ordinary module type (AllAconfigDeclarationsFactory) and once as a parallel singleton (AllAconfigDeclarationsSingletonFactory). The previous single RegisterSingletonModuleType was split into a module that runs the finalized-flags / metalava pipeline and a singleton that emits the combined artifacts (see section 3.6.10). A new all_aconfig_declarations_extension module type accompanies the split.

From build/soong/aconfig/codegen/init.go (RegisterBuildComponents, lines 83-88):

func RegisterBuildComponents(ctx android.RegistrationContext) {
    ctx.RegisterModuleType("aconfig_declarations_group",
        AconfigDeclarationsGroupFactory)
    ctx.RegisterModuleType("cc_aconfig_library",
        CcAconfigLibraryFactory)
    ctx.RegisterModuleType("java_aconfig_library",
        JavaDeclarationsLibraryFactory)
    ctx.RegisterModuleType("rust_aconfig_library",
        RustAconfigLibraryFactory)
}

3.6.2 aconfig_declarations

The aconfig_declarations module type is the starting point of the flag pipeline. It processes .aconfig source files and produces a binary cache.

Properties (from aconfig_declarations.go lines 39-56):

Property Type Required Description
srcs proptools.Configurable[[]string] Yes List of .aconfig files
package string Yes Java-style package name
container string Yes Container the flags belong to
exportable bool No Whether flags can be repackaged for export

In Android 17 srcs became a proptools.Configurable[[]string] (rather than a plain []string), so the list of declaration files can vary via select() based on product/release variables.

Example from frameworks/base:

// frameworks/base/android-sdk-flags/Android.bp

aconfig_declarations {
    name: "android.sdk.flags-aconfig",
    package: "android.sdk",
    container: "system",
    srcs: ["flags.aconfig"],
}

The build action invokes aconfig create-cache with all declaration files and any matching values from the release configuration. In Android 17 the core build rule in init.go (lines 32-51) writes the declarations and values to a response file to avoid command-line length limits, and uses Soong's CpIfChanged helper instead of a hand-written cmp/mv idiom:

aconfigRule = pctx.AndroidStaticRule("aconfig",
    blueprint.RuleParams{
        Command2: blueprint.NewCommand(
            Aconfig, ` create-cache`,
            ` --package ${package}`,
            ` ${container}`,
            ` @${out}.rsp`,        // declarations + values via rspfile
            ` ${default-permission}`,
            ` ${allow-read-write}`,
            ` ${mainline-beta-namespace-config}`,
            ` ${force-read-only}`,
            ` --cache ${out}.tmp`,
            ` && `, android.CpIfChanged, ` ${out}.tmp ${out}`,
        ),
        Rspfile:        "${out}.rsp",
        RspfileContent: "${declarations} ${values}",
        Restat:         true,
    }, ...)

CpIfChanged only rewrites the cache file if its contents actually changed (Restat: true re-stats the output), avoiding unnecessary rebuilds of downstream codegen targets. Two of the substituted arguments are wired to release-config build flags: ${allow-read-write} is the negation of RELEASE_ACONFIG_REQUIRE_ALL_READ_ONLY, and ${force-read-only} is driven by RELEASE_CONFIG_FORCE_READ_ONLY.

3.6.3 aconfig_values

The aconfig_values module type provides flag value overrides for a specific package. Values modules are collected into value sets.

Properties (from aconfig_values.go lines 28-33):

Property Type Required Description
srcs []string Yes List of .values or .textproto files
package string Yes Package to which these values apply

Example:

// build/release/aconfig/bp4a/android.app/Android.bp

aconfig_values {
    name: "aconfig-values-platform_build_release-bp4a-android.app-all",
    package: "android.app",
    srcs: [
        "*_flag_values.textproto",
    ],
}

3.6.4 aconfig_value_set

The aconfig_value_set module type aggregates multiple aconfig_values modules into a single set that can be referenced by a release configuration.

Properties (from aconfig_value_set.go lines 31-37):

Property Type Description
values []string List of aconfig_values module names
srcs []string Paths to Android.bp files containing values

Example:

// build/release/aconfig/bp4a/Android.bp

aconfig_value_set {
    name: "aconfig_value_set-platform_build_release-bp4a",
    srcs: [
        "*/Android.bp",
    ],
}

The srcs property is a newer approach that automatically discovers aconfig_values modules from the specified Android.bp files.

3.6.5 Release Configuration Integration

The bridge between value sets and the build is the release configuration variable RELEASE_ACONFIG_VALUE_SETS. This variable lists the aconfig_value_set modules that should be applied for the current build target.

In the Soong build, each aconfig_declarations module automatically adds a dependency on the value sets specified by this variable (from aconfig_declarations.go lines 92-98):

func (module *DeclarationsModule) DepsMutator(
        ctx android.BottomUpMutatorContext) {
    valuesFromConfig := ctx.Config().ReleaseAconfigValueSets()
    if len(valuesFromConfig) > 0 {
        ctx.AddDependency(ctx.Module(), implicitValuesTag,
            valuesFromConfig...)
    }
}

The resolution chain:

flowchart LR
    RC["Release Config<br/>(e.g., bp4a.scl)"] -->|"RELEASE_ACONFIG_<br/>VALUE_SETS"| AVS["aconfig_value_set<br/>modules"]
    AVS -->|"values deps"| AV["aconfig_values<br/>modules"]
    AV -->|"package match"| AD["aconfig_declarations<br/>modules"]
    AD -->|"create-cache"| Cache["Binary cache<br/>(per package)"]

    style RC fill:#e1f5fe
    style Cache fill:#c8e6c9

3.6.6 java_aconfig_library

The java_aconfig_library module type generates a Java library from an aconfig_declarations module:

Properties (from codegen/java_aconfig_library.go lines 34-51):

Property Type Required Description
aconfig_declarations string Yes Name of the aconfig_declarations module
mode string No Code generation mode (default: "production")
preserve_legacy_impl_interface bool No Force-keep the FeatureFlags/FeatureFlagsImpl indirection (Android 17)

Example:

java_aconfig_library {
    name: "android.sdk.flags-aconfig-java",
    aconfig_declarations: "android.sdk.flags-aconfig",
}

The preserve_legacy_impl_interface property is new in Android 17. By default the codegen rule passes --allow-impl-interface-removal, driven by the RELEASE_ACONFIG_DEFAULT_ALLOW_JAVA_IMPL_INTERFACE_REMOVAL build flag; this lets read-only flags be dropped from the generated FeatureFlags interface and implementation when nothing needs the runtime indirection. Setting preserve_legacy_impl_interface: true overrides that and keeps the full interface for callers that still depend on it.

The module automatically adds dependencies on (only when sdk_version is not "none"):

  • aconfig-annotations-lib -- for R8 optimization annotations
  • unsupportedappusage -- for backward compatibility annotations
  • aconfig_storage_stub -- for runtime storage access

And registers JarJar rename rules for the generated classes to support repackaging in the exported case:

module.AddJarJarRenameRule(declarations.Package+".Flags", "")
module.AddJarJarRenameRule(declarations.Package+".FeatureFlags", "")
module.AddJarJarRenameRule(
    declarations.Package+".FeatureFlagsImpl", "")
module.AddJarJarRenameRule(
    declarations.Package+".CustomFeatureFlags", "")
module.AddJarJarRenameRule(
    declarations.Package+".FakeFeatureFlagsImpl", "")

3.6.7 cc_aconfig_library

The cc_aconfig_library module type generates a C/C++ library:

Properties (from codegen/cc_aconfig_library.go lines 38-48):

Property Type Required Description
aconfig_declarations string Yes Name of the aconfig_declarations module
mode string No Code generation mode (default: "production")

Example:

cc_aconfig_library {
    name: "my-feature-flags-cc",
    aconfig_declarations: "my-feature-flags",
}

For production and exported modes, the library automatically depends on:

  • libaconfig_storage_read_api_cc -- C++ storage read API
  • libbase -- Android base library
  • liblog -- Android logging

For force-read-only mode, these dependencies are omitted since no runtime storage access is needed.

The generated file names follow the pattern:

  • Source: <package_with_underscores>.cc (e.g., com_android_apex_flags.cc)
  • Header: include/<package_with_underscores>.h

3.6.8 rust_aconfig_library

The rust_aconfig_library module type generates a Rust library crate:

rust_aconfig_library {
    name: "my-feature-flags-rust",
    aconfig_declarations: "my-feature-flags",
}

This creates a library that can be added to rlibs, dylibs, or rustlibs dependencies of other Rust modules.

3.6.9 aconfig_declarations_group

The aconfig_declarations_group module type aggregates multiple codegen libraries into a single dependency, simplifying large build configurations like frameworks/base/AconfigFlags.bp:

aconfig_declarations_group {
    name: "framework-minus-apex-aconfig-declarations",
    aconfig_declarations_groups: [
        "aconfig_trade_in_mode_flags",
        "audio-framework-aconfig",
    ],
    java_aconfig_libraries: [
        "android.app.flags-aconfig-java",
        "android.content.flags-aconfig-java",
        "android.location.flags-aconfig-java",
        "android.os.flags-aconfig-java",
        // ... many more
    ],
}

3.6.10 all_aconfig_declarations Singleton

The all_aconfig_declarations singleton module collects every aconfig_declarations module in the entire tree into a single combined file. This combined file is exported to the flag management server (Google's internal "Gantry" system):

From all_aconfig_declarations.go (lines 37-43):

// A singleton module that collects all of the aconfig flags
// declared in the tree into a single combined file for export
// to the external flag setting server (inside Google it's Gantry).
//
// Note that this is ALL aconfig_declarations modules present
// in the tree, not just ones that are relevant to the product
// currently being built.

The singleton produces:

  • all_aconfig_declarations.pb -- binary protobuf of all flags
  • all_aconfig_declarations.textproto -- text protobuf of all flags
  • Storage files: .package.map, .flag.map, .flag.info, .val

These artifacts are distributed as part of the docs, droid, sdk, release_config_metadata, and gms build goals.

In Android 17 the old SingletonModule was split into a plain module (AllAconfigDeclarationsFactory) and a singleton (AllAconfigDeclarationsSingletonFactory). The singleton emits the combined artifacts above; the module holds the API-surface properties (Api_signature_files, Finalized_flags_file) and runs the metalava / record-finalized-flags pipeline to produce finalized-flags.txt, publishing it through AllAconfigDeclarationsInfoProvider. A companion all_aconfig_declarations_extension module type (build/soong/aconfig/all_aconfig_declarations_extension.go) extends a base all_aconfig_declarations to generate an alternate finalized-flags.txt for additional API surfaces in dist builds. The RELEASE_ACONFIG_FINALIZE_NON_API_FLAGS build flag adds a finalize step for non-API flags in this pipeline.

3.6.11 exported_java_aconfig_library

The exported_java_aconfig_library singleton generates a JAR file containing Java flag accessor code for all exported flags across the entire tree. This JAR is distributed as android-flags.jar with the SDK:

ctx.DistForGoalWithFilename("sdk", this.intermediatePath,
    "android-flags.jar")

Apps built outside the platform (in Android Studio) can use this JAR to access exported flags without needing to build the full platform.

3.6.12 Dependency Graph

The complete dependency graph for a typical flag integration:

flowchart TB
    subgraph "Declaration Layer"
        AD["aconfig_declarations"]
    end

    subgraph "Values Layer"
        AV1["aconfig_values<br/>(package A)"]
        AV2["aconfig_values<br/>(package B)"]
        AVS["aconfig_value_set"]
        AV1 --> AVS
        AV2 --> AVS
    end

    subgraph "Release Config"
        RC["RELEASE_ACONFIG_<br/>VALUE_SETS"]
        RC -->|"implicit dep"| AD
    end

    subgraph "Codegen Layer"
        JL["java_aconfig_library"]
        CL["cc_aconfig_library"]
        RL["rust_aconfig_library"]
    end

    subgraph "Consumer Layer"
        JM["Java/Kotlin module<br/>(static_libs)"]
        CM["C++ module<br/>(shared_libs)"]
        RM["Rust module<br/>(rustlibs)"]
    end

    AD --> JL
    AD --> CL
    AD --> RL
    JL --> JM
    CL --> CM
    RL --> RM
    AVS --> RC

    style AD fill:#e1f5fe
    style JL fill:#c8e6c9
    style CL fill:#c8e6c9
    style RL fill:#c8e6c9

3.6.13 Build Flags (build_flag_declarations)

In addition to aconfig feature flags, the build system supports build flags -- a separate flag type used to control build-time behavior (as opposed to runtime feature toggles). Build flags are managed by the build_flags package in build/soong/aconfig/build_flags/:

// build/soong/aconfig/build_flags/declarations.go

type DeclarationsModule struct {
    android.ModuleBase
    android.DefaultableModuleBase

    properties struct {
        // Build flag declaration files
        Srcs []string `android:"path"`
    }
}

Build flags differ from aconfig flags in that they:

  • Only affect build behavior, never runtime behavior
  • Do not generate accessor code (Java/C++/Rust)
  • Are consumed directly by the build system (Soong, Make)
  • Do not require storage files or aconfigd

Examples of build flags:

  • RELEASE_ACONFIG_REQUIRE_ALL_READ_ONLY (force all flags to read-only)
  • RELEASE_EXPORTED_FLAG_CHECK (enable exported flag validation)
  • RELEASE_CONFIG_FORCE_READ_ONLY (force read-only at config level)

3.6.14 One Package Per Module Rule

The all_aconfig_declarations singleton enforces a critical constraint: each package may only have one aconfig_declarations module in the entire tree. This is checked during the singleton's GenerateBuildActions (from all_aconfig_declarations.go, around line 241):

var numOffendingPkg = 0
offendingPkgsMessage := ""
for pkg, cnt := range packages {
    if cnt > 1 {
        offendingPkgsMessage += fmt.Sprintf(
            "%d aconfig_declarations found for package %s\n",
            cnt, pkg)
        numOffendingPkg++
    }
}

if numOffendingPkg > 0 {
    panic("Only one aconfig_declarations allowed " +
          "for each package.\n" + offendingPkgsMessage)
}

This restriction ensures that:

  • Each flag has exactly one authoritative declaration
  • Flag IDs and storage offsets are deterministic
  • The server-side flag management system (Gantry) has an unambiguous mapping from package to flags

3.6.15 Build-Time Flags vs. Release Configuration

The aconfig system integrates with the broader release configuration infrastructure through several build flags:

Build Flag Effect
RELEASE_ACONFIG_VALUE_SETS Product variable (not a flag_declaration) listing the aconfig_value_set modules that apply
RELEASE_ACONFIG_FLAG_DEFAULT_PERMISSION Default permission for all flags (defaults to READ_WRITE)
RELEASE_ACONFIG_REQUIRE_ALL_READ_ONLY If true, it is an error to set any flag to READ_WRITE
RELEASE_CONFIG_FORCE_READ_ONLY If true, aconfig forces all flag permissions to READ_ONLY
RELEASE_ACONFIG_EXTRA_RELEASE_CONFIGS Additional release configs to generate artifacts for
RELEASE_ACONFIG_STORAGE_VERSION Version number for storage file format (defaults to "2")

The RELEASE_ACONFIG_REQUIRE_ALL_READ_ONLY flag is particularly important for production release builds. When set, it forbids any flag from being READ_WRITE, ensuring that no flag can be changed at runtime in the released build. (Note that RELEASE_ACONFIG_VALUE_SETS is read through Config.ReleaseAconfigValueSets() as a product variable; the others are flag_declaration entries under build/release/flag_declarations/.)

Android 17 adds several more RELEASE_ACONFIG_* flag declarations under build/release/flag_declarations/:

Build Flag (Android 17) Effect
RELEASE_ACONFIG_OPTIMIZE_READ_ONLY_JAVA Read-only Java getters return their default directly, bypassing test-override support
RELEASE_ACONFIG_ENABLE_INT_FLAG Allows declaring integer-typed aconfig flags
RELEASE_ACONFIG_GENERATE_CHECKS_SDK_ANNOTATION Emit @ChecksSdkIntAtLeast on finalized exported getters
RELEASE_ACONFIG_DEFAULT_ALLOW_JAVA_IMPL_INTERFACE_REMOVAL Default to removing unnecessary internal Java codegen where possible
RELEASE_ACONFIG_PARSE_V4 Parse and write the v4 storage format (integer-flag storage)
RELEASE_ACONFIG_SUPPORT_MINOR_SDK Generate minor-SDK (SDK_INT_FULL) checks for finalized flags
RELEASE_ACONFIG_FINALIZE_NON_API_FLAGS Run a finalize step for non-API flags in the finalized-flags pipeline

3.7 Testing with Flags

3.7.1 The Testing Challenge

Feature flags create a combinatorial testing problem. If a module has N flags, there are 2^N possible flag combinations. The aconfig testing infrastructure provides several mechanisms to manage this complexity:

  1. SetFlagsRule -- A JUnit TestRule that controls flag values within a test process
  2. @EnableFlags / @DisableFlags -- Annotations for declarative flag configuration per test method or class
  3. @RequiresFlagsEnabled / @RequiresFlagsDisabled -- Annotations that skip tests when flag preconditions are not met
  4. FlagsParameterization -- Utility for running tests across multiple flag combinations
  5. FakeFeatureFlagsImpl -- Generated test double for each flag package
  6. CheckFlagsRule -- A TestRule for device-side tests that checks flag preconditions

3.7.2 SetFlagsRule

The SetFlagsRule class in platform_testing/libraries/flag-helpers/junit/src_base/android/platform/test/flag/junit/SetFlagsRule.java is the primary testing mechanism. It works by replacing the FEATURE_FLAGS field in each Flags class with a FakeFeatureFlagsImpl instance:

public final class SetFlagsRule implements TestRule {

    // Key constants for reflection
    private static final String FAKE_FEATURE_FLAGS_IMPL_CLASS_NAME =
        "FakeFeatureFlagsImpl";
    private static final String FEATURE_FLAGS_FIELD_NAME =
        "FEATURE_FLAGS";
    private static final String FLAGS_CLASS_NAME = "Flags";

    // Two initialization modes
    public enum DefaultInitValueType {
        NULL_DEFAULT,    // Flags must be explicitly set
        DEVICE_DEFAULT,  // Use device/build default values
    }
}

The rule uses reflection to:

  1. Find the FEATURE_FLAGS static field in Flags.java
  2. Save the original FeatureFlagsImpl instance
  3. Replace it with a FakeFeatureFlagsImpl
  4. Restore the original after the test

3.7.3 @EnableFlags and @DisableFlags Annotations

These annotations provide a declarative way to set flag values for tests. They are defined in platform_testing/libraries/annotations/src/android/platform/test/annotations/:

// EnableFlags.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface EnableFlags {
    /**
     * The list of the feature flags to be enabled.
     * Each item is the full flag name with the format
     * {package_name}.{flag_name}.
     */
    String[] value();
}
// DisableFlags.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DisableFlags {
    String[] value();
}

Usage in tests:

@RunWith(AndroidJUnit4.class)
public class MyFeatureTest {
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Test
    @EnableFlags(Flags.FLAG_MY_NEW_FEATURE)
    public void testWithFeatureEnabled() {
        assertTrue(Flags.myNewFeature());
        // Test the enabled code path
    }

    @Test
    @DisableFlags(Flags.FLAG_MY_NEW_FEATURE)
    public void testWithFeatureDisabled() {
        assertFalse(Flags.myNewFeature());
        // Test the disabled code path
    }

    @Test
    @EnableFlags(Flags.FLAG_MY_NEW_FEATURE)
    @DisableFlags(Flags.FLAG_OTHER_FEATURE)
    public void testMixedFlags() {
        assertTrue(Flags.myNewFeature());
        assertFalse(Flags.otherFeature());
    }
}

The annotations follow specific precedence rules:

  • Method-level annotations override class-level annotations for the same flag
  • A flag cannot be both enabled and disabled at the same level (this is an error)
  • If a flag is set by both the class and method annotations, the values must be consistent

3.7.4 @RequiresFlagsEnabled and @RequiresFlagsDisabled

While @EnableFlags and @DisableFlags actively set flag values, @RequiresFlagsEnabled and @RequiresFlagsDisabled express preconditions. If the flag is not in the required state, the test is skipped (via JUnit Assume):

@Test
@RequiresFlagsEnabled(Flags.FLAG_MY_NEW_FEATURE)
public void testOnlyWhenFeatureExists() {
    // This test only runs if the flag is already enabled
    // on the device under test
}

These annotations are particularly useful for CTS tests that must run on different device configurations.

3.7.5 Programmatic Flag Control

In addition to annotations, flags can be set programmatically through the SetFlagsRule:

@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

@Before
public void setUp() {
    mSetFlagsRule.enableFlags(Flags.FLAG_MY_FEATURE);
}

@Test
public void myTest() {
    // Flag is enabled
    mSetFlagsRule.disableFlags(Flags.FLAG_MY_FEATURE);
    // Flag is now disabled
}

Note: The enableFlags() and disableFlags() methods on SetFlagsRule are deprecated in favor of the annotation-based approach. The annotations provide better readability and support for FlagsParameterization.

3.7.6 FlagsParameterization

FlagsParameterization enables running the same test across multiple flag combinations:

@RunWith(Parameterized.class)
public class MyParameterizedTest {
    @Parameterized.Parameters(name = "{0}")
    public static List<FlagsParameterization> getParams() {
        return FlagsParameterization.allCombinationsOf(
            Flags.FLAG_FEATURE_A,
            Flags.FLAG_FEATURE_B
        );
    }

    @Rule
    public final SetFlagsRule mSetFlagsRule;

    public MyParameterizedTest(
            FlagsParameterization flags) {
        mSetFlagsRule = new SetFlagsRule(flags);
    }

    @Test
    public void testInteraction() {
        // This test runs 4 times:
        // A=true  B=true
        // A=true  B=false
        // A=false B=true
        // A=false B=false
    }
}

When @EnableFlags is used with FlagsParameterization, tests that conflict with the parameterization are skipped via JUnit assumption failure, not failed.

3.7.7 Test Mode Code Generation

When java_aconfig_library is configured with mode: "test", the generated FeatureFlagsImpl.java throws on every flag access:

public final class FeatureFlagsImpl implements FeatureFlags {
    @Override
    public boolean myFlag() {
        throw new UnsupportedOperationException(
            "Method is not implemented.");
    }
}

And Flags.java exposes additional methods for test setup:

public static void setFeatureFlags(FeatureFlags featureFlags) {
    Flags.FEATURE_FLAGS = featureFlags;
}

public static void unsetFeatureFlags() {
    Flags.FEATURE_FLAGS = null;
}

This pattern forces tests to explicitly configure flag state, preventing accidental dependencies on production defaults.

3.7.8 C++ Test Mode

In C++ test mode, the generated header provides setter functions and a reset function:

namespace com::android::aconfig::test {

// Normal accessor
inline bool my_flag() {
    return provider_->my_flag();
}

// Test setter
inline void my_flag(bool val) {
    provider_->my_flag(val);
}

// Reset all flags
inline void reset_flags() {
    return provider_->reset_flags();
}

}  // namespace

C++ test code:

TEST(MyTest, FeatureEnabled) {
    com::android::aconfig::test::my_flag(true);
    // Test with flag enabled
    com::android::aconfig::test::reset_flags();
}

3.7.9 CheckFlagsRule for Device Tests

While SetFlagsRule actively sets flag values within the test process, CheckFlagsRule is designed for device-side instrumentation tests where flags cannot be programmatically controlled. Instead of setting flag values, it verifies that the device's flag state matches the test's requirements:

@RunWith(AndroidJUnit4.class)
public class MyDeviceTest {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule =
        DeviceFlagsValueProvider.createCheckFlagsRule();

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_MY_FEATURE)
    public void testOnlyWhenEnabled() {
        // This test only runs if the device has
        // the flag enabled
    }

    @Test
    @RequiresFlagsDisabled(Flags.FLAG_MY_FEATURE)
    public void testOnlyWhenDisabled() {
        // This test only runs if the device has
        // the flag disabled
    }
}

CheckFlagsRule reads the actual device flag state (from DeviceConfig or aconfigd) and skips tests whose preconditions are not met. This is essential for CTS tests that must pass on all device configurations.

The distinction between SetFlagsRule and CheckFlagsRule:

Aspect SetFlagsRule CheckFlagsRule
Flag control Actively sets values Reads device state
Test behavior Forces flag state Skips on mismatch
Use case Unit tests, Robolectric Device instrumentation tests
Annotations @EnableFlags/@DisableFlags @RequiresFlagsEnabled/Disabled
Implementation FakeFeatureFlagsImpl DeviceFlagsValueProvider

3.7.10 Host-Side Flag Testing

For host-side tests (running on the development machine, not on a device), the HostFlagsValueProvider reads flag values from the build configuration:

// platform_testing/libraries/flag-helpers/junit/
//   src_host/.../host/HostFlagsValueProvider.java

public class HostFlagsValueProvider implements IFlagsValueProvider {
    // Reads flag values from the aconfig cache files
    // generated during the build
}

This enables CTS and similar test suites to make flag-aware decisions when running tests from a host machine against a connected device.

3.7.11 Ravenwood Flag Support

Ravenwood (Android's host-side device testing framework) runs tests on the host JVM without a real Android framework. Because SetFlagsRule works purely through reflection on the generated Flags / FakeFeatureFlagsImpl classes, the same rule and the same @EnableFlags / @DisableFlags annotations function under Ravenwood without a dedicated Ravenwood-specific flag provider. Flag values resolve against the in-process fake rather than a live device.

3.7.12 Testing Architecture Diagram

flowchart TB
    subgraph "Test Setup"
        SF["SetFlagsRule"]
        EF["@EnableFlags"]
        DF["@DisableFlags"]
        EF --> SF
        DF --> SF
    end

    subgraph "Flag Injection"
        SF -->|"Reflection"| FlagsClass["Flags.FEATURE_FLAGS"]
        FlagsClass -->|"Replaced with"| Fake["FakeFeatureFlagsImpl"]
    end

    subgraph "Test Execution"
        TestCode["Test method"]
        TestCode -->|"Flags.myFlag()"| FlagsClass
        FlagsClass --> Fake
        Fake -->|"Map lookup"| Result["true/false"]
    end

    subgraph "Teardown"
        SF -->|"Restore"| Original["FeatureFlagsImpl<br/>(original)"]
    end

    style Fake fill:#c8e6c9
    style Original fill:#fff9c4

3.7.13 Best Practices for Flag Testing

  1. Test both states. Every flag should have tests for both enabled and disabled paths. Missing coverage on one path leads to regressions when the flag is flipped.

  2. Use annotations over programmatic control. The @EnableFlags and @DisableFlags annotations are more readable and composable than programmatic enableFlags() / disableFlags() calls.

  3. Test interactions. When two flags interact, use FlagsParameterization.allCombinationsOf() to cover all four states.

  4. Use DEVICE_DEFAULT for integration tests. The DefaultInitValueType.DEVICE_DEFAULT mode starts with build-time defaults, matching production behavior more closely.

  5. Clean up test flags. SetFlagsRule automatically restores flag state after each test, but be aware that static state in the code under test may retain flag-dependent values.


3.8 Legacy Feature Flags

Before the aconfig system, Android used several ad hoc mechanisms for feature flagging. Understanding these is important because they are still widely used in the codebase and because aconfig builds upon (and sometimes delegates to) these older systems.

3.8.1 SystemProperties

The SystemProperties class (android.os.SystemProperties) provides key-value string properties, many of which function as feature flags:

// Check a system property flag
boolean enabled = SystemProperties.getBoolean(
    "ro.feature.my_feature", false);

Property categories relevant to flagging:

Prefix Mutability Description
ro.* Read-only Set at boot, cannot be changed at runtime
persist.* Persistent Survives reboot, writable at runtime
sys.* Volatile Writable, lost at reboot
debug.* Debug Typically used for development flags

Limitations:

  • No type safety -- everything is a string
  • No centralized declaration -- properties are defined by convention
  • No build-system integration -- values are set in init scripts, build properties, or at runtime
  • Property values are length-limited: SystemProperties.PROP_VALUE_MAX is 91 (the key length is effectively unbounded today, PROP_NAME_MAX = Integer.MAX_VALUE)
  • No support for per-user or per-profile flags

3.8.2 Settings.Global and Settings.Secure

The Settings provider offers persistent key-value storage with more flexibility than SystemProperties:

// Read a settings-based flag
int value = Settings.Global.getInt(
    context.getContentResolver(),
    "my_feature_flag", 0);
Table Scope Use Case
Settings.Global Device-wide System-level feature flags
Settings.Secure Per-user User-specific feature flags
Settings.System Per-user User-visible settings (not flags)

Limitations:

  • Requires ContentResolver (context dependency)
  • Not available before SettingsProvider starts
  • No type safety beyond basic int/float/string getters
  • No centralized declaration or lifecycle management

3.8.3 DeviceConfig

DeviceConfig (android.provider.DeviceConfig) was introduced in Android 10 as a purpose-built feature flag system. Its implementation now lives in the ConfigInfrastructure Mainline module at packages/modules/ConfigInfrastructure/framework/java/android/provider/DeviceConfig.java (it was modularized out of frameworks/base). It stores flags organized by namespace and supports server-side flag pushes:

// Read a DeviceConfig flag
boolean enabled = DeviceConfig.getBoolean(
    "my_namespace", "my_flag", false);

// Listen for changes
DeviceConfig.addOnPropertiesChangedListener(
    "my_namespace",
    executor,
    properties -> {
        boolean newValue = properties.getBoolean(
            "my_flag", false);
    });

DeviceConfig was the precursor to aconfig's runtime storage and is still used as a backend for flags with metadata { storage: DEVICE_CONFIG }. The aconfig system generates code that reads from DeviceConfig when this backend is selected.

Limitations:

  • Built on top of Settings.Global (same IPC overhead)
  • Requires SettingsProvider to be initialized
  • No compile-time dead code elimination
  • No standardized declaration format (flags are defined by convention)

3.8.4 config.xml Resource Overlays

Resource-based feature flags use XML configuration files that can be overlaid by OEMs:

<!-- frameworks/base/core/res/res/values/config.xml -->
<resources>
    <bool name="config_enableMultiWindow">true</bool>
    <integer name="config_maxRunningUsers">4</integer>
</resources>

OEMs override these through Runtime Resource Overlays (RROs) or build-time static overlays:

<!-- device/vendor/overlay/res/values/config.xml -->
<resources>
    <bool name="config_enableMultiWindow">false</bool>
</resources>

Limitations:

  • Build-time only (RROs can change at install time, but not dynamically)
  • No server-side control
  • No lifecycle management
  • Overlays are per-device, not per-user or per-population

3.8.5 Comparison Matrix

graph LR
    subgraph "Modern (aconfig)"
        A["Declared in .aconfig<br/>Type-safe generated code<br/>Build + Runtime control<br/>Lifecycle managed"]
    end

    subgraph "Legacy"
        B["SystemProperties<br/>String-based<br/>Boot-time only"]
        C["Settings.Global<br/>ContentProvider-based<br/>Runtime writable"]
        D["DeviceConfig<br/>Namespace-organized<br/>Server-pushable"]
        E["config.xml<br/>Resource overlays<br/>OEM-configurable"]
    end

    A -.->|"replaces"| D
    A -.->|"replaces"| B
    A -.->|"replaces"| C
Feature aconfig SystemProperties Settings.Global DeviceConfig config.xml
Type safety Yes (codegen) No No No Limited
Declaration .aconfig Convention Convention Convention XML
Build-time control Yes Yes No No Yes
Runtime control Yes (RW) Limited Yes Yes No
Server-side push Yes No No Yes No
Dead code elim. Yes (R8) No No No No
Lifecycle mgmt Yes No No No No
Test infrastructure Yes Manual Manual Manual Manual
Boot-time available Yes (new) Yes No No Yes
Per-user No No Secure only No No

3.8.6 Build-Time Feature Macros

Before aconfig, native code commonly used preprocessor macros for feature flagging:

// Traditional approach
#ifdef ENABLE_FANCY_RENDERING
    renderFancy(scene);
#else
    renderBasic(scene);
#endif

These macros are set at compile time through Android.bp or Android.mk cflags:

cc_library {
    name: "mylib",
    cflags: ["-DENABLE_FANCY_RENDERING"],
}

Limitations:

  • The flag and its check are disconnected (no single declaration)
  • No runtime override capability
  • No test infrastructure for exercising both code paths
  • Typos in macro names silently create new macros
  • No centralized visibility into which flags exist

The aconfig C++ codegen preserves the zero-overhead nature of compile-time macros for fixed read-only flags (using constexpr inline functions and preprocessor defines) while adding runtime flexibility for read-write flags.

3.8.7 @FlaggedApi Annotation

The @FlaggedApi annotation (android.annotation.FlaggedApi, whose source lives at frameworks/libs/modules-utils/java/android/annotation/FlaggedApi.java) bridges aconfig flags with the Android API surface. When a new public API is gated by a flag:

@FlaggedApi(Flags.FLAG_MY_NEW_API)
public void myNewApi() {
    // This API only exists when the flag is enabled
}

The metalava documentation tool and the API surface checker use this annotation to:

  • Include or exclude the API from the public API signature based on flag state
  • Track which APIs are gated by which flags
  • Enforce that finalized APIs are properly associated with their flags
  • Generate SDK stubs that reflect the flag-dependent API surface

The all_aconfig_declarations singleton generates metadata that metalava consumes to verify the consistency between flag states and API visibility.

3.8.8 Migration from Legacy to aconfig

When migrating a legacy flag to aconfig:

  1. Declare the flag in a .aconfig file with the same semantic meaning
  2. Generate the library with java_aconfig_library or cc_aconfig_library
  3. Replace the legacy read (SystemProperties.getBoolean(...) or DeviceConfig.getBoolean(...)) with the generated accessor (Flags.myFlag())
  4. Add values to the appropriate release configuration to match the legacy flag's default behavior
  5. Add tests using @EnableFlags / @DisableFlags
  6. Remove the legacy flag once all consumers have migrated

For flags that were previously controlled via DeviceConfig, the migration can be gradual: set metadata { storage: DEVICE_CONFIG } in the aconfig declaration to keep using the DeviceConfig backend while gaining the benefits of type-safe generated code and centralized declaration.


3.9 Android 17 Changes

This section consolidates the Android 17 changes to the aconfig system. Several were noted in passing in earlier sections; here they are gathered with their source citations so the evolution from Android 16 is easy to see in one place.

3.9.1 Integer Flags

Through Android 16, every aconfig flag was boolean. Android 17 introduces a flag type dimension so a flag can carry an integer value. The proto schema in build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto adds:

enum flag_type {
  FLAG_TYPE_UNSPECIFIED = 0;  // assume boolean for backward compatibility
  FLAG_TYPE_BOOLEAN = 1;
  FLAG_TYPE_INTEGER = 2;
}

with flag_declaration.type (field 8), flag_value.value_int (field 5), and parsed_flag.type/value_int (fields 13 and 14). The whole pipeline carries the plumbing: the cache, the parser, and the storage format all understand integer flags.

The feature is deliberately staged. Declaring a FLAG_TYPE_INTEGER flag is rejected by the parser (aconfig_protos/src/lib.rs) unless the enable_int_flag Rust cfg is set, which the RELEASE_ACONFIG_ENABLE_INT_FLAG build flag (build/release/flag_declarations/RELEASE_ACONFIG_ENABLE_INT_FLAG.textproto) controls. And although the storage format and the read API can carry integer values, accessor code generation for integer flags is not yet wired -- every generated Java/C++/Rust accessor in Android 17 still returns bool. Integer flags are therefore best understood as schema-and-storage groundwork in this release.

3.9.2 Version-4 Storage Format

Integer flags require a new on-disk layout. Android 17 adds storage version 4, defined in build/make/tools/aconfig/aconfig_storage_file/src/lib.rs:

pub const MAX_SUPPORTED_FILE_VERSION: u32 =
    if cfg!(enable_parse_v4) { 4 } else { 3 } as u32;
pub const DEFAULT_FILE_VERSION: u32 = 2;

Whether v4 is read and written is gated by the enable_parse_v4 cfg, set by the RELEASE_ACONFIG_PARSE_V4 build flag. Relative to v3, version 4 adds:

  • Three integer variants to StoredFlagType (ReadWriteInt64, ReadOnlyInt64, FixedReadOnlyInt64) and a FlagValueType enum (Boolean, Int64).
  • An int_start_index field on PackageTableNode (the integer-flag analogue of boolean_start_index), mirrored as int_start_index on the read API's PackageReadContext.
  • num_int_flags and int_flag_offset fields on the flag-info header, plus an int_nodes list.
  • A Rust read function get_int64_flag_value(file, index) -> Result<i64> in build/make/tools/aconfig/aconfig_storage_read_api/src/lib.rs. There is no cxx::bridge counterpart yet, so C++ generated code reads only booleans.

The read flow and the four storage file types are otherwise unchanged from section 3.4.

3.9.3 Read-Only Java Optimization and Impl-Interface Removal

Two cooperating build flags let Android 17 shrink the generated Java for read-only flags:

  • RELEASE_ACONFIG_OPTIMIZE_READ_ONLY_JAVA makes read-only getters in Flags.java return their compile-time default value directly, bypassing the FEATURE_FLAGS indirection. CustomFeatureFlags.isOptimizationEnabled() (a method marked @AssumeTrueForR8) reflects this so R8 can fold away the dynamic read-only checks in release builds.
  • RELEASE_ACONFIG_DEFAULT_ALLOW_JAVA_IMPL_INTERFACE_REMOVAL lets the generator drop read-only flags from the FeatureFlags interface and FeatureFlagsImpl entirely. It is plumbed into codegen as the --allow-impl-interface-removal argument by build/soong/aconfig/codegen/java_aconfig_library.go, and the new preserve_legacy_impl_interface module property opts a library out.

When both apply to a package whose flags are all read-only, codegen can collapse the package down to a single Flags class with no FeatureFlags, FeatureFlagsImpl, CustomFeatureFlags, or FakeFeatureFlagsImpl. The selection happens in build/make/tools/aconfig/aconfig/src/codegen/java.rs (the is_read_only_optimized / preserve_impl_interface logic) and build/make/tools/aconfig/aconfig/src/commands.rs (optimize_read_only_getter = cfg!(optimize_read_only_java) && mode != Test).

3.9.4 Finalized-Flag and Exported-Flag Pipeline

The all_aconfig_declarations module was split into a module (which runs the finalized-flags / metalava pipeline) and a singleton (which emits the combined flag artifacts), with a new all_aconfig_declarations_extension module type for extra API surfaces -- see section 3.6.10. Two related codegen behaviors are new:

  • Minor-SDK finalized checks. For finalized exported flags at or above Baklava, the generated ExportedFlags getter now checks both the major and the minor SDK: Build.VERSION.SDK_INT >= 36 && Build.VERSION.SDK_INT_FULL >= <level>. The condition is produced by ApiLevel::conditional() in build/make/tools/aconfig/convert_finalized_flags/src/lib.rs and gated by RELEASE_ACONFIG_SUPPORT_MINOR_SDK.
  • @ChecksSdkIntAtLeast generation. With RELEASE_ACONFIG_GENERATE_CHECKS_SDK_ANNOTATION, finalized exported getters carry an @androidx.annotation.ChecksSdkIntAtLeast annotation so static analysis understands the version gate.

The finalized_flag message in build/make/tools/aconfig/aconfig_protos/protos/aconfig_internal.proto still records name, package, and min_sdk; a code comment reserves future minor SDK / SDK-extension support.

3.9.5 Runtime Daemon and aflags

The runtime side gained several refinements:

  • aconfigd-system is now a pure-Rust rust_binary (system/server_configurable_flags/aconfigd/Android.bp); the enable_full_rust_system_aconfigd migration flag has been removed.
  • A new early-platform-init entry point (gated by enable_earlier_aconfigd()) initializes platform storage earlier in boot and writes an /metadata/aconfig/early_init_done marker (system/server_configurable_flags/aconfigd/aconfigd.rc).
  • A platform_storage_records.pb index and an enable_aconfigd_from_mainline() switch split platform-owned records from Mainline-managed records (system/server_configurable_flags/aconfigd/src/aconfigd_commands.rs).
  • aflags list --format proto emits a Base64-encoded ProtoFlagList (gated by the android.provider.flags.aflags_list_proto flag), and aflags list can now merge Mainline Beta flags from device_config when aflags_list_mainline_beta is set. The clear subcommand is aflags unset.

3.9.6 Other Build-Integration Changes

  • The aconfig create-cache Soong rule now passes declarations and values through a response file and uses CpIfChanged instead of an inline cmp/mv (build/soong/aconfig/init.go); it also passes new mainline-beta-namespace-config and force-read-only arguments.
  • aconfig_declarations.srcs is now a proptools.Configurable[[]string], so the set of declaration files can vary via select() (build/soong/aconfig/aconfig_declarations.go).
  • Auto-added Java codegen dependencies (aconfig-annotations-lib, unsupportedappusage, aconfig_storage_stub) are now added only when the module's sdk_version is not "none" (build/soong/aconfig/codegen/java_aconfig_library.go).

3.10 Try It

The following exercises walk through the complete aconfig workflow, from declaring a flag to testing it in all states.

3.10.1 Exercise 1: Inspect Existing Flags

Explore the flags declared in the AOSP tree:

# Count all .aconfig declaration files
find . -name "*.aconfig" -type f | wc -l
# Expected: ~490 files (Android 17)

# Examine a simple flag declaration
cat system/apex/apexd/apexd.aconfig

# Examine a complex declaration with metadata
cat packages/modules/ConfigInfrastructure/framework/flags.aconfig

# Look at the Android.bp that wires up declarations
cat frameworks/base/android-sdk-flags/Android.bp

3.10.2 Exercise 2: Trace the Build Pipeline

Follow a single flag through the build system:

# Find all aconfig_declarations modules for a package
grep -r "aconfig_declarations {" \
    frameworks/base/android-sdk-flags/Android.bp

# Find the corresponding java_aconfig_library
grep -A5 "java_aconfig_library {" \
    frameworks/base/android-sdk-flags/Android.bp

# See which release configs set values for this package
find build/release/aconfig -name "*.textproto" \
    -exec grep -l "android.sdk" {} \;

3.10.3 Exercise 3: Examine Generated Code

After building, inspect the generated flag code:

# Build the flag library
m android.sdk.flags-aconfig-java

# Find the generated source jar
find out/soong/.intermediates -name "*.srcjar" \
    -path "*android.sdk.flags-aconfig-java*"

# Extract and examine
mkdir /tmp/flags-gen
cd /tmp/flags-gen
unzip <path-to-srcjar>
cat android/sdk/Flags.java
cat android/sdk/FeatureFlags.java
cat android/sdk/FeatureFlagsImpl.java

3.10.4 Exercise 4: Use the aconfig Tool Directly

The aconfig binary can be used standalone for exploration:

# Build the aconfig tool
m aconfig

# Create a test .aconfig file
cat > /tmp/test.aconfig << 'EOF'
package: "com.example.test"
container: "system"

flag {
    name: "my_test_flag"
    namespace: "test_namespace"
    description: "A test flag for learning"
    bug: "12345"
}

flag {
    name: "my_readonly_flag"
    namespace: "test_namespace"
    description: "A read-only test flag"
    bug: "12345"
    is_fixed_read_only: true
}
EOF

# Create a values override file
cat > /tmp/test.values << 'EOF'
flag_value {
    package: "com.example.test"
    name: "my_test_flag"
    state: ENABLED
    permission: READ_WRITE
}
flag_value {
    package: "com.example.test"
    name: "my_readonly_flag"
    state: ENABLED
    permission: READ_ONLY
}
EOF

# Create the cache
aconfig create-cache \
    --package com.example.test \
    --container system \
    --declarations /tmp/test.aconfig \
    --values /tmp/test.values \
    --cache /tmp/test-cache.pb

# Dump the cache in human-readable format
aconfig dump-cache \
    --cache /tmp/test-cache.pb \
    --format '{fully_qualified_name} state={state} \
              permission={permission}'

# Generate Java code
mkdir -p /tmp/java-out
aconfig create-java-lib \
    --cache /tmp/test-cache.pb \
    --mode production \
    --out /tmp/java-out

# Examine the generated code
find /tmp/java-out -name "*.java" -exec echo "=== {} ===" \; \
    -exec cat {} \;

3.10.5 Exercise 5: Query Flags with dump-cache

The dump-cache command supports rich formatting and filtering:

# Show all flags with their trace (which files set values)
aconfig dump-cache \
    --cache /tmp/test-cache.pb \
    --format '{fully_qualified_name} {trace}'

# Filter by permission
aconfig dump-cache \
    --cache /tmp/test-cache.pb \
    --filter 'permission:READ_WRITE' \
    --format '{name}: {state}'

# Filter by state
aconfig dump-cache \
    --cache /tmp/test-cache.pb \
    --filter 'state:ENABLED+permission:READ_ONLY' \
    --format '{fully_qualified_name}'

# Output as text protobuf
aconfig dump-cache \
    --cache /tmp/test-cache.pb \
    --format textproto

3.10.6 Exercise 6: Write Flag-Guarded Code

Create a simple module that uses aconfig flags.

Step 1: Declare flags (my_module/flags.aconfig):

package: "com.example.mymodule"
container: "system"

flag {
    name: "enable_new_algorithm"
    namespace: "performance"
    description: "Use the new O(n log n) algorithm"
    bug: "999999"
}

flag {
    name: "enable_caching"
    namespace: "performance"
    description: "Enable result caching"
    bug: "999998"
    is_fixed_read_only: true
}

Step 2: Add build rules (my_module/Android.bp):

aconfig_declarations {
    name: "my-module-flags",
    package: "com.example.mymodule",
    container: "system",
    srcs: ["flags.aconfig"],
}

java_aconfig_library {
    name: "my-module-flags-java",
    aconfig_declarations: "my-module-flags",
}

java_library {
    name: "my-module",
    srcs: ["src/**/*.java"],
    static_libs: ["my-module-flags-java"],
}

Step 3: Use flags in code (my_module/src/.../MyProcessor.java):

import com.example.mymodule.Flags;

public class MyProcessor {
    public Result process(Input input) {
        if (Flags.enableNewAlgorithm()) {
            return newAlgorithm(input);
        } else {
            return legacyAlgorithm(input);
        }
    }

    private Result fetchResult(Key key) {
        if (Flags.enableCaching()) {
            Result cached = cache.get(key);
            if (cached != null) return cached;
        }
        Result result = computeResult(key);
        if (Flags.enableCaching()) {
            cache.put(key, result);
        }
        return result;
    }
}

3.10.7 Exercise 7: Write Flag Tests

Write tests covering both flag states:

import static org.junit.Assert.*;

import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import com.example.mymodule.Flags;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class MyProcessorTest {

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    private final MyProcessor mProcessor = new MyProcessor();

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_NEW_ALGORITHM)
    public void testNewAlgorithm() {
        Result result = mProcessor.process(testInput);
        // Verify new algorithm behavior
        assertEquals(expectedNewResult, result);
    }

    @Test
    @DisableFlags(Flags.FLAG_ENABLE_NEW_ALGORITHM)
    public void testLegacyAlgorithm() {
        Result result = mProcessor.process(testInput);
        // Verify legacy algorithm behavior
        assertEquals(expectedLegacyResult, result);
    }

    @Test
    @EnableFlags({
        Flags.FLAG_ENABLE_NEW_ALGORITHM,
        Flags.FLAG_ENABLE_CACHING
    })
    public void testNewAlgorithmWithCaching() {
        Result first = mProcessor.process(testInput);
        Result second = mProcessor.process(testInput);
        // Verify caching behavior
        assertSame(first, second);
    }
}

3.10.8 Exercise 8: Parameterized Flag Testing

Test all flag combinations:

import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.flag.junit.FlagsParameterization;
import com.example.mymodule.Flags;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.List;

@RunWith(Parameterized.class)
public class MyProcessorParameterizedTest {

    @Parameterized.Parameters(name = "{0}")
    public static List<FlagsParameterization> getParams() {
        return FlagsParameterization.allCombinationsOf(
            Flags.FLAG_ENABLE_NEW_ALGORITHM,
            Flags.FLAG_ENABLE_CACHING
        );
        // Generates 4 combinations:
        // [new=T, cache=T], [new=T, cache=F],
        // [new=F, cache=T], [new=F, cache=F]
    }

    @Rule
    public final SetFlagsRule mSetFlagsRule;

    public MyProcessorParameterizedTest(
            FlagsParameterization flags) {
        mSetFlagsRule = new SetFlagsRule(flags);
    }

    @Test
    public void testProcessNeverCrashes() {
        MyProcessor processor = new MyProcessor();
        // This test runs 4 times, once per combination
        Result result = processor.process(testInput);
        assertNotNull(result);
    }
}

3.10.9 Exercise 9: Inspect Runtime Flag State on Device

Use device tools to examine and manipulate flags at runtime:

# List all aconfig flags on the device
adb shell aflags list

# Filter by package
adb shell aflags list | grep "com.android.apex"

# Check a specific flag value
adb shell device_config get \
    core_experiments_team_internal \
    com.android.provider.flags.dump_improvements

# Override a read-write flag
adb shell aflags enable \
    com.android.provider.flags.dump_improvements

# Verify the override
adb shell aflags list | grep dump_improvements

# Clear the override (the subcommand is "unset")
adb shell aflags unset \
    com.android.provider.flags.dump_improvements

# Inspect flag storage files
adb shell ls -la /metadata/aconfig/
adb shell ls -la /metadata/aconfig/maps/
adb shell ls -la /metadata/aconfig/flags/

3.10.10 Exercise 10: Create a C++ Flag Library

Integrate aconfig with a native module:

Step 1: Declare flags (my_native/flags.aconfig):

package: "com.example.mynative"
container: "system"

flag {
    name: "use_new_codec"
    namespace: "media"
    description: "Use the new hardware codec path"
    bug: "111111"
}

Step 2: Build rules (my_native/Android.bp):

aconfig_declarations {
    name: "my-native-flags",
    package: "com.example.mynative",
    container: "system",
    srcs: ["flags.aconfig"],
}

cc_aconfig_library {
    name: "my-native-flags-cc",
    aconfig_declarations: "my-native-flags",
}

cc_library {
    name: "my-native-lib",
    srcs: ["my_codec.cpp"],
    shared_libs: [
        "my-native-flags-cc",
        "libaconfig_storage_read_api_cc",
        "libbase",
        "liblog",
    ],
}

Step 3: Use in C++ code (my_native/my_codec.cpp):

#include "com_example_mynative.h"

void processFrame(Frame& frame) {
    if (com::example::mynative::use_new_codec()) {
        newCodecPath(frame);
    } else {
        legacyCodecPath(frame);
    }
}

3.10.11 Exercise 11: Explore the Soong Build Integration

Trace how Soong processes aconfig modules:

# Look at the Soong module registration
cat build/soong/aconfig/init.go

# Examine the declarations module implementation
cat build/soong/aconfig/aconfig_declarations.go

# See how values flow from release config to declarations
grep -n "ReleaseAconfigValueSets" \
    build/soong/aconfig/aconfig_declarations.go

# Examine the codegen module types
cat build/soong/aconfig/codegen/init.go

# Look at the Java codegen integration
cat build/soong/aconfig/codegen/java_aconfig_library.go

# See the all_aconfig_declarations singleton
cat build/soong/aconfig/all_aconfig_declarations.go

3.10.12 Exercise 12: Examine the aconfig Proto Schema

Study the protobuf definitions that underpin the system:

# The main aconfig proto definition
cat build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto

# The internal proto for finalized flags
cat build/make/tools/aconfig/aconfig_protos/protos/aconfig_internal.proto

# The storage metadata proto
cat build/make/tools/aconfig/aconfig_storage_file/protos/\
    aconfig_storage_metadata.proto

Key messages to understand:

Message File Purpose
flag_declaration aconfig.proto Input: individual flag declaration
flag_declarations aconfig.proto Input: package-level declaration wrapper
flag_value aconfig.proto Input: value override for a flag
flag_values aconfig.proto Input: collection of value overrides
parsed_flag aconfig.proto Output: fully resolved flag with trace
parsed_flags aconfig.proto Output: collection of resolved flags
tracepoint aconfig.proto Output: origin record for a flag value
finalized_flag aconfig_internal.proto Internal: API finalization record
storage_file_info aconfig_storage_metadata.proto Storage file locations

Summary

The aconfig feature flag system represents a fundamental shift in how Android manages the gap between code development and feature availability. Its key contributions to the platform are:

Build-time infrastructure:

  • The .aconfig declaration format provides a standardized, protobuf-backed schema for flag metadata (package, namespace, container, permission, purpose, storage backend)
  • The Soong module types (aconfig_declarations, aconfig_values, aconfig_value_set, java_aconfig_library, cc_aconfig_library, rust_aconfig_library) create a type-safe, dependency-tracked pipeline from declaration to usable library
  • Release configurations select which value sets apply, enabling per-target flag customization without code changes

Code generation:

  • The aconfig tool generates type-safe accessor code in Java, C++, and Rust, eliminating string-based flag lookups
  • Generated code includes R8 optimization annotations that enable dead code elimination for read-only flags
  • Four code generation modes (production, test, exported, force-read-only) serve different build contexts

Runtime resolution:

  • The aconfigd service and memory-mapped storage files provide boot-time flag availability and zero-IPC flag reads
  • The legacy DeviceConfig backend remains available for backward compatibility
  • Read-write flags support server-side overrides without requiring OTA updates

Testing:

  • The SetFlagsRule, @EnableFlags / @DisableFlags annotations, and FlagsParameterization provide comprehensive unit testing support
  • Generated FakeFeatureFlagsImpl classes enable isolated testing
  • Test mode generation forces explicit flag configuration, preventing accidental dependencies on production defaults

The combination of these capabilities -- trunk-stable development, type-safe code generation, efficient runtime resolution, and comprehensive testing -- addresses the fundamental challenge of shipping hundreds of features on a continuous development cadence while maintaining platform stability.

Key Source Files

Path Description
build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto Flag declaration and value protobuf schema
build/make/tools/aconfig/aconfig/src/commands.rs Core aconfig tool command implementations
build/make/tools/aconfig/aconfig/src/codegen/java.rs Java code generation logic
build/make/tools/aconfig/aconfig/src/codegen/cpp.rs C++ code generation logic
build/make/tools/aconfig/aconfig/src/codegen/rust.rs Rust code generation logic
build/make/tools/aconfig/aconfig/src/codegen/mod.rs CodegenMode enum and shared utilities
build/make/tools/aconfig/aconfig/templates/Flags.java.template Java Flags class template
build/make/tools/aconfig/aconfig/templates/FeatureFlags.java.template Java FeatureFlags interface template
build/make/tools/aconfig/aconfig/templates/FeatureFlagsImpl.new_storage.java.template New storage FeatureFlagsImpl template
build/make/tools/aconfig/aconfig/templates/FeatureFlagsImpl.legacy_flag.internal.java.template DeviceConfig (legacy) FeatureFlagsImpl template
build/make/tools/aconfig/aconfig/templates/ExportedFlags.java.template Exported flags accessor template
build/make/tools/aconfig/aconfig/templates/FeatureFlagsImpl.test_mode.java.template Test mode FeatureFlagsImpl template
build/make/tools/aconfig/aconfig/templates/FakeFeatureFlagsImpl.java.template Test fake implementation template
build/make/tools/aconfig/aconfig/templates/CustomFeatureFlags.java.template Custom delegation wrapper template
build/make/tools/aconfig/aconfig/templates/cpp_exported_header.template C++ header template
build/make/tools/aconfig/aconfig/templates/cpp_source_file.template C++ source template
build/soong/aconfig/init.go Soong module registration and build rules
build/soong/aconfig/aconfig_declarations.go aconfig_declarations module type
build/soong/aconfig/aconfig_values.go aconfig_values module type
build/soong/aconfig/aconfig_value_set.go aconfig_value_set module type
build/soong/aconfig/all_aconfig_declarations.go Module + singleton collecting all declarations
build/soong/aconfig/all_aconfig_declarations_extension.go Extension module for extra API surfaces (Android 17)
build/soong/aconfig/exported_java_aconfig_library.go Exported JAR singleton
build/soong/aconfig/codegen/init.go Codegen module registration and build rules
build/soong/aconfig/codegen/java_aconfig_library.go java_aconfig_library module type
build/soong/aconfig/codegen/cc_aconfig_library.go cc_aconfig_library module type
build/soong/aconfig/codegen/rust_aconfig_library.go rust_aconfig_library module type
build/soong/aconfig/codegen/aconfig_declarations_group.go Group module type
build/make/tools/aconfig/aconfig_storage_read_api/src/lib.rs Storage read API (incl. get_int64_flag_value)
build/make/tools/aconfig/aconfig_storage_file/src/lib.rs Storage file format, versions, StoredFlagType
build/make/tools/aconfig/convert_finalized_flags/src/lib.rs Finalized-flag SDK-level condition generation
build/make/tools/aconfig/aconfig_storage_file/protos/aconfig_storage_metadata.proto Storage metadata proto
system/server_configurable_flags/aconfigd/aconfigd.rc aconfigd init service definition
system/server_configurable_flags/aconfigd/src/aconfigd_commands.rs aconfigd command handlers
build/make/tools/aconfig/aflags/src/main.rs aflags device CLI tool
platform_testing/libraries/flag-helpers/junit/src_base/android/platform/test/flag/junit/SetFlagsRule.java Test rule for flag control
platform_testing/libraries/annotations/src/android/platform/test/annotations/EnableFlags.java @EnableFlags annotation
platform_testing/libraries/annotations/src/android/platform/test/annotations/DisableFlags.java @DisableFlags annotation
frameworks/base/AconfigFlags.bp Framework flag library aggregation
build/release/aconfig/bp4a/Android.bp Release config value set example