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:
-
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.
-
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.
-
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.
-
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.
-
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 partitionsystem_ext-- the system_ext partitionvendor-- the vendor partitionproduct-- 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:
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_rateis valid;AdjustRateandadjust__rateare 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:
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
- Declaration default: All flags start as
DISABLEDwithREAD_WRITEpermission (defined incommands.rsline 74-75). - Values files are applied in order. Later values override earlier ones.
- Build-time permission enforcement: If
RELEASE_ACONFIG_REQUIRE_ALL_READ_ONLYis set, all flags are forced toREAD_ONLYregardless of their declared permission. - Fixed read-only enforcement: Flags with
is_fixed_read_only: truecannot 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):
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:
-
Flag name constants follow the pattern
FLAG_<UPPER_SNAKE_CASE>and store the fully-qualified flag name (package.flag_name). -
R8 annotations enable the R8 optimizer to assume a flag's value at compile time:
@AssumeTrueForR8-- on flags that areENABLEDandREAD_ONLY@AssumeFalseForR8-- on flags that areDISABLEDandREAD_ONLY-
Read-write flags have no such annotation, since their value can change at runtime.
-
Method naming converts snake_case flag names to camelCase method names (e.g.,
mount_before_databecomesmountBeforeData()). -
In test mode,
Flags.javaadditionally exposessetFeatureFlags()andunsetFeatureFlags()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:
- Test mode (checked first, overrides everything else) -- uses
FeatureFlagsImpl.test_mode.java.template(throws on every access). - 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. - DeviceConfig storage (
use_device_config, non-exported) -- usesFeatureFlagsImpl.legacy_flag.internal.java.template, which reads each flag viaDeviceConfig.getProperties()/getBoolean(). - New aconfigd storage (the default, non-exported) -- uses
FeatureFlagsImpl.new_storage.java.template, which reads from memory-mapped files viaPlatformAconfigPackageInternal/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:
-
Fixed read-only flags become
constexprinline functions that return a preprocessor macro value. This enables the compiler to eliminate dead code at compile time. -
Read-write flags go through a
provider_pointer that is initialized at runtime from memory-mapped storage. -
C-linkage functions (
extern "C") are provided for consumption from C code and JNI. -
[[clang::no_destroy]]annotation is applied to the provider pointer to avoid destruction-order issues in thread-safe contexts. -
In test mode, each flag also gets a setter function (
void disabled_rw(bool val)) and areset_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:
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 runtimeReadWriteBoolean-- value can be overridden at runtimeFixedReadOnlyBoolean-- 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:
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:
-
Integrity verification: generated code includes the fingerprint and verifies it at runtime against the storage file, detecting mismatches between code and storage.
-
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:
-
Boot ordering dependency: DeviceConfig requires SettingsProvider to be running. Flags needed before SettingsProvider initialization cannot use this backend.
-
IPC overhead: Each
DeviceConfig.getProperties()call involves a Binder IPC to the SettingsProvider process. -
No atomic multi-flag reads: While
getProperties()returns all flags in a namespace atomically, cross-namespace reads are not atomic. -
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:
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
.aconfigfile withDISABLEDdefault state - Guarded in code with
if (Flags.myNewFeature()) { ... } - Tested locally by overriding with
aflagsor 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
ENABLEDwithREAD_WRITEpermission - 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
ENABLEDandREAD_ONLYin 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
.aconfigfile - 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_WRITEto allow quick rollback if the fix causes regression - Promoted to
READ_ONLYafter 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 annotationsunsupportedappusage-- for backward compatibility annotationsaconfig_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:
For production and exported modes, the library automatically depends on:
libaconfig_storage_read_api_cc-- C++ storage read APIlibbase-- Android base libraryliblog-- 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:
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 flagsall_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:
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:
- SetFlagsRule -- A JUnit
TestRulethat controls flag values within a test process - @EnableFlags / @DisableFlags -- Annotations for declarative flag configuration per test method or class
- @RequiresFlagsEnabled / @RequiresFlagsDisabled -- Annotations that skip tests when flag preconditions are not met
- FlagsParameterization -- Utility for running tests across multiple flag combinations
- FakeFeatureFlagsImpl -- Generated test double for each flag package
- CheckFlagsRule -- A
TestRulefor 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:
- Find the
FEATURE_FLAGSstatic field inFlags.java - Save the original
FeatureFlagsImplinstance - Replace it with a
FakeFeatureFlagsImpl - 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¶
-
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.
-
Use annotations over programmatic control. The
@EnableFlagsand@DisableFlagsannotations are more readable and composable than programmaticenableFlags()/disableFlags()calls. -
Test interactions. When two flags interact, use
FlagsParameterization.allCombinationsOf()to cover all four states. -
Use DEVICE_DEFAULT for integration tests. The
DefaultInitValueType.DEVICE_DEFAULTmode starts with build-time defaults, matching production behavior more closely. -
Clean up test flags.
SetFlagsRuleautomatically 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_MAXis 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
SettingsProviderstarts - 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:
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:
- Declare the flag in a
.aconfigfile with the same semantic meaning - Generate the library with
java_aconfig_libraryorcc_aconfig_library - Replace the legacy read (
SystemProperties.getBoolean(...)orDeviceConfig.getBoolean(...)) with the generated accessor (Flags.myFlag()) - Add values to the appropriate release configuration to match the legacy flag's default behavior
- Add tests using
@EnableFlags/@DisableFlags - 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 aFlagValueTypeenum (Boolean,Int64). - An
int_start_indexfield onPackageTableNode(the integer-flag analogue ofboolean_start_index), mirrored asint_start_indexon the read API'sPackageReadContext. num_int_flagsandint_flag_offsetfields on the flag-info header, plus anint_nodeslist.- A Rust read function
get_int64_flag_value(file, index) -> Result<i64>inbuild/make/tools/aconfig/aconfig_storage_read_api/src/lib.rs. There is nocxx::bridgecounterpart 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_JAVAmakes read-only getters inFlags.javareturn their compile-time default value directly, bypassing theFEATURE_FLAGSindirection.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_REMOVALlets the generator drop read-only flags from theFeatureFlagsinterface andFeatureFlagsImplentirely. It is plumbed into codegen as the--allow-impl-interface-removalargument bybuild/soong/aconfig/codegen/java_aconfig_library.go, and the newpreserve_legacy_impl_interfacemodule 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
ExportedFlagsgetter now checks both the major and the minor SDK:Build.VERSION.SDK_INT >= 36 && Build.VERSION.SDK_INT_FULL >= <level>. The condition is produced byApiLevel::conditional()inbuild/make/tools/aconfig/convert_finalized_flags/src/lib.rsand gated byRELEASE_ACONFIG_SUPPORT_MINOR_SDK. @ChecksSdkIntAtLeastgeneration. WithRELEASE_ACONFIG_GENERATE_CHECKS_SDK_ANNOTATION, finalized exported getters carry an@androidx.annotation.ChecksSdkIntAtLeastannotation 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-systemis now a pure-Rustrust_binary(system/server_configurable_flags/aconfigd/Android.bp); theenable_full_rust_system_aconfigdmigration flag has been removed.- A new
early-platform-initentry point (gated byenable_earlier_aconfigd()) initializes platform storage earlier in boot and writes an/metadata/aconfig/early_init_donemarker (system/server_configurable_flags/aconfigd/aconfigd.rc). - A
platform_storage_records.pbindex and anenable_aconfigd_from_mainline()switch split platform-owned records from Mainline-managed records (system/server_configurable_flags/aconfigd/src/aconfigd_commands.rs). aflags list --format protoemits a Base64-encodedProtoFlagList(gated by theandroid.provider.flags.aflags_list_protoflag), andaflags listcan now merge Mainline Beta flags fromdevice_configwhenaflags_list_mainline_betais set. The clear subcommand isaflags unset.
3.9.6 Other Build-Integration Changes¶
- The
aconfig create-cacheSoong rule now passes declarations and values through a response file and usesCpIfChangedinstead of an inlinecmp/mv(build/soong/aconfig/init.go); it also passes newmainline-beta-namespace-configandforce-read-onlyarguments. aconfig_declarations.srcsis now aproptools.Configurable[[]string], so the set of declaration files can vary viaselect()(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'ssdk_versionis 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
.aconfigdeclaration 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
aconfigtool 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
aconfigdservice 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/@DisableFlagsannotations, andFlagsParameterizationprovide comprehensive unit testing support - Generated
FakeFeatureFlagsImplclasses 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 |