Skip to content

Instantly share code, notes, and snippets.

@bhardwajAbhi
Created December 23, 2025 06:26
Show Gist options
  • Select an option

  • Save bhardwajAbhi/4d86f0a967ae9fbbe6aa920c486cf7c3 to your computer and use it in GitHub Desktop.

Select an option

Save bhardwajAbhi/4d86f0a967ae9fbbe6aa920c486cf7c3 to your computer and use it in GitHub Desktop.
AOSP Persistent Storage Guide

AOSP Framework: Persistent Storage Methods for System-Wide Values

Executive Summary

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.


Table of Contents

  1. Overview of Persistent Storage in AOSP
  2. How Quick Settings Persist State
  3. Option 1: Settings Provider
  4. Option 2: System Properties
  5. Option 3: System Config XML
  6. Option 4: Custom Content Provider
  7. Option 5: Service-Local Storage
  8. Comparison Matrix
  9. Decision Tree
  10. Implementation Best Practices
  11. Advanced Considerations
  12. Conclusion

Overview of Persistent Storage in AOSP {#overview}

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.


How Quick Settings Persist State {#how-qs-works}

The Quick Settings Architecture

Quick Settings in AOSP is managed by SystemUI and relies on a combination of mechanisms:

  1. Tile Configuration: The set of active QS tiles for a user is stored in Settings.Secure under the key sysui_qs_tiles as a comma-separated list. This ensures the tile layout persists across reboots.

  2. Radio State: Global radios (WiFi, Bluetooth, Airplane Mode) have their state stored in Settings.Global:

    • airplane_mode_on (int): Whether airplane mode is enabled
    • wifi_on (int): Whether WiFi is enabled
    • airplane_mode_radios (string): Comma-separated list of radios to toggle
    • airplane_mode_toggleable_radios (string): Radios that can be toggled in airplane mode
  3. Tile-Specific State: Individual tile states (e.g., whether a QS tile is enabled) are handled by the TileSpecRepository in SystemUI, which reads and writes these settings on startup and when changes occur.

  4. 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.

Example: WiFi and Airplane Mode Flow

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.


Option 1: Settings Provider {#option-1-settings-provider}

Overview

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.

Storage Mechanism

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.

Implementation

Define and Use a Setting

// 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
        }
    }
);

Variants: System, Global, Secure

  • 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_SETTINGS or system UID.
  • Settings.Secure: Sensitive settings (location services, installed packages). Per-user, requires WRITE_SECURE_SETTINGS or system UID.

For framework-level flags accessible everywhere, Settings.Global is the standard choice.

Advantages

  • 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.

Disadvantages

  • 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.

Best For

  • 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.

Option 2: System Properties {#option-2-system-properties}

Overview

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).

Storage Mechanism

  • Non-persistent properties (e.g., ro.build.version): Loaded from default.prop and property configs at boot, not persisted.
  • Persistent properties (e.g., persist.sys.locale): Written to /data/property/persistent_properties.xml and restored at boot.

Property ownership is controlled via property_contexts files, which specify which UID/GID can read/write each property.

Implementation

Define and Use a 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");

Define Property Context

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

Reading from Native Code

#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");

Advantages

  • 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.

Disadvantages

  • 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.

When to Use System Properties

  • 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.

Option 3: System Config XML {#option-3-system-config-xml}

Overview

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.

Storage Mechanism

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.

Implementation

Define Configuration in config.xml

<!-- 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>

Read from Java Code

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);
    }
}

Device-Specific Override

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>

Advantages

  • 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.

Disadvantages

  • 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.

When to Use System 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.


Option 4: Custom Content Provider {#option-4-custom-content-provider}

Overview

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.

Implementation Outline

1. Define Your Content Provider

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";
    }
}

2. Register in AndroidManifest.xml

<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>

3. Query and Modify

// 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
);

Advantages

  • 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.

Disadvantages

  • 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.

When to Use a Custom Provider

  • 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.

Option 5: Service-Local Storage {#option-5-service-local-storage}

Overview

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.

Implementation Example

1. Define AIDL Interface (Optional)

// IMyFeatureService.aidl
package com.android.myfeature;

interface IMyFeatureService {
    boolean isFeatureEnabled(String featureId);
    void setFeatureEnabled(String featureId, boolean enabled);
    Map getFeatureConfig(String featureId);
}

2. Implement Service

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);
        }
    }
}

3. Storage Backend

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
    }
}

4. Register Service in SystemServer

// In SystemServer.java
private void startMyFeatureService() {
    Slog.i(TAG, "Starting MyFeatureService");
    mMyFeatureService = new MyFeatureService(mSystemContext);
    mMyFeatureService.onStart();
}

Advantages

  • 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.

Disadvantages

  • 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.

When to Use Service-Local Storage

  • 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.

Comparison Matrix {#comparison-matrix}

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 ⚠️ Via AIDL/API
Early Boot Access ❌ No ✅ Yes ✅ Yes (resources) ❌ No ❌ No
Native Access ❌ Limited ✅ Yes ⚠️ Via resources.so ❌ No ✅ AIDL
Runtime Mutable ✅ Yes ✅ Yes ❌ No ✅ Yes ✅ Yes
Per-User Support ✅ System, Secure ❌ Global only ❌ No ⚠️ Custom ⚠️ Custom
Performance (Read) ✅ Fast ✅ Very Fast ✅ Very Fast ⚠️ Moderate ✅ Very Fast
Data Complexity ⚠️ Scalars only ⚠️ Strings only ⚠️ Simple types ✅ Complex ✅ Complex
Complexity ⚠️ Low ⚠️ Low ✅ Low ❌ High ❌ High
SELinux Burden ⚠️ Moderate ❌ High ✅ Low ⚠️ Moderate ⚠️ Moderate
Backup/Restore ✅ Yes ⚠️ Partial ✅ Yes ✅ Custom ⚠️ Custom

Decision Tree {#decision-tree}

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

Implementation Best Practices {#best-practices}

1. Centralize Constant Definitions

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;
}

2. Use Manager Classes for Encapsulation

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
        );
    }
}

3. Handle Permission Checks Gracefully

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);
    }
}

4. Leverage Config XML for Defaults

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);
    }
}

5. Document and Version Your Keys

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 {
    // ...
}

6. Handle Multi-User Scenarios

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);
}

7. Plan for Migration and Upgrade

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
    }
}

Advanced Considerations {#advanced-considerations}

Caching and Performance

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;
    }
}

SELinux and Property Context Definitions

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 };

Backup and Restore

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.

Observability and Debugging

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 1

For 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.feature

Conclusion {#conclusion}

Choosing 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.


References and Further Reading


Report Generated: December 2025 For AOSP Framework Persistent Storage Implementation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment