This report explores mechanisms for storing system-wide configuration and state values that persist across device reboots and are accessible from any framework component in AOSP. Using Quick Settings tile states (WiFi toggle, airplane mode) as the reference use case, we examine five primary approaches: Settings Provider, System Properties, System Config XML, Custom Content Providers, and Service-Local Storage. Each method has distinct trade-offs regarding persistence, accessibility, performance, and complexity.
- Overview of Persistent Storage in AOSP
- How Quick Settings Persist State
- Option 1: Settings Provider
- Option 2: System Properties
- Option 3: System Config XML
- Option 4: Custom Content Provider
- Option 5: Service-Local Storage
- Comparison Matrix
- Decision Tree
- Implementation Best Practices
- Advanced Considerations
- Conclusion
AOSP provides multiple layers of persistent configuration and state storage:
- Settings Provider: A system content provider backed by SQLite databases storing user-configurable settings across three tables (System, Global, Secure).
- System Properties: A key-value service where properties prefixed with
persist.survive reboots. - System Config XML: Declarative configuration files defining device-level defaults and feature flags.
- Resource Overlays: Static and runtime resource overlays allowing OEM customization.
- Custom Storage: Application or service-specific storage mechanisms for complex data structures.
Each layer is appropriate for different use cases. Choosing the right layer is critical for maintainability, performance, and security.
Quick Settings in AOSP is managed by SystemUI and relies on a combination of mechanisms:
-
Tile Configuration: The set of active QS tiles for a user is stored in
Settings.Secureunder the keysysui_qs_tilesas a comma-separated list. This ensures the tile layout persists across reboots. -
Radio State: Global radios (WiFi, Bluetooth, Airplane Mode) have their state stored in
Settings.Global:airplane_mode_on(int): Whether airplane mode is enabledwifi_on(int): Whether WiFi is enabledairplane_mode_radios(string): Comma-separated list of radios to toggleairplane_mode_toggleable_radios(string): Radios that can be toggled in airplane mode
-
Tile-Specific State: Individual tile states (e.g., whether a QS tile is enabled) are handled by the
TileSpecRepositoryin SystemUI, which reads and writes these settings on startup and when changes occur. -
State Synchronization: When a user toggles a QS tile, the corresponding setting is written via the Settings content provider, triggering observers to notify dependent components. On reboot, the framework reads these values from persistent storage to restore the previous state.
User toggles WiFi in QS
↓
SystemUI.QSTileHostBridge writes to Settings.Global.wifi_on
↓
Settings content provider stores in /data/system/users/0/settings_global.xml
↓
ContentObservers in framework and SystemUI are notified
↓
WifiManager, ConnectivityManager update state accordingly
↓
On reboot:
- Framework reads Settings.Global.wifi_on
- Restores WiFi state (if not changed by RIL/modem)
This pattern—storing simple state in Settings.Global, reading it on boot, observing changes—is the foundation for system-wide persistent values in AOSP.
The Settings provider is a system content provider that manages three namespace tables: Settings.System, Settings.Global, and Settings.Secure. It is the recommended approach for most framework-level persistent values that are not boot-critical.
Settings are backed by SQLite databases:
/data/system/users/0/settings_system.xml/data/system/users/0/settings_global.xml/data/system/users/0/settings_secure.xml
Each entry is a row with (name, value, user) columns. Changes are mirrored in-memory by the Settings provider for fast reads.
// In framework code with proper permissions
import android.provider.Settings;
import android.content.ContentResolver;
public class FeatureFlags {
public static final String MY_FEATURE_ENABLED = "my_feature_enabled";
public static final String MY_FEATURE_MODE = "my_feature_mode";
}
// Write to Settings.Global
public void enableFeature(ContentResolver resolver, boolean enabled) {
Settings.Global.putInt(resolver, FeatureFlags.MY_FEATURE_ENABLED,
enabled ? 1 : 0);
}
// Read from Settings.Global
public boolean isFeatureEnabled(ContentResolver resolver) {
return Settings.Global.getInt(resolver, FeatureFlags.MY_FEATURE_ENABLED,
0) != 0;
}
// Observe changes
ContentResolver resolver = context.getContentResolver();
resolver.registerContentObserver(
Settings.Global.getUriFor(FeatureFlags.MY_FEATURE_ENABLED),
false,
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
boolean enabled = Settings.Global.getInt(resolver,
FeatureFlags.MY_FEATURE_ENABLED, 0) != 0;
// React to change
}
}
);- Settings.System: User-visible settings (ringtone, display brightness). Per-user, generally modifiable by user-level apps.
- Settings.Global: Device-wide settings (airplane mode, adb_enabled). Shared across all users, requires
WRITE_GLOBAL_SETTINGSor system UID. - Settings.Secure: Sensitive settings (location services, installed packages). Per-user, requires
WRITE_SECURE_SETTINGSor system UID.
For framework-level flags accessible everywhere, Settings.Global is the standard choice.
- ✅ Persistent: Survives reboots via SQLite backing and XML serialization.
- ✅ Observable: ContentObserver allows reactive programming; changes notify all registered observers.
- ✅ Backed up: Backed up and restored on device migration.
- ✅ Access control: Built-in permission system (WRITE_GLOBAL_SETTINGS, WRITE_SECURE_SETTINGS).
- ✅ Scalable: Handles hundreds or thousands of settings without performance degradation.
- ✅ Documented: Well-established pattern in AOSP, familiar to most developers.
- ❌ Limited to scalars: Designed for int, long, float, and string values; not ideal for complex objects.
- ❌ Type handling: No native type safety; values are stored and retrieved as strings/ints, requiring casting.
- ❌ Permission overhead: Requires the calling code to have the appropriate permission or run as system UID.
- ❌ Schema flexibility: Adding or removing settings requires careful versioning to avoid breaking existing devices.
- Feature flags and toggles accessible from multiple framework components.
- User-configurable preferences that survive reboots.
- System-wide policy values that need to be observed for changes.
System properties are key-value pairs maintained by init and the system property service. Properties prefixed with persist. are persisted to /data/property and survive reboots. Properties are extremely fast to read and accessible from native and Java code across all layers (framework, system apps, vendor, HAL).
- Non-persistent properties (e.g.,
ro.build.version): Loaded fromdefault.propand property configs at boot, not persisted. - Persistent properties (e.g.,
persist.sys.locale): Written to/data/property/persistent_properties.xmland restored at boot.
Property ownership is controlled via property_contexts files, which specify which UID/GID can read/write each property.
// Read a persistent property
import android.os.SystemProperties;
boolean enabled = SystemProperties.getBoolean("persist.my.feature", false);
int mode = SystemProperties.getInt("persist.my.feature.mode", 0);
String value = SystemProperties.get("persist.my.feature.data", "default");
// Write a persistent property (requires appropriate SELinux context)
SystemProperties.set("persist.my.feature", "1");
SystemProperties.set("persist.my.feature.mode", "2");Create or update property_contexts in your device tree or framework config:
persist.my.feature u:object_r:system_property:s0
persist.my.feature.mode u:object_r:system_property:s0
persist.my.feature.data u:object_r:system_property:s0
#include <cutils/properties.h>
char value[PROPERTY_VALUE_MAX];
property_get("persist.my.feature", value, "0");
bool enabled = (strcmp(value, "1") == 0);
// Write from native (if SELinux allows)
property_set("persist.my.feature", "1");- ✅ Extremely fast: Read path is optimized; properties are cached in memory.
- ✅ Cross-partition accessibility: Accessible from framework, system apps, vendor, HAL, and native code.
- ✅ Early boot: Can be read and used during boot before the full Android runtime is available.
- ✅ Lightweight: No database overhead; simple string-based storage.
- ✅ Native support: Direct access from C/C++ code via
cutils/properties.h.
- ❌ Not observable: No built-in mechanism like ContentObserver; reactive code must poll or use alternative signaling (e.g., AIDL callbacks).
- ❌ Value limitations: Properties are strings; must manually convert to int/bool/enum. Max length ~92 characters.
- ❌ SELinux complexity: Requires defining property context rules, which can be error-prone and require testing.
- ❌ Discoverability: No standard schema; easy to accidentally collide names or use inconsistent prefixes.
- ❌ Not designed for frequent writes: High-frequency writes to persistent properties can degrade performance.
- Values that must be read during early boot or from HAL/vendor partitions.
- Flags that are rarely changed and need to be accessed from native code.
- Device behavior that is hardware-dependent and set at manufacturing or flashing time.
System configuration values can be defined as static XML or as resource overlays. This is appropriate for feature defaults, OEM-tunable behavior, and compile-time flags that do not change at runtime.
Configuration files are typically stored in:
frameworks/base/core/res/res/values/config.xml: Boolean and integer configuration values.frameworks/base/core/res/res/values/arrays.xml: String array configurations.device/<manufacturer>/<device>/overlay/frameworks/base/core/res/res/values/config.xml: Device-specific overrides.
Runtime Resource Overlays (RRO) allow changing configuration without code changes via resource overlays packaged as APKs.
<!-- frameworks/base/core/res/res/values/config.xml -->
<resources>
<!-- My feature configuration -->
<bool name="config_my_feature_enabled">false</bool>
<integer name="config_my_feature_mode">0</integer>
<string name="config_my_feature_default_data">default</string>
<string-array name="config_my_feature_options">
<item>option1</item>
<item>option2</item>
<item>option3</item>
</string-array>
</resources>import android.content.res.Resources;
public class FeatureConfig {
public static boolean isFeatureEnabled(Context context) {
Resources res = context.getResources();
return res.getBoolean(R.bool.config_my_feature_enabled);
}
public static int getFeatureMode(Context context) {
Resources res = context.getResources();
return res.getInteger(R.integer.config_my_feature_mode);
}
public static String getDefaultData(Context context) {
Resources res = context.getResources();
return res.getString(R.string.config_my_feature_default_data);
}
public static String[] getOptions(Context context) {
Resources res = context.getResources();
return res.getStringArray(R.array.config_my_feature_options);
}
}In your device tree:
<!-- device/mycompany/mydevice/overlay/frameworks/base/core/res/res/values/config.xml -->
<resources>
<bool name="config_my_feature_enabled">true</bool>
<integer name="config_my_feature_mode">2</integer>
</resources>- ✅ Clean separation: Configuration is declarative and separate from code.
- ✅ OEM-friendly: Device manufacturers can override via overlays without code changes.
- ✅ Build-time control: Suitable for feature flags that are determined at build time.
- ✅ No runtime overhead: Values are baked into the APK resource table.
- ❌ Not runtime mutable: Values cannot be changed at runtime; they are read-only.
- ❌ Not per-user: Configuration is global; cannot vary per user.
- ❌ Not persistent across dynamic changes: If you want to persist a user's choice, you must mirror it to Settings or properties.
- ❌ Limited schema: Constrained to bool, int, string, and arrays defined in config.xml.
- Device default values for feature flags.
- OEM-tunable settings that are set at build or overlay time.
- Build-time feature gates that determine which code paths are compiled in.
Note: For values that must be runtime-modifiable (like a user toggle), store the default in config.xml and the user's choice in Settings.Global or persist.* properties.
For complex data structures (e.g., JSON, protobuf, or structured records) that do not fit the simple key-value model of Settings, a custom content provider can be implemented to offer a tailored schema and API.
public class MyFeatureProvider extends ContentProvider {
private static final String AUTHORITY = "com.android.myfeature";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
private SQLiteDatabase mDb;
@Override
public boolean onCreate() {
MyFeatureDatabase dbHelper = new MyFeatureDatabase(getContext());
mDb = dbHelper.getWritableDatabase();
return mDb != null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return mDb.query("my_feature_table", projection, selection,
selectionArgs, null, null, sortOrder);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
long rowId = mDb.insert("my_feature_table", null, values);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.withAppendedPath(CONTENT_URI, String.valueOf(rowId));
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int count = mDb.update("my_feature_table", values, selection,
selectionArgs);
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = mDb.delete("my_feature_table", selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
@Override
public String getType(Uri uri) {
return "vnd.android.cursor.dir/vnd.myfeature.item";
}
}<manifest ...>
<application>
<provider
android:name=".MyFeatureProvider"
android:authorities="com.android.myfeature"
android:exported="true"
android:grantUriPermissions="true">
<grant-uri-permission android:pathPattern=".*" />
</provider>
</application>
</manifest>// Insert
ContentValues values = new ContentValues();
values.put("feature_id", "wifi");
values.put("enabled", 1);
values.put("mode", 2);
context.getContentResolver().insert(MyFeatureProvider.CONTENT_URI, values);
// Query
Cursor cursor = context.getContentResolver().query(
MyFeatureProvider.CONTENT_URI,
null,
"feature_id = ?",
new String[]{"wifi"},
null
);
if (cursor.moveToFirst()) {
int enabled = cursor.getInt(cursor.getColumnIndex("enabled"));
// ...
}
cursor.close();
// Update
ContentValues updates = new ContentValues();
updates.put("enabled", 0);
context.getContentResolver().update(
MyFeatureProvider.CONTENT_URI,
updates,
"feature_id = ?",
new String[]{"wifi"}
);
// Observe
context.getContentResolver().registerContentObserver(
MyFeatureProvider.CONTENT_URI,
true,
observer
);- ✅ Structured data: Supports complex schemas, JSON, blobs, and custom types.
- ✅ Observable: ContentObserver support for reactive programming.
- ✅ Flexible queries: SQL-like query capabilities for filtering and sorting.
- ✅ Encapsulation: Can enforce access control and validation at the provider level.
- ✅ Persistent: Backed by SQLite or custom persistent storage.
- ❌ Complexity: Requires implementing ContentProvider interface, database schema, and migration logic.
- ❌ Overhead: More code and maintenance burden compared to Settings or properties.
- ❌ Performance: Slower than in-memory caching of simple properties.
- ❌ Multi-partition access: More difficult to access from vendor or HAL unless the provider is system-partition-based.
- Storing structured configuration (e.g., profiles, policies, lists of rules).
- When you need complex queries or filtering.
- When you want to enforce data validation or access control at the provider level.
For subsystem-specific state that does not need system-wide visibility, a service can maintain local storage in a private file or database, exposing state via an AIDL/Binder interface or direct API. This is appropriate when the state is only consumed by a single service or a tightly coupled set of components.
// IMyFeatureService.aidl
package com.android.myfeature;
interface IMyFeatureService {
boolean isFeatureEnabled(String featureId);
void setFeatureEnabled(String featureId, boolean enabled);
Map getFeatureConfig(String featureId);
}public class MyFeatureService extends SystemService {
private static final String TAG = "MyFeatureService";
private Context mContext;
private MyFeatureStorage mStorage;
public MyFeatureService(Context context) {
super(context);
mContext = context;
mStorage = new MyFeatureStorage(context);
}
@Override
public void onStart() {
publishBinderService(Context.MY_FEATURE_SERVICE,
new MyFeatureServiceBinder());
}
private class MyFeatureServiceBinder extends IMyFeatureService.Stub {
@Override
public boolean isFeatureEnabled(String featureId) throws RemoteException {
return mStorage.isEnabled(featureId);
}
@Override
public void setFeatureEnabled(String featureId, boolean enabled)
throws RemoteException {
mStorage.setEnabled(featureId, enabled);
}
@Override
public Map getFeatureConfig(String featureId) throws RemoteException {
return mStorage.getConfig(featureId);
}
}
}public class MyFeatureStorage {
private static final String STORAGE_FILE = "/data/system/myfeature_config.xml";
private Map<String, FeatureConfig> mCache;
private File mStorageFile;
public MyFeatureStorage(Context context) {
mStorageFile = new File(STORAGE_FILE);
mCache = new ConcurrentHashMap<>();
loadFromDisk();
}
private void loadFromDisk() {
if (!mStorageFile.exists()) return;
try (FileInputStream fis = new FileInputStream(mStorageFile)) {
// Parse XML or JSON and populate mCache
} catch (IOException e) {
Log.w("MyFeatureStorage", "Failed to load config", e);
}
}
public synchronized void setEnabled(String featureId, boolean enabled) {
FeatureConfig config = mCache.get(featureId);
if (config == null) {
config = new FeatureConfig(featureId);
}
config.enabled = enabled;
mCache.put(featureId, config);
saveToDisk();
}
public boolean isEnabled(String featureId) {
FeatureConfig config = mCache.get(featureId);
return config != null && config.enabled;
}
private void saveToDisk() {
// Serialize mCache to XML/JSON and write to STORAGE_FILE
}
}// In SystemServer.java
private void startMyFeatureService() {
Slog.i(TAG, "Starting MyFeatureService");
mMyFeatureService = new MyFeatureService(mSystemContext);
mMyFeatureService.onStart();
}- ✅ Encapsulation: State and logic are tightly coupled; only relevant components access it.
- ✅ Flexibility: Custom storage format (XML, JSON, SQLite, protobuf) chosen by implementer.
- ✅ No permission overhead: Access control is implicit in Binder security.
- ✅ Performance: In-memory caching avoids repeated disk I/O.
- ✅ Versioning: Easier to evolve schema with custom serialization logic.
- ❌ Not system-wide: State is not easily accessible from unrelated framework components.
- ❌ Complexity: More code for serialization, versioning, migration.
- ❌ No standard observability: Custom callbacks or polling required for change notification.
- ❌ Isolation: Requires AIDL or manager API; no direct access.
- Configuration specific to a subsystem (e.g., SystemUI state, SoundService preferences).
- State that changes frequently and does not need to be accessed from many components.
- Complex data structures that do not fit the Settings or properties model.
| Criterion | Settings.Global | System Properties | Config XML | Custom Provider | Service Storage |
|---|---|---|---|---|---|
| Persistence | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Reboot-Safe | ✅ Yes | ✅ Yes (persist.*) | ✅ Yes | ✅ Yes | ✅ Yes |
| Observable | ✅ ContentObserver | ❌ Polling only | ❌ No | ✅ ContentObserver | ❌ Custom callbacks |
| System-Wide Access | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | |
| Early Boot Access | ❌ No | ✅ Yes | ✅ Yes (resources) | ❌ No | ❌ No |
| Native Access | ❌ Limited | ✅ Yes | ❌ No | ✅ AIDL | |
| Runtime Mutable | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
| Per-User Support | ✅ System, Secure | ❌ Global only | ❌ No | ||
| Performance (Read) | ✅ Fast | ✅ Very Fast | ✅ Very Fast | ✅ Very Fast | |
| Data Complexity | ✅ Complex | ✅ Complex | |||
| Complexity | ✅ Low | ❌ High | ❌ High | ||
| SELinux Burden | ❌ High | ✅ Low | |||
| Backup/Restore | ✅ Yes | ✅ Yes | ✅ Custom |
Use this flowchart to select the appropriate storage mechanism:
Start: Need persistent system-wide value?
│
├─ Must be accessible before Android runtime starts?
│ ├─ YES → Use System Properties (persist.*) + Config XML fallback
│ └─ NO → Continue
│
├─ Must be accessible from vendor/HAL (native code)?
│ ├─ YES → Use System Properties (persist.*)
│ └─ NO → Continue
│
├─ Is the value simple (bool, int, string)?
│ ├─ YES → Continue
│ └─ NO → Use Custom Content Provider or Service Storage → Done
│
├─ Must changes be observable (ContentObserver)?
│ ├─ YES → Use Settings.Global or Settings.Secure → Done
│ └─ NO → Continue
│
├─ Can the value be determined at build/overlay time (never runtime-modified)?
│ ├─ YES → Use System Config XML (with Settings.Global for user overrides) → Done
│ └─ NO → Continue
│
├─ Is the value user-configurable or sensitive?
│ ├─ SENSITIVE → Use Settings.Secure → Done
│ ├─ USER-CONFIGURABLE → Use Settings.System → Done
│ └─ DEVICE-WIDE POLICY → Use Settings.Global → Done
│
└─ Use Settings.Global (default for framework-level flags) → Done
Create a constants file to keep setting/property keys consistent across the codebase:
public class PersistentValues {
// Settings.Global keys
public static final String WIFI_TOGGLE_STATE = "my_wifi_toggle_state";
public static final String AIRPLANE_MODE_OVERRIDE = "my_airplane_mode_override";
// System properties
public static final String PROP_FEATURE_ENABLED = "persist.my.feature.enabled";
public static final String PROP_FEATURE_MODE = "persist.my.feature.mode";
// Default values
public static final boolean DEFAULT_WIFI_STATE = true;
public static final int DEFAULT_FEATURE_MODE = 0;
}Wrap access to persistent values in manager classes to simplify client code:
public class MyFeatureManager {
private ContentResolver mResolver;
public MyFeatureManager(ContentResolver resolver) {
mResolver = resolver;
}
public boolean isWiFiToggleEnabled() {
return Settings.Global.getInt(mResolver, PersistentValues.WIFI_TOGGLE_STATE,
PersistentValues.DEFAULT_WIFI_STATE ? 1 : 0) != 0;
}
public void setWiFiToggleEnabled(boolean enabled) {
Settings.Global.putInt(mResolver, PersistentValues.WIFI_TOGGLE_STATE,
enabled ? 1 : 0);
}
public void registerObserver(ContentObserver observer) {
mResolver.registerContentObserver(
Settings.Global.getUriFor(PersistentValues.WIFI_TOGGLE_STATE),
false,
observer
);
}
}Always handle SecurityException when accessing Settings or properties:
public boolean isFeatureEnabled(ContentResolver resolver) {
try {
return Settings.Global.getInt(resolver, "my_feature", 0) != 0;
} catch (SecurityException e) {
Log.w(TAG, "No permission to read settings", e);
return false; // Fallback default
}
}
public void setFeatureEnabled(ContentResolver resolver, boolean enabled) {
try {
Settings.Global.putInt(resolver, "my_feature", enabled ? 1 : 0);
} catch (SecurityException e) {
Log.e(TAG, "No permission to write settings", e);
}
}Always define defaults in config.xml and fall back to them when reading from Settings:
// Read from Settings with config.xml default fallback
public boolean isFeatureEnabledWithDefault(Context context, ContentResolver resolver) {
try {
return Settings.Global.getInt(resolver, "my_feature", 0) != 0;
} catch (SettingNotFoundException e) {
// Fall back to build-time config
return context.getResources().getBoolean(R.bool.config_my_feature_enabled);
}
}Maintain documentation for all persistent keys, their types, and default values:
/**
* Persistent value definitions for MyFeature subsystem.
*
* WIFI_TOGGLE_STATE (Settings.Global, int)
* - Controls whether WiFi toggle is visible in QS
* - Default: 1 (enabled)
* - Valid values: 0 (disabled), 1 (enabled)
* - Supported since API level 30
*
* AIRPLANE_MODE_OVERRIDE (Settings.Global, int)
* - User preference for airplane mode behavior
* - Default: 0 (disabled)
* - Valid values: 0, 1, 2, 3
* - Supported since API level 31
*/
public class PersistentValueDocs {
// ...
}Be aware of multi-user implications:
- Settings.System: Per-user, accessible only within that user's context.
- Settings.Global: Shared across all users.
- Settings.Secure: Per-user but sensitive; requires permission.
- System Properties: Global; no per-user variant.
// If handling multiple users, consider UserManager
public void setFeatureForUser(ContentResolver resolver, int userId, boolean enabled) {
// For Settings.System (per-user):
// Must resolve content provider as the target user
// Usually done via ActivityManager or UserManager
// For Settings.Global, no special handling needed
Settings.Global.putInt(resolver, "my_feature", enabled ? 1 : 0);
}When adding new persistent values or changing their format:
public class PersistentValueMigration {
private static final int VERSION = 2;
public void migrateIfNeeded(ContentResolver resolver) {
int currentVersion = Settings.Global.getInt(resolver, "my_feature_version", 0);
if (currentVersion < 1) {
// Migrate from old format
migrateV0ToV1(resolver);
}
if (currentVersion < 2) {
// Migrate to new format
migrateV1ToV2(resolver);
}
Settings.Global.putInt(resolver, "my_feature_version", VERSION);
}
private void migrateV0ToV1(ContentResolver resolver) {
// Example: rename a key or change its type
}
private void migrateV1ToV2(ContentResolver resolver) {
// Example: split one key into two or consolidate
}
}For frequently accessed values, implement local caching to avoid repeated content provider queries:
public class CachedFeatureManager {
private boolean mCachedEnabled = false;
private long mCacheTime = 0;
private static final long CACHE_DURATION = 5000; // 5 seconds
private ContentResolver mResolver;
private ContentObserver mObserver;
public CachedFeatureManager(ContentResolver resolver) {
mResolver = resolver;
mObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
mCacheTime = 0; // Invalidate cache
}
};
mResolver.registerContentObserver(
Settings.Global.getUriFor("my_feature"),
false,
mObserver
);
}
public boolean isFeatureEnabled() {
long now = System.currentTimeMillis();
if (now - mCacheTime < CACHE_DURATION) {
return mCachedEnabled;
}
mCachedEnabled = Settings.Global.getInt(mResolver, "my_feature", 0) != 0;
mCacheTime = now;
return mCachedEnabled;
}
}When using system properties, properly define SELinux contexts to avoid "permission denied" errors:
# In device/mycompany/mydevice/sepolicy/property.te
type my_feature_property, property_type;
# In device/mycompany/mydevice/sepolicy/property_contexts
persist.my.feature u:object_r:my_feature_property:s0
# In device/mycompany/mydevice/sepolicy/system_app.te
allow system_app my_feature_property:property_service { read write };
Ensure your persistent values are included in system backups:
// In your backup agent (if using custom storage)
public class MyFeatureBackupAgent extends BackupAgent {
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
// Backup your custom data
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
// Restore your custom data
}
}Settings.Global and Settings.Secure are automatically backed up by the framework.
Use dumpsys to inspect Settings values:
# Dump all global settings
adb shell dumpsys settings global
# Dump all secure settings for a user
adb shell dumpsys settings secure --user 0
# Dump a specific setting
adb shell settings get global my_feature
# Set a value
adb shell settings put global my_feature 1For system properties:
# Get a property
adb shell getprop persist.my.feature
# Set a property
adb shell setprop persist.my.feature 1
# List all properties matching a pattern
adb shell getprop | grep my.featureChoosing the right persistent storage mechanism in AOSP depends on your specific use case, performance requirements, and architectural constraints.
For most framework-level, system-wide persistent values (like Quick Settings toggle states), Settings.Global is the recommended approach. It provides:
- Reliable persistence across reboots
- System-wide accessibility
- Observable changes via ContentObserver
- Built-in backup and restore
- Well-established patterns and documentation
However, each method has its place:
- System Properties (persist.*): When early boot access or native code integration is required
- System Config XML: For build-time defaults and OEM overrides
- Custom Content Provider: For complex structured data with sophisticated querying
- Service-Local Storage: For subsystem-specific state with encapsulation
By understanding the trade-offs and following best practices—centralizing constants, using manager classes, handling permissions, and planning for migration—you can implement a robust, maintainable persistent storage layer that serves your AOSP framework needs.
- Android Settings Provider Documentation
- Android System Properties Guide
- AOSP Quick Settings Tiles Documentation
- Android Content Provider Guide
- AOSP System Configuration Overview
Report Generated: December 2025 For AOSP Framework Persistent Storage Implementation