Rewrite of the settings provider.

This change modifies how global, secure, and system settings are
managed. In particular, we are moving away from the database to
an in-memory model where the settings are persisted asynchronously
to XML.

This simplifies evolution and improves performance, for example,
changing a setting is down from around 400 ms to 10 ms as we do not
hit the disk. The trade off is that we may lose data if the system
dies before persisting the change.

In practice this is not a problem because 1) this is very rare;
2) apps changing a setting use the setting itself to know if it
changed, so next time the app runs (after a reboot that lost data)
the app will be oblivious that data was lost.

When persisting the settings we delay the write a bit to batch
multiple changes. If a change occurs we reschedule the write
but when a maximal delay occurs after the first non-persisted
change we write to disk no matter what. This prevents a malicious
app poking the settings all the time to prevent them being persisted.

The settings are persisted in separate XML files for each type of
setting per user. Specifically, they are in the user's system
directory and the files are named: settings_type_of_settings.xml.

Data migration is performed after the data base is upgraded to its
last version after which the global, system, and secure tables are
dropped.

The global, secure, and system settings now have the same version
and are upgraded as a whole per user to allow migration of settings
between these them. The upgrade steps should be added to the
SettingsProvider.UpgradeController and not in the DatabaseHelper.

Setting states are mapped to an integer key derived from the user
id and the setting type. Therefore, all setting states are in
a lookup table which makes all opertions very fast.

The code is a complete rewrite aiming for improved clarity and
increased maintainability as opposed to using minor optimizations.
Now setting and getting the changed setting takes around 10 ms. We
can optimize later if needed.

Now the code path through the call API and the one through the
content provider APIs end up being the same which fixes bugs where
some enterprise cases were not implemented in the content provider
code path.

Note that we are keeping the call code path as it is a bit faster
than the provider APIs with about 2 ms for setting and getting
a setting. The front-end settings APIs use the call method.

Further, we are restricting apps writing to the system settings.
If the app is targeting API higher than Lollipop MR1 we do not
let them have their settings in the system ones. Otherwise, we
warn that this will become an error. System apps like GMS core
can change anything like the system or shell or root.

Since old apps can add their settings, this can increase the
system memory footprint with no limit. Therefore, we limit the
amount of settings data an app can write to the system settings
before starting to reject new data.

Another problem with the system settings was that an app with a
permission to write there can put invalid values for the settings.
We now have validators for these settings that ensure only valid
values are accepted.

Since apps can put their settings in the system table, when the
app is uninstalled this data is stale in the sytem table without
ever being used. Now we keep the package that last changed the
setting and when the package is removed all settings it touched
that are not in the ones defined in the APIs are dropped.

Keeping in memory settings means that we cannot handle arbitrary
SQL operations, rather the supported operations are on a single
setting by name and all settings (querying). This should not be
a problem in practice but we have to verify it. For that reason,
we log unsupported SQL operations to the event log to do some
crunching and see what if any cases we should additionally support.

There are also tests for the settings provider in this change.

Change-Id: I941dc6e567588d9812905b147dbe1a3191c8dd68
diff --git a/packages/SettingsProvider/Android.mk b/packages/SettingsProvider/Android.mk
index c16f7b6..2b833b2 100644
--- a/packages/SettingsProvider/Android.mk
+++ b/packages/SettingsProvider/Android.mk
@@ -3,7 +3,8 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_SRC_FILES := $(call all-subdir-java-files) \
+    src/com/android/providers/settings/EventLogTags.logtags
 
 LOCAL_JAVA_LIBRARIES := telephony-common ims-common
 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
index 06e26bd..729efcb 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
@@ -58,6 +58,7 @@
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Database helper class for {@link SettingsProvider}.
@@ -78,6 +79,9 @@
 
     private static final HashSet<String> mValidTables = new HashSet<String>();
 
+    private static final String DATABASE_JOURNAL_SUFFIX = "-journal";
+    private static final String DATABASE_BACKUP_SUFFIX = "-backup";
+
     private static final String TABLE_SYSTEM = "system";
     private static final String TABLE_SECURE = "secure";
     private static final String TABLE_GLOBAL = "global";
@@ -86,13 +90,13 @@
         mValidTables.add(TABLE_SYSTEM);
         mValidTables.add(TABLE_SECURE);
         mValidTables.add(TABLE_GLOBAL);
-        mValidTables.add("bluetooth_devices");
-        mValidTables.add("bookmarks");
 
         // These are old.
+        mValidTables.add("bluetooth_devices");
+        mValidTables.add("bookmarks");
         mValidTables.add("favorites");
-        mValidTables.add("gservices");
         mValidTables.add("old_favorites");
+        mValidTables.add("android_metadata");
     }
 
     static String dbNameForUser(final int userHandle) {
@@ -118,6 +122,33 @@
         return mValidTables.contains(name);
     }
 
+    public void dropDatabase() {
+        close();
+        File databaseFile = mContext.getDatabasePath(getDatabaseName());
+        if (databaseFile.exists()) {
+            databaseFile.delete();
+        }
+        File databaseJournalFile = mContext.getDatabasePath(getDatabaseName()
+                + DATABASE_JOURNAL_SUFFIX);
+        if (databaseJournalFile.exists()) {
+            databaseJournalFile.delete();
+        }
+    }
+
+    public void backupDatabase() {
+        close();
+        File databaseFile = mContext.getDatabasePath(getDatabaseName());
+        if (!databaseFile.exists()) {
+            return;
+        }
+        File backupFile = mContext.getDatabasePath(getDatabaseName()
+                + DATABASE_BACKUP_SUFFIX);
+        if (backupFile.exists()) {
+            return;
+        }
+        databaseFile.renameTo(backupFile);
+    }
+
     private void createSecureTable(SQLiteDatabase db) {
         db.execSQL("CREATE TABLE secure (" +
                 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -1221,9 +1252,11 @@
                     // Migrate now-global settings. Note that this happens before
                     // new users can be created.
                     createGlobalTable(db);
-                    String[] settingsToMove = hashsetToStringArray(SettingsProvider.sSystemGlobalKeys);
+                    String[] settingsToMove = setToStringArray(
+                            SettingsProvider.sSystemMovedToGlobalSettings);
                     moveSettingsToNewTable(db, TABLE_SYSTEM, TABLE_GLOBAL, settingsToMove, false);
-                    settingsToMove = hashsetToStringArray(SettingsProvider.sSecureGlobalKeys);
+                    settingsToMove = setToStringArray(
+                            SettingsProvider.sSecureMovedToGlobalSettings);
                     moveSettingsToNewTable(db, TABLE_SECURE, TABLE_GLOBAL, settingsToMove, false);
 
                     db.setTransactionSuccessful();
@@ -1489,9 +1522,11 @@
                 db.beginTransaction();
                 try {
                     // Migrate now-global settings
-                    String[] settingsToMove = hashsetToStringArray(SettingsProvider.sSystemGlobalKeys);
+                    String[] settingsToMove = setToStringArray(
+                            SettingsProvider.sSystemMovedToGlobalSettings);
                     moveSettingsToNewTable(db, TABLE_SYSTEM, TABLE_GLOBAL, settingsToMove, true);
-                    settingsToMove = hashsetToStringArray(SettingsProvider.sSecureGlobalKeys);
+                    settingsToMove = setToStringArray(
+                            SettingsProvider.sSecureMovedToGlobalSettings);
                     moveSettingsToNewTable(db, TABLE_SECURE, TABLE_GLOBAL, settingsToMove, true);
 
                     db.setTransactionSuccessful();
@@ -1855,7 +1890,8 @@
                 try {
                     stmt = db.compileStatement("INSERT OR IGNORE INTO global(name,value)"
                             + " VALUES(?,?);");
-                    loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED, ImsConfig.FeatureValueConstants.ON);
+                    loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED,
+                            ImsConfig.FeatureValueConstants.ON);
                     db.setTransactionSuccessful();
                 } finally {
                     db.endTransaction();
@@ -1895,34 +1931,50 @@
             }
             upgradeVersion = 118;
         }
+
+        /**
+         * IMPORTANT: Do not add any more upgrade steps here as the global,
+         * secure, and system settings are no longer stored in a database
+         * but are kept in memory and persisted to XML. The correct places
+         * for adding upgrade steps are:
+         *
+         * Global: SettingsProvider.UpgradeController#onUpgradeGlobalSettings
+         * Secure: SettingsProvider.UpgradeController#onUpgradeSecureSettings
+         * System: SettingsProvider.UpgradeController#onUpgradeSystemSettings
+         */
+
         // *** Remember to update DATABASE_VERSION above!
 
         if (upgradeVersion != currentVersion) {
-            Log.w(TAG, "Got stuck trying to upgrade from version " + upgradeVersion
-                    + ", must wipe the settings provider");
-            db.execSQL("DROP TABLE IF EXISTS global");
-            db.execSQL("DROP TABLE IF EXISTS globalIndex1");
-            db.execSQL("DROP TABLE IF EXISTS system");
-            db.execSQL("DROP INDEX IF EXISTS systemIndex1");
-            db.execSQL("DROP TABLE IF EXISTS secure");
-            db.execSQL("DROP INDEX IF EXISTS secureIndex1");
-            db.execSQL("DROP TABLE IF EXISTS gservices");
-            db.execSQL("DROP INDEX IF EXISTS gservicesIndex1");
-            db.execSQL("DROP TABLE IF EXISTS bluetooth_devices");
-            db.execSQL("DROP TABLE IF EXISTS bookmarks");
-            db.execSQL("DROP INDEX IF EXISTS bookmarksIndex1");
-            db.execSQL("DROP INDEX IF EXISTS bookmarksIndex2");
-            db.execSQL("DROP TABLE IF EXISTS favorites");
-            onCreate(db);
-
-            // Added for diagnosing settings.db wipes after the fact
-            String wipeReason = oldVersion + "/" + upgradeVersion + "/" + currentVersion;
-            db.execSQL("INSERT INTO secure(name,value) values('" +
-                    "wiped_db_reason" + "','" + wipeReason + "');");
+            recreateDatabase(db, oldVersion, upgradeVersion, currentVersion);
         }
     }
 
-    private String[] hashsetToStringArray(HashSet<String> set) {
+    public void recreateDatabase(SQLiteDatabase db, int oldVersion,
+            int upgradeVersion, int currentVersion) {
+        db.execSQL("DROP TABLE IF EXISTS global");
+        db.execSQL("DROP TABLE IF EXISTS globalIndex1");
+        db.execSQL("DROP TABLE IF EXISTS system");
+        db.execSQL("DROP INDEX IF EXISTS systemIndex1");
+        db.execSQL("DROP TABLE IF EXISTS secure");
+        db.execSQL("DROP INDEX IF EXISTS secureIndex1");
+        db.execSQL("DROP TABLE IF EXISTS gservices");
+        db.execSQL("DROP INDEX IF EXISTS gservicesIndex1");
+        db.execSQL("DROP TABLE IF EXISTS bluetooth_devices");
+        db.execSQL("DROP TABLE IF EXISTS bookmarks");
+        db.execSQL("DROP INDEX IF EXISTS bookmarksIndex1");
+        db.execSQL("DROP INDEX IF EXISTS bookmarksIndex2");
+        db.execSQL("DROP TABLE IF EXISTS favorites");
+
+        onCreate(db);
+
+        // Added for diagnosing settings.db wipes after the fact
+        String wipeReason = oldVersion + "/" + upgradeVersion + "/" + currentVersion;
+        db.execSQL("INSERT INTO secure(name,value) values('" +
+                "wiped_db_reason" + "','" + wipeReason + "');");
+    }
+
+    private String[] setToStringArray(Set<String> set) {
         String[] array = new String[set.size()];
         return set.toArray(array);
     }
@@ -2639,7 +2691,8 @@
 
             loadBooleanSetting(stmt, Settings.Global.GUEST_USER_ENABLED,
                     R.bool.def_guest_user_enabled);
-            loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED, ImsConfig.FeatureValueConstants.ON);
+            loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED,
+                    ImsConfig.FeatureValueConstants.ON);
             // --- New global settings start here
         } finally {
             if (stmt != null) stmt.close();
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags b/packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags
new file mode 100644
index 0000000..298d776
--- /dev/null
+++ b/packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags
@@ -0,0 +1,5 @@
+# See system/core/logcat/e for a description of the format of this file.
+
+option java_package com.android.providers.settings;
+
+52100 unsupported_settings_query (uri|3),(selection|3),(whereArgs|3)
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 264dcae..8371117 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -110,11 +110,7 @@
 
     private static final String TAG = "SettingsBackupAgent";
 
-    private static final int COLUMN_NAME = 1;
-    private static final int COLUMN_VALUE = 2;
-
     private static final String[] PROJECTION = {
-        Settings.NameValueTable._ID,
         Settings.NameValueTable.NAME,
         Settings.NameValueTable.VALUE
     };
@@ -473,8 +469,8 @@
             ParcelFileDescriptor newState) throws IOException {
 
         HashSet<String> movedToGlobal = new HashSet<String>();
-        Settings.System.getMovedKeys(movedToGlobal);
-        Settings.Secure.getMovedKeys(movedToGlobal);
+        Settings.System.getMovedToGlobalSettings(movedToGlobal);
+        Settings.Secure.getMovedToGlobalSettings(movedToGlobal);
 
         while (data.readNextHeader()) {
             final String key = data.getKey();
@@ -577,8 +573,8 @@
         if (version <= FULL_BACKUP_VERSION) {
             // Generate the moved-to-global lookup table
             HashSet<String> movedToGlobal = new HashSet<String>();
-            Settings.System.getMovedKeys(movedToGlobal);
-            Settings.Secure.getMovedKeys(movedToGlobal);
+            Settings.System.getMovedToGlobalSettings(movedToGlobal);
+            Settings.Secure.getMovedToGlobalSettings(movedToGlobal);
 
             // system settings data first
             int nBytes = in.readInt();
@@ -824,11 +820,14 @@
             String key = settings[i];
             String value = cachedEntries.remove(key);
 
+            final int nameColumnIndex = cursor.getColumnIndex(Settings.NameValueTable.NAME);
+            final int valueColumnIndex = cursor.getColumnIndex(Settings.NameValueTable.VALUE);
+
             // If the value not cached, let us look it up.
             if (value == null) {
                 while (!cursor.isAfterLast()) {
-                    String cursorKey = cursor.getString(COLUMN_NAME);
-                    String cursorValue = cursor.getString(COLUMN_VALUE);
+                    String cursorKey = cursor.getString(nameColumnIndex);
+                    String cursorValue = cursor.getString(valueColumnIndex);
                     cursor.moveToNext();
                     if (key.equals(cursorKey)) {
                         value = cursorValue;
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 6828301..ff2c004 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -16,1211 +16,461 @@
 
 package com.android.providers.settings;
 
-import java.io.FileNotFoundException;
-import java.security.SecureRandom;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
+import android.Manifest;
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.backup.BackupManager;
 import android.content.BroadcastReceiver;
 import android.content.ContentProvider;
-import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
-import android.database.AbstractCursor;
 import android.database.Cursor;
+import android.database.MatrixCursor;
 import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
 import android.database.sqlite.SQLiteQueryBuilder;
+import android.hardware.camera2.utils.ArrayUtils;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.DropBoxManager;
-import android.os.FileObserver;
+import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.provider.Settings.Secure;
 import android.text.TextUtils;
-import android.util.Log;
-import android.util.LruCache;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Slog;
 import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.os.BackgroundThread;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
 
+import com.android.providers.settings.SettingsState.Setting;
+
+/**
+ * <p>
+ * This class is a content provider that publishes the system settings.
+ * It can be accessed via the content provider APIs or via custom call
+ * commands. The latter is a bit faster and is the preferred way to access
+ * the platform settings.
+ * </p>
+ * <p>
+ * There are three settings types, global (with signature level protection
+ * and shared across users), secure (with signature permission level
+ * protection and per user), and system (with dangerous permission level
+ * protection and per user). Global settings are stored under the device owner.
+ * Each of these settings is represented by a {@link
+ * com.android.providers.settings.SettingsState} object mapped to an integer
+ * key derived from the setting type in the most significant bits and user
+ * id in the least significant bits. Settings are synchronously loaded on
+ * instantiation of a SettingsState and asynchronously persisted on mutation.
+ * Settings are stored in the user specific system directory.
+ * </p>
+ * <p>
+ * Apps targeting APIs Lollipop MR1 and lower can add custom settings entries
+ * and get a warning. Targeting higher API version prohibits this as the
+ * system settings are not a place for apps to save their state. When a package
+ * is removed the settings it added are deleted. Apps cannot delete system
+ * settings added by the platform. System settings values are validated to
+ * ensure the clients do not put bad values. Global and secure settings are
+ * changed only by trusted parties, therefore no validation is performed. Also
+ * there is a limit on the amount of app specific settings that can be added
+ * to prevent unlimited growth of the system process memory footprint.
+ * </p>
+ */
+@SuppressWarnings("deprecation")
 public class SettingsProvider extends ContentProvider {
-    private static final String TAG = "SettingsProvider";
-    private static final boolean LOCAL_LOGV = false;
+    private static final boolean DEBUG = false;
 
-    private static final boolean USER_CHECK_THROWS = true;
+    private static final boolean DROP_DATABASE_ON_MIGRATION = !Build.IS_DEBUGGABLE;
+
+    private static final String LOG_TAG = "SettingsProvider";
 
     private static final String TABLE_SYSTEM = "system";
     private static final String TABLE_SECURE = "secure";
     private static final String TABLE_GLOBAL = "global";
+
+    // Old tables no longer exist.
     private static final String TABLE_FAVORITES = "favorites";
     private static final String TABLE_OLD_FAVORITES = "old_favorites";
+    private static final String TABLE_BLUETOOTH_DEVICES = "bluetooth_devices";
+    private static final String TABLE_BOOKMARKS = "bookmarks";
+    private static final String TABLE_ANDROID_METADATA = "android_metadata";
 
-    private static final String[] COLUMN_VALUE = new String[] { "value" };
-
-    // Caches for each user's settings, access-ordered for acting as LRU.
-    // Guarded by themselves.
-    private static final int MAX_CACHE_ENTRIES = 200;
-    private static final SparseArray<SettingsCache> sSystemCaches
-            = new SparseArray<SettingsCache>();
-    private static final SparseArray<SettingsCache> sSecureCaches
-            = new SparseArray<SettingsCache>();
-    private static final SettingsCache sGlobalCache = new SettingsCache(TABLE_GLOBAL);
-
-    // The count of how many known (handled by SettingsProvider)
-    // database mutations are currently being handled for this user.
-    // Used by file observers to not reload the database when it's ourselves
-    // modifying it.
-    private static final SparseArray<AtomicInteger> sKnownMutationsInFlight
-            = new SparseArray<AtomicInteger>();
-
-    // Each defined user has their own settings
-    protected final SparseArray<DatabaseHelper> mOpenHelpers = new SparseArray<DatabaseHelper>();
-
-    // Keep the list of managed profiles synced here
-    private List<UserInfo> mManagedProfiles = null;
-
-    // Over this size we don't reject loading or saving settings but
-    // we do consider them broken/malicious and don't keep them in
-    // memory at least:
-    private static final int MAX_CACHE_ENTRY_SIZE = 500;
-
-    private static final Bundle NULL_SETTING = Bundle.forPair("value", null);
-
-    // Used as a sentinel value in an instance equality test when we
-    // want to cache the existence of a key, but not store its value.
-    private static final Bundle TOO_LARGE_TO_CACHE_MARKER = Bundle.forPair("_dummy", null);
-
-    private UserManager mUserManager;
-    private BackupManager mBackupManager;
-
-    /**
-     * Settings which need to be treated as global/shared in multi-user environments.
-     */
-    static final HashSet<String> sSecureGlobalKeys;
-    static final HashSet<String> sSystemGlobalKeys;
-
-    // Settings that cannot be modified if associated user restrictions are enabled.
-    static final Map<String, String> sRestrictedKeys;
-
-    private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid";
-
-    static final HashSet<String> sSecureCloneToManagedKeys;
-    static final HashSet<String> sSystemCloneToManagedKeys;
-
+    // The set of removed legacy tables.
+    private static final Set<String> REMOVED_LEGACY_TABLES = new ArraySet<>();
     static {
-        // Keys (name column) from the 'secure' table that are now in the owner user's 'global'
-        // table, shared across all users
-        // These must match Settings.Secure.MOVED_TO_GLOBAL
-        sSecureGlobalKeys = new HashSet<String>();
-        Settings.Secure.getMovedKeys(sSecureGlobalKeys);
+        REMOVED_LEGACY_TABLES.add(TABLE_FAVORITES);
+        REMOVED_LEGACY_TABLES.add(TABLE_OLD_FAVORITES);
+        REMOVED_LEGACY_TABLES.add(TABLE_BLUETOOTH_DEVICES);
+        REMOVED_LEGACY_TABLES.add(TABLE_BOOKMARKS);
+        REMOVED_LEGACY_TABLES.add(TABLE_ANDROID_METADATA);
+    }
 
-        // Keys from the 'system' table now moved to 'global'
-        // These must match Settings.System.MOVED_TO_GLOBAL
-        sSystemGlobalKeys = new HashSet<String>();
-        Settings.System.getNonLegacyMovedKeys(sSystemGlobalKeys);
+    private static final int MUTATION_OPERATION_INSERT = 1;
+    private static final int MUTATION_OPERATION_DELETE = 2;
+    private static final int MUTATION_OPERATION_UPDATE = 3;
 
-        sRestrictedKeys = new HashMap<String, String>();
-        sRestrictedKeys.put(Settings.Secure.LOCATION_MODE, UserManager.DISALLOW_SHARE_LOCATION);
-        sRestrictedKeys.put(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+    private static final String[] ALL_COLUMNS = new String[] {
+            Settings.NameValueTable._ID,
+            Settings.NameValueTable.NAME,
+            Settings.NameValueTable.VALUE
+    };
+
+    private static final Bundle NULL_SETTING = Bundle.forPair(Settings.NameValueTable.VALUE, null);
+
+    // Per user settings that cannot be modified if associated user restrictions are enabled.
+    private static final Map<String, String> sSettingToUserRestrictionMap = new ArrayMap<>();
+    static {
+        sSettingToUserRestrictionMap.put(Settings.Secure.LOCATION_MODE,
                 UserManager.DISALLOW_SHARE_LOCATION);
-        sRestrictedKeys.put(Settings.Secure.INSTALL_NON_MARKET_APPS,
+        sSettingToUserRestrictionMap.put(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+                UserManager.DISALLOW_SHARE_LOCATION);
+        sSettingToUserRestrictionMap.put(Settings.Secure.INSTALL_NON_MARKET_APPS,
                 UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
-        sRestrictedKeys.put(Settings.Global.ADB_ENABLED, UserManager.DISALLOW_DEBUGGING_FEATURES);
-        sRestrictedKeys.put(Settings.Global.PACKAGE_VERIFIER_ENABLE,
+        sSettingToUserRestrictionMap.put(Settings.Global.ADB_ENABLED,
+                UserManager.DISALLOW_DEBUGGING_FEATURES);
+        sSettingToUserRestrictionMap.put(Settings.Global.PACKAGE_VERIFIER_ENABLE,
                 UserManager.ENSURE_VERIFY_APPS);
-        sRestrictedKeys.put(Settings.Global.PREFERRED_NETWORK_MODE,
+        sSettingToUserRestrictionMap.put(Settings.Global.PREFERRED_NETWORK_MODE,
                 UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
-
-        sSecureCloneToManagedKeys = new HashSet<String>();
-        for (int i = 0; i < Settings.Secure.CLONE_TO_MANAGED_PROFILE.length; i++) {
-            sSecureCloneToManagedKeys.add(Settings.Secure.CLONE_TO_MANAGED_PROFILE[i]);
-        }
-        sSystemCloneToManagedKeys = new HashSet<String>();
-        for (int i = 0; i < Settings.System.CLONE_TO_MANAGED_PROFILE.length; i++) {
-            sSystemCloneToManagedKeys.add(Settings.System.CLONE_TO_MANAGED_PROFILE[i]);
-        }
     }
 
-    private boolean settingMovedToGlobal(final String name) {
-        return sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name);
+    // Per user secure settings that moved to the for all users global settings.
+    static final Set<String> sSecureMovedToGlobalSettings = new ArraySet<>();
+    static {
+        Settings.Secure.getMovedToGlobalSettings(sSecureMovedToGlobalSettings);
     }
 
-    /**
-     * Decode a content URL into the table, projection, and arguments
-     * used to access the corresponding database rows.
-     */
-    private static class SqlArguments {
-        public String table;
-        public final String where;
-        public final String[] args;
-
-        /** Operate on existing rows. */
-        SqlArguments(Uri url, String where, String[] args) {
-            if (url.getPathSegments().size() == 1) {
-                // of the form content://settings/secure, arbitrary where clause
-                this.table = url.getPathSegments().get(0);
-                if (!DatabaseHelper.isValidTable(this.table)) {
-                    throw new IllegalArgumentException("Bad root path: " + this.table);
-                }
-                this.where = where;
-                this.args = args;
-            } else if (url.getPathSegments().size() != 2) {
-                throw new IllegalArgumentException("Invalid URI: " + url);
-            } else if (!TextUtils.isEmpty(where)) {
-                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
-            } else {
-                // of the form content://settings/secure/element_name, no where clause
-                this.table = url.getPathSegments().get(0);
-                if (!DatabaseHelper.isValidTable(this.table)) {
-                    throw new IllegalArgumentException("Bad root path: " + this.table);
-                }
-                if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table) ||
-                    TABLE_GLOBAL.equals(this.table)) {
-                    this.where = Settings.NameValueTable.NAME + "=?";
-                    final String name = url.getPathSegments().get(1);
-                    this.args = new String[] { name };
-                    // Rewrite the table for known-migrated names
-                    if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table)) {
-                        if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) {
-                            this.table = TABLE_GLOBAL;
-                        }
-                    }
-                } else {
-                    // of the form content://bookmarks/19
-                    this.where = "_id=" + ContentUris.parseId(url);
-                    this.args = null;
-                }
-            }
-        }
-
-        /** Insert new rows (no where clause allowed). */
-        SqlArguments(Uri url) {
-            if (url.getPathSegments().size() == 1) {
-                this.table = url.getPathSegments().get(0);
-                if (!DatabaseHelper.isValidTable(this.table)) {
-                    throw new IllegalArgumentException("Bad root path: " + this.table);
-                }
-                this.where = null;
-                this.args = null;
-            } else {
-                throw new IllegalArgumentException("Invalid URI: " + url);
-            }
-        }
+    // Per user system settings that moved to the for all users global settings.
+    static final Set<String> sSystemMovedToGlobalSettings = new ArraySet<>();
+    static {
+        Settings.System.getMovedToGlobalSettings(sSystemMovedToGlobalSettings);
     }
 
-    /**
-     * Get the content URI of a row added to a table.
-     * @param tableUri of the entire table
-     * @param values found in the row
-     * @param rowId of the row
-     * @return the content URI for this particular row
-     */
-    private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) {
-        if (tableUri.getPathSegments().size() != 1) {
-            throw new IllegalArgumentException("Invalid URI: " + tableUri);
-        }
-        String table = tableUri.getPathSegments().get(0);
-        if (TABLE_SYSTEM.equals(table) ||
-                TABLE_SECURE.equals(table) ||
-                TABLE_GLOBAL.equals(table)) {
-            String name = values.getAsString(Settings.NameValueTable.NAME);
-            return Uri.withAppendedPath(tableUri, name);
-        } else {
-            return ContentUris.withAppendedId(tableUri, rowId);
-        }
+    // Per user system settings that moved to the per user secure settings.
+    static final Set<String> sSystemMovedToSecureSettings = new ArraySet<>();
+    static {
+        Settings.System.getMovedToSecureSettings(sSystemMovedToSecureSettings);
     }
 
-    /**
-     * Send a notification when a particular content URI changes.
-     * Modify the system property used to communicate the version of
-     * this table, for tables which have such a property.  (The Settings
-     * contract class uses these to provide client-side caches.)
-     * @param uri to send notifications for
-     */
-    private void sendNotify(Uri uri, int userHandle) {
-        // Update the system property *first*, so if someone is listening for
-        // a notification and then using the contract class to get their data,
-        // the system property will be updated and they'll get the new data.
-
-        boolean backedUpDataChanged = false;
-        String property = null, table = uri.getPathSegments().get(0);
-        final boolean isGlobal = table.equals(TABLE_GLOBAL);
-        if (table.equals(TABLE_SYSTEM)) {
-            property = Settings.System.SYS_PROP_SETTING_VERSION;
-            backedUpDataChanged = true;
-        } else if (table.equals(TABLE_SECURE)) {
-            property = Settings.Secure.SYS_PROP_SETTING_VERSION;
-            backedUpDataChanged = true;
-        } else if (isGlobal) {
-            property = Settings.Global.SYS_PROP_SETTING_VERSION;    // this one is global
-            backedUpDataChanged = true;
-        }
-
-        if (property != null) {
-            long version = SystemProperties.getLong(property, 0) + 1;
-            if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version);
-            SystemProperties.set(property, Long.toString(version));
-        }
-
-        // Inform the backup manager about a data change
-        if (backedUpDataChanged) {
-            mBackupManager.dataChanged();
-        }
-        // Now send the notification through the content framework.
-
-        String notify = uri.getQueryParameter("notify");
-        if (notify == null || "true".equals(notify)) {
-            final int notifyTarget = isGlobal ? UserHandle.USER_ALL : userHandle;
-            final long oldId = Binder.clearCallingIdentity();
-            try {
-                getContext().getContentResolver().notifyChange(uri, null, true, notifyTarget);
-            } finally {
-                Binder.restoreCallingIdentity(oldId);
-            }
-            if (LOCAL_LOGV) Log.v(TAG, "notifying for " + notifyTarget + ": " + uri);
-        } else {
-            if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri);
-        }
+    // Per all users global settings that moved to the per user secure settings.
+    static final Set<String> sGlobalMovedToSecureSettings = new ArraySet<>();
+    static {
+        Settings.Global.getMovedToSecureSettings(sGlobalMovedToSecureSettings);
     }
 
-    /**
-     * Make sure the caller has permission to write this data.
-     * @param args supplied by the caller
-     * @throws SecurityException if the caller is forbidden to write.
-     */
-    private void checkWritePermissions(SqlArguments args) {
-        if ((TABLE_SECURE.equals(args.table) || TABLE_GLOBAL.equals(args.table)) &&
-            getContext().checkCallingOrSelfPermission(
-                    android.Manifest.permission.WRITE_SECURE_SETTINGS) !=
-            PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException(
-                    String.format("Permission denial: writing to secure settings requires %1$s",
-                                  android.Manifest.permission.WRITE_SECURE_SETTINGS));
-        }
+    // Per user secure settings that are cloned for the managed profiles of the user.
+    private static final Set<String> sSecureCloneToManagedSettings = new ArraySet<>();
+    static {
+        Settings.Secure.getCloneToManagedProfileSettings(sSecureCloneToManagedSettings);
     }
 
-    private void checkUserRestrictions(String setting, int userId) {
-        String userRestriction = sRestrictedKeys.get(setting);
-        if (!TextUtils.isEmpty(userRestriction)
-            && mUserManager.hasUserRestriction(userRestriction, new UserHandle(userId))) {
-            throw new SecurityException(
-                    "Permission denial: user is restricted from changing this setting.");
-        }
+    // Per user system settings that are cloned for the managed profiles of the user.
+    private static final Set<String> sSystemCloneToManagedSettings = new ArraySet<>();
+    static {
+        Settings.System.getCloneToManagedProfileSettings(sSystemCloneToManagedSettings);
     }
 
-    // FileObserver for external modifications to the database file.
-    // Note that this is for platform developers only with
-    // userdebug/eng builds who should be able to tinker with the
-    // sqlite database out from under the SettingsProvider, which is
-    // normally the exclusive owner of the database.  But we keep this
-    // enabled all the time to minimize development-vs-user
-    // differences in testing.
-    private static SparseArray<SettingsFileObserver> sObserverInstances
-            = new SparseArray<SettingsFileObserver>();
-    private class SettingsFileObserver extends FileObserver {
-        private final AtomicBoolean mIsDirty = new AtomicBoolean(false);
-        private final int mUserHandle;
-        private final String mPath;
+    private final Object mLock = new Object();
 
-        public SettingsFileObserver(int userHandle, String path) {
-            super(path, FileObserver.CLOSE_WRITE |
-                  FileObserver.CREATE | FileObserver.DELETE |
-                  FileObserver.MOVED_TO | FileObserver.MODIFY);
-            mUserHandle = userHandle;
-            mPath = path;
-        }
+    @GuardedBy("mLock")
+    private SettingsRegistry mSettingsRegistry;
 
-        public void onEvent(int event, String path) {
-            final AtomicInteger mutationCount;
-            synchronized (SettingsProvider.this) {
-                mutationCount = sKnownMutationsInFlight.get(mUserHandle);
-            }
-            if (mutationCount != null && mutationCount.get() > 0) {
-                // our own modification.
-                return;
-            }
-            Log.d(TAG, "User " + mUserHandle + " external modification to " + mPath
-                    + "; event=" + event);
-            if (!mIsDirty.compareAndSet(false, true)) {
-                // already handled. (we get a few update events
-                // during an sqlite write)
-                return;
-            }
-            Log.d(TAG, "User " + mUserHandle + " updating our caches for " + mPath);
-            fullyPopulateCaches(mUserHandle);
-            mIsDirty.set(false);
-        }
-    }
+    @GuardedBy("mLock")
+    private UserManager mUserManager;
+
+    @GuardedBy("mLock")
+    private AppOpsManager mAppOpsManager;
+
+    @GuardedBy("mLock")
+    private PackageManager mPackageManager;
 
     @Override
     public boolean onCreate() {
-        mBackupManager = new BackupManager(getContext());
-        mUserManager = UserManager.get(getContext());
-
-        setAppOps(AppOpsManager.OP_NONE, AppOpsManager.OP_WRITE_SETTINGS);
-        establishDbTracking(UserHandle.USER_OWNER);
-
-        IntentFilter userFilter = new IntentFilter();
-        userFilter.addAction(Intent.ACTION_USER_REMOVED);
-        userFilter.addAction(Intent.ACTION_USER_ADDED);
-        getContext().registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
-                        UserHandle.USER_OWNER);
-                if (intent.getAction().equals(Intent.ACTION_USER_REMOVED)) {
-                    onUserRemoved(userHandle);
-                } else if (intent.getAction().equals(Intent.ACTION_USER_ADDED)) {
-                    onProfilesChanged();
-                }
-            }
-        }, userFilter);
-
-        onProfilesChanged();
-
+        synchronized (mLock) {
+            mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
+            mAppOpsManager = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
+            mPackageManager = getContext().getPackageManager();
+            mSettingsRegistry = new SettingsRegistry();
+        }
+        registerBroadcastReceivers();
         return true;
     }
 
-    void onUserRemoved(int userHandle) {
-        synchronized (this) {
-            // the db file itself will be deleted automatically, but we need to tear down
-            // our caches and other internal bookkeeping.
-            FileObserver observer = sObserverInstances.get(userHandle);
-            if (observer != null) {
-                observer.stopWatching();
-                sObserverInstances.delete(userHandle);
-            }
-
-            mOpenHelpers.delete(userHandle);
-            sSystemCaches.delete(userHandle);
-            sSecureCaches.delete(userHandle);
-            sKnownMutationsInFlight.delete(userHandle);
-            onProfilesChanged();
-        }
-    }
-
-    /**
-     * Updates the list of managed profiles. It assumes that only the primary user
-     * can have managed profiles. Modify this code if that changes in the future.
-     */
-    void onProfilesChanged() {
-        synchronized (this) {
-            mManagedProfiles = mUserManager.getProfiles(UserHandle.USER_OWNER);
-            if (mManagedProfiles != null) {
-                // Remove the primary user from the list
-                for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
-                    if (mManagedProfiles.get(i).id == UserHandle.USER_OWNER) {
-                        mManagedProfiles.remove(i);
-                    }
-                }
-                // If there are no managed profiles, reset the variable
-                if (mManagedProfiles.size() == 0) {
-                    mManagedProfiles = null;
-                }
-            }
-            if (LOCAL_LOGV) {
-                Slog.d(TAG, "Managed Profiles = " + mManagedProfiles);
-            }
-        }
-    }
-
-    private void establishDbTracking(int userHandle) {
-        if (LOCAL_LOGV) {
-            Slog.i(TAG, "Installing settings db helper and caches for user " + userHandle);
-        }
-
-        DatabaseHelper dbhelper;
-
-        synchronized (this) {
-            dbhelper = mOpenHelpers.get(userHandle);
-            if (dbhelper == null) {
-                dbhelper = new DatabaseHelper(getContext(), userHandle);
-                mOpenHelpers.append(userHandle, dbhelper);
-
-                sSystemCaches.append(userHandle, new SettingsCache(TABLE_SYSTEM));
-                sSecureCaches.append(userHandle, new SettingsCache(TABLE_SECURE));
-                sKnownMutationsInFlight.append(userHandle, new AtomicInteger(0));
-            }
-        }
-
-        // Initialization of the db *outside* the locks.  It's possible that racing
-        // threads might wind up here, the second having read the cache entries
-        // written by the first, but that's benign: the SQLite helper implementation
-        // manages concurrency itself, and it's important that we not run the db
-        // initialization with any of our own locks held, so we're fine.
-        SQLiteDatabase db = dbhelper.getWritableDatabase();
-
-        // Watch for external modifications to the database files,
-        // keeping our caches in sync.  We synchronize the observer set
-        // separately, and of course it has to run after the db file
-        // itself was set up by the DatabaseHelper.
-        synchronized (sObserverInstances) {
-            if (sObserverInstances.get(userHandle) == null) {
-                SettingsFileObserver observer = new SettingsFileObserver(userHandle, db.getPath());
-                sObserverInstances.append(userHandle, observer);
-                observer.startWatching();
-            }
-        }
-
-        ensureAndroidIdIsSet(userHandle);
-
-        startAsyncCachePopulation(userHandle);
-    }
-
-    class CachePrefetchThread extends Thread {
-        private int mUserHandle;
-
-        CachePrefetchThread(int userHandle) {
-            super("populate-settings-caches");
-            mUserHandle = userHandle;
-        }
-
-        @Override
-        public void run() {
-            fullyPopulateCaches(mUserHandle);
-        }
-    }
-
-    private void startAsyncCachePopulation(int userHandle) {
-        new CachePrefetchThread(userHandle).start();
-    }
-
-    private void fullyPopulateCaches(final int userHandle) {
-        DatabaseHelper dbHelper;
-        synchronized (this) {
-            dbHelper = mOpenHelpers.get(userHandle);
-        }
-        if (dbHelper == null) {
-            // User is gone.
-            return;
-        }
-        // Only populate the globals cache once, for the owning user
-        if (userHandle == UserHandle.USER_OWNER) {
-            fullyPopulateCache(dbHelper, TABLE_GLOBAL, sGlobalCache);
-        }
-        fullyPopulateCache(dbHelper, TABLE_SECURE, sSecureCaches.get(userHandle));
-        fullyPopulateCache(dbHelper, TABLE_SYSTEM, sSystemCaches.get(userHandle));
-    }
-
-    // Slurp all values (if sane in number & size) into cache.
-    private void fullyPopulateCache(DatabaseHelper dbHelper, String table, SettingsCache cache) {
-        SQLiteDatabase db = dbHelper.getReadableDatabase();
-        Cursor c = db.query(
-            table,
-            new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE },
-            null, null, null, null, null,
-            "" + (MAX_CACHE_ENTRIES + 1) /* limit */);
-        try {
-            synchronized (cache) {
-                cache.evictAll();
-                cache.setFullyMatchesDisk(true);  // optimistic
-                int rows = 0;
-                while (c.moveToNext()) {
-                    rows++;
-                    String name = c.getString(0);
-                    String value = c.getString(1);
-                    cache.populate(name, value);
-                }
-                if (rows > MAX_CACHE_ENTRIES) {
-                    // Somewhat redundant, as removeEldestEntry() will
-                    // have already done this, but to be explicit:
-                    cache.setFullyMatchesDisk(false);
-                    Log.d(TAG, "row count exceeds max cache entries for table " + table);
-                }
-                if (LOCAL_LOGV) Log.d(TAG, "cache for settings table '" + table
-                        + "' rows=" + rows + "; fullycached=" + cache.fullyMatchesDisk());
-            }
-        } finally {
-            c.close();
-        }
-    }
-
-    private boolean ensureAndroidIdIsSet(int userHandle) {
-        final Cursor c = queryForUser(Settings.Secure.CONTENT_URI,
-                new String[] { Settings.NameValueTable.VALUE },
-                Settings.NameValueTable.NAME + "=?",
-                new String[] { Settings.Secure.ANDROID_ID }, null,
-                userHandle);
-        try {
-            final String value = c.moveToNext() ? c.getString(0) : null;
-            if (value == null) {
-                // sanity-check the user before touching the db
-                final UserInfo user = mUserManager.getUserInfo(userHandle);
-                if (user == null) {
-                    // can happen due to races when deleting users; treat as benign
-                    return false;
+    @Override
+    public Bundle call(String method, String name, Bundle args) {
+        synchronized (mLock) {
+            final int requestingUserId = getRequestingUserId(args);
+            switch (method) {
+                case Settings.CALL_METHOD_GET_GLOBAL: {
+                    Setting setting = getGlobalSettingLocked(name);
+                    return packageValueForCallResult(setting);
                 }
 
-                final SecureRandom random = new SecureRandom();
-                final String newAndroidIdValue = Long.toHexString(random.nextLong());
-                final ContentValues values = new ContentValues();
-                values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID);
-                values.put(Settings.NameValueTable.VALUE, newAndroidIdValue);
-                final Uri uri = insertForUser(Settings.Secure.CONTENT_URI, values, userHandle);
-                if (uri == null) {
-                    Slog.e(TAG, "Unable to generate new ANDROID_ID for user " + userHandle);
-                    return false;
+                case Settings.CALL_METHOD_GET_SECURE: {
+                    Setting setting = getSecureSettingLocked(name, requestingUserId);
+                    return packageValueForCallResult(setting);
                 }
-                Slog.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue
-                        + "] for user " + userHandle);
-                // Write a dropbox entry if it's a restricted profile
-                if (user.isRestricted()) {
-                    DropBoxManager dbm = (DropBoxManager)
-                            getContext().getSystemService(Context.DROPBOX_SERVICE);
-                    if (dbm != null && dbm.isTagEnabled(DROPBOX_TAG_USERLOG)) {
-                        dbm.addText(DROPBOX_TAG_USERLOG, System.currentTimeMillis()
-                                + ",restricted_profile_ssaid,"
-                                + newAndroidIdValue + "\n");
-                    }
+
+                case Settings.CALL_METHOD_GET_SYSTEM: {
+                    Setting setting = getSystemSettingLocked(name, requestingUserId);
+                    return packageValueForCallResult(setting);
                 }
-            }
-            return true;
-        } finally {
-            c.close();
-        }
-    }
 
-    // Lazy-initialize the settings caches for non-primary users
-    private SettingsCache getOrConstructCache(int callingUser, SparseArray<SettingsCache> which) {
-        getOrEstablishDatabase(callingUser); // ignore return value; we don't need it
-        return which.get(callingUser);
-    }
+                case Settings.CALL_METHOD_PUT_GLOBAL: {
+                    String value = getSettingValue(args);
+                    insertGlobalSettingLocked(name, value, requestingUserId);
+                } break;
 
-    // Lazy initialize the database helper and caches for this user, if necessary
-    private DatabaseHelper getOrEstablishDatabase(int callingUser) {
-        if (callingUser >= Process.SYSTEM_UID) {
-            if (USER_CHECK_THROWS) {
-                throw new IllegalArgumentException("Uid rather than user handle: " + callingUser);
-            } else {
-                Slog.wtf(TAG, "establish db for uid rather than user: " + callingUser);
-            }
-        }
+                case Settings.CALL_METHOD_PUT_SECURE: {
+                    String value = getSettingValue(args);
+                    insertSecureSettingLocked(name, value, requestingUserId);
+                } break;
 
-        long oldId = Binder.clearCallingIdentity();
-        try {
-            DatabaseHelper dbHelper;
-            synchronized (this) {
-                dbHelper = mOpenHelpers.get(callingUser);
-            }
-            if (null == dbHelper) {
-                establishDbTracking(callingUser);
-                synchronized (this) {
-                    dbHelper = mOpenHelpers.get(callingUser);
-                }
-            }
-            return dbHelper;
-        } finally {
-            Binder.restoreCallingIdentity(oldId);
-        }
-    }
+                case Settings.CALL_METHOD_PUT_SYSTEM: {
+                    String value = getSettingValue(args);
+                    insertSystemSettingLocked(name, value, requestingUserId);
+                } break;
 
-    public SettingsCache cacheForTable(final int callingUser, String tableName) {
-        if (TABLE_SYSTEM.equals(tableName)) {
-            return getOrConstructCache(callingUser, sSystemCaches);
-        }
-        if (TABLE_SECURE.equals(tableName)) {
-            return getOrConstructCache(callingUser, sSecureCaches);
-        }
-        if (TABLE_GLOBAL.equals(tableName)) {
-            return sGlobalCache;
+                default: {
+                    Slog.w(LOG_TAG, "call() with invalid method: " + method);
+                } break;
+            }
         }
         return null;
     }
 
-    /**
-     * Used for wiping a whole cache on deletes when we're not
-     * sure what exactly was deleted or changed.
-     */
-    public void invalidateCache(final int callingUser, String tableName) {
-        SettingsCache cache = cacheForTable(callingUser, tableName);
-        if (cache == null) {
-            return;
-        }
-        synchronized (cache) {
-            cache.evictAll();
-            cache.mCacheFullyMatchesDisk = false;
-        }
-    }
-
-    /**
-     * Checks if the calling user is a managed profile of the primary user.
-     * Currently only the primary user (USER_OWNER) can have managed profiles.
-     * @param callingUser the user trying to read/write settings
-     * @return true if it is a managed profile of the primary user
-     */
-    private boolean isManagedProfile(int callingUser) {
-        synchronized (this) {
-            if (mManagedProfiles == null) return false;
-            for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
-                if (mManagedProfiles.get(i).id == callingUser) {
-                    return true;
-                }
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Fast path that avoids the use of chatty remoted Cursors.
-     */
     @Override
-    public Bundle call(String method, String request, Bundle args) {
-        int callingUser = UserHandle.getCallingUserId();
-        if (args != null) {
-            int reqUser = args.getInt(Settings.CALL_METHOD_USER_KEY, callingUser);
-            if (reqUser != callingUser) {
-                callingUser = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
-                        Binder.getCallingUid(), reqUser, false, true,
-                        "get/set setting for user", null);
-                if (LOCAL_LOGV) Slog.v(TAG, "   access setting for user " + callingUser);
-            }
-        }
-
-        // Note: we assume that get/put operations for moved-to-global names have already
-        // been directed to the new location on the caller side (otherwise we'd fix them
-        // up here).
-        DatabaseHelper dbHelper;
-        SettingsCache cache;
-
-        // Get methods
-        if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) {
-            if (LOCAL_LOGV) Slog.v(TAG, "call(system:" + request + ") for " + callingUser);
-            // Check if this request should be (re)directed to the primary user's db
-            if (callingUser != UserHandle.USER_OWNER
-                    && shouldShadowParentProfile(callingUser, sSystemCloneToManagedKeys, request)) {
-                callingUser = UserHandle.USER_OWNER;
-            }
-            dbHelper = getOrEstablishDatabase(callingUser);
-            cache = sSystemCaches.get(callingUser);
-            return lookupValue(dbHelper, TABLE_SYSTEM, cache, request);
-        }
-        if (Settings.CALL_METHOD_GET_SECURE.equals(method)) {
-            if (LOCAL_LOGV) Slog.v(TAG, "call(secure:" + request + ") for " + callingUser);
-            // Check if this is a setting to be copied from the primary user
-            if (shouldShadowParentProfile(callingUser, sSecureCloneToManagedKeys, request)) {
-                // If the request if for location providers and there's a restriction, return none
-                if (Secure.LOCATION_PROVIDERS_ALLOWED.equals(request)
-                        && mUserManager.hasUserRestriction(
-                                UserManager.DISALLOW_SHARE_LOCATION, new UserHandle(callingUser))) {
-                    return sSecureCaches.get(callingUser).putIfAbsent(request, "");
-                }
-                callingUser = UserHandle.USER_OWNER;
-            }
-            dbHelper = getOrEstablishDatabase(callingUser);
-            cache = sSecureCaches.get(callingUser);
-            return lookupValue(dbHelper, TABLE_SECURE, cache, request);
-        }
-        if (Settings.CALL_METHOD_GET_GLOBAL.equals(method)) {
-            if (LOCAL_LOGV) Slog.v(TAG, "call(global:" + request + ") for " + callingUser);
-            // fast path: owner db & cache are immutable after onCreate() so we need not
-            // guard on the attempt to look them up
-            return lookupValue(getOrEstablishDatabase(UserHandle.USER_OWNER), TABLE_GLOBAL,
-                    sGlobalCache, request);
-        }
-
-        // Put methods - new value is in the args bundle under the key named by
-        // the Settings.NameValueTable.VALUE static.
-        final String newValue = (args == null)
-                ? null : args.getString(Settings.NameValueTable.VALUE);
-
-        // Framework can't do automatic permission checking for calls, so we need
-        // to do it here.
-        if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.WRITE_SETTINGS)
-                != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException(
-                    String.format("Permission denial: writing to settings requires %1$s",
-                                  android.Manifest.permission.WRITE_SETTINGS));
-        }
-
-        // Also need to take care of app op.
-        if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SETTINGS, Binder.getCallingUid(),
-                getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
-            return null;
-        }
-
-        final ContentValues values = new ContentValues();
-        values.put(Settings.NameValueTable.NAME, request);
-        values.put(Settings.NameValueTable.VALUE, newValue);
-        if (Settings.CALL_METHOD_PUT_SYSTEM.equals(method)) {
-            if (LOCAL_LOGV) {
-                Slog.v(TAG, "call_put(system:" + request + "=" + newValue + ") for "
-                        + callingUser);
-            }
-            // Extra check for USER_OWNER to optimize for the 99%
-            if (callingUser != UserHandle.USER_OWNER && shouldShadowParentProfile(callingUser,
-                    sSystemCloneToManagedKeys, request)) {
-                // Don't write these settings, as they are cloned from the parent profile
-                return null;
-            }
-            insertForUser(Settings.System.CONTENT_URI, values, callingUser);
-            // Clone the settings to the managed profiles so that notifications can be sent out
-            if (callingUser == UserHandle.USER_OWNER && mManagedProfiles != null
-                    && sSystemCloneToManagedKeys.contains(request)) {
-                final long token = Binder.clearCallingIdentity();
-                try {
-                    for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
-                        if (LOCAL_LOGV) {
-                            Slog.v(TAG, "putting to additional user "
-                                    + mManagedProfiles.get(i).id);
-                        }
-                        insertForUser(Settings.System.CONTENT_URI, values,
-                                mManagedProfiles.get(i).id);
-                    }
-                } finally {
-                    Binder.restoreCallingIdentity(token);
-                }
-            }
-        } else if (Settings.CALL_METHOD_PUT_SECURE.equals(method)) {
-            if (LOCAL_LOGV) {
-                Slog.v(TAG, "call_put(secure:" + request + "=" + newValue + ") for "
-                        + callingUser);
-            }
-            // Extra check for USER_OWNER to optimize for the 99%
-            if (callingUser != UserHandle.USER_OWNER && shouldShadowParentProfile(callingUser,
-                    sSecureCloneToManagedKeys, request)) {
-                // Don't write these settings, as they are cloned from the parent profile
-                return null;
-            }
-            insertForUser(Settings.Secure.CONTENT_URI, values, callingUser);
-            // Clone the settings to the managed profiles so that notifications can be sent out
-            if (callingUser == UserHandle.USER_OWNER && mManagedProfiles != null
-                    && sSecureCloneToManagedKeys.contains(request)) {
-                final long token = Binder.clearCallingIdentity();
-                try {
-                    for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
-                        if (LOCAL_LOGV) {
-                            Slog.v(TAG, "putting to additional user "
-                                    + mManagedProfiles.get(i).id);
-                        }
-                        try {
-                            insertForUser(Settings.Secure.CONTENT_URI, values,
-                                    mManagedProfiles.get(i).id);
-                        } catch (SecurityException e) {
-                            // Temporary fix, see b/17450158
-                            Slog.w(TAG, "Cannot clone request '" + request + "' with value '"
-                                    + newValue + "' to managed profile (id "
-                                    + mManagedProfiles.get(i).id + ")", e);
-                        }
-                    }
-                } finally {
-                    Binder.restoreCallingIdentity(token);
-                }
-            }
-        } else if (Settings.CALL_METHOD_PUT_GLOBAL.equals(method)) {
-            if (LOCAL_LOGV) {
-                Slog.v(TAG, "call_put(global:" + request + "=" + newValue + ") for "
-                        + callingUser);
-            }
-            insertForUser(Settings.Global.CONTENT_URI, values, callingUser);
-        } else {
-            Slog.w(TAG, "call() with invalid method: " + method);
-        }
-
-        return null;
-    }
-
-    /**
-     * Check if the user is a managed profile and name is one of the settings to be cloned
-     * from the parent profile.
-     */
-    private boolean shouldShadowParentProfile(int userId, HashSet<String> keys, String name) {
-        return isManagedProfile(userId) && keys.contains(name);
-    }
-
-    // Looks up value 'key' in 'table' and returns either a single-pair Bundle,
-    // possibly with a null value, or null on failure.
-    private Bundle lookupValue(DatabaseHelper dbHelper, String table,
-            final SettingsCache cache, String key) {
-        if (cache == null) {
-           Slog.e(TAG, "cache is null for user " + UserHandle.getCallingUserId() + " : key=" + key);
-           return null;
-        }
-        synchronized (cache) {
-            Bundle value = cache.get(key);
-            if (value != null) {
-                if (value != TOO_LARGE_TO_CACHE_MARKER) {
-                    return value;
-                }
-                // else we fall through and read the value from disk
-            } else if (cache.fullyMatchesDisk()) {
-                // Fast path (very common).  Don't even try touch disk
-                // if we know we've slurped it all in.  Trying to
-                // touch the disk would mean waiting for yaffs2 to
-                // give us access, which could takes hundreds of
-                // milliseconds.  And we're very likely being called
-                // from somebody's UI thread...
-                return NULL_SETTING;
-            }
-        }
-
-        SQLiteDatabase db = dbHelper.getReadableDatabase();
-        Cursor cursor = null;
-        try {
-            cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key},
-                              null, null, null, null);
-            if (cursor != null && cursor.getCount() == 1) {
-                cursor.moveToFirst();
-                return cache.putIfAbsent(key, cursor.getString(0));
-            }
-        } catch (SQLiteException e) {
-            Log.w(TAG, "settings lookup error", e);
-            return null;
-        } finally {
-            if (cursor != null) cursor.close();
-        }
-        cache.putIfAbsent(key, null);
-        return NULL_SETTING;
-    }
-
-    @Override
-    public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
-        return queryForUser(url, select, where, whereArgs, sort, UserHandle.getCallingUserId());
-    }
-
-    private Cursor queryForUser(Uri url, String[] select, String where, String[] whereArgs,
-            String sort, int forUser) {
-        if (LOCAL_LOGV) Slog.v(TAG, "query(" + url + ") for user " + forUser);
-        SqlArguments args = new SqlArguments(url, where, whereArgs);
-        DatabaseHelper dbH;
-        dbH = getOrEstablishDatabase(
-                TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : forUser);
-        SQLiteDatabase db = dbH.getReadableDatabase();
-
-        // The favorites table was moved from this provider to a provider inside Home
-        // Home still need to query this table to upgrade from pre-cupcake builds
-        // However, a cupcake+ build with no data does not contain this table which will
-        // cause an exception in the SQL stack. The following line is a special case to
-        // let the caller of the query have a chance to recover and avoid the exception
-        if (TABLE_FAVORITES.equals(args.table)) {
-            return null;
-        } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
-            args.table = TABLE_FAVORITES;
-            Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null);
-            if (cursor != null) {
-                boolean exists = cursor.getCount() > 0;
-                cursor.close();
-                if (!exists) return null;
-            } else {
-                return null;
-            }
-        }
-
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        qb.setTables(args.table);
-
-        Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
-        // the default Cursor interface does not support per-user observation
-        try {
-            AbstractCursor c = (AbstractCursor) ret;
-            c.setNotificationUri(getContext().getContentResolver(), url, forUser);
-        } catch (ClassCastException e) {
-            // details of the concrete Cursor implementation have changed and this code has
-            // not been updated to match -- complain and fail hard.
-            Log.wtf(TAG, "Incompatible cursor derivation!");
-            throw e;
-        }
-        return ret;
-    }
-
-    @Override
-    public String getType(Uri url) {
-        // If SqlArguments supplies a where clause, then it must be an item
-        // (because we aren't supplying our own where clause).
-        SqlArguments args = new SqlArguments(url, null, null);
-        if (TextUtils.isEmpty(args.where)) {
+    public String getType(Uri uri) {
+        Arguments args = new Arguments(uri, null, null, true);
+        if (TextUtils.isEmpty(args.name)) {
             return "vnd.android.cursor.dir/" + args.table;
         } else {
-            return "vnd.android.cursor.item/" + args.table;
+                return "vnd.android.cursor.item/" + args.table;
         }
     }
 
     @Override
-    public int bulkInsert(Uri uri, ContentValues[] values) {
-        final int callingUser = UserHandle.getCallingUserId();
-        if (LOCAL_LOGV) Slog.v(TAG, "bulkInsert() for user " + callingUser);
-        SqlArguments args = new SqlArguments(uri);
-        if (TABLE_FAVORITES.equals(args.table)) {
-            return 0;
-        }
-        checkWritePermissions(args);
-        SettingsCache cache = cacheForTable(callingUser, args.table);
-
-        final AtomicInteger mutationCount;
-        synchronized (this) {
-            mutationCount = sKnownMutationsInFlight.get(callingUser);
-        }
-        if (mutationCount != null) {
-            mutationCount.incrementAndGet();
-        }
-        DatabaseHelper dbH = getOrEstablishDatabase(
-                TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : callingUser);
-        SQLiteDatabase db = dbH.getWritableDatabase();
-        db.beginTransaction();
-        try {
-            int numValues = values.length;
-            for (int i = 0; i < numValues; i++) {
-                checkUserRestrictions(values[i].getAsString(Settings.Secure.NAME), callingUser);
-                if (db.insert(args.table, null, values[i]) < 0) return 0;
-                SettingsCache.populate(cache, values[i]);
-                if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
-            }
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-            if (mutationCount != null) {
-                mutationCount.decrementAndGet();
-            }
+    public Cursor query(Uri uri, String[] projection, String where, String[] whereArgs,
+            String order) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "query() for user: " + UserHandle.getCallingUserId());
         }
 
-        sendNotify(uri, callingUser);
-        return values.length;
-    }
+        Arguments args = new Arguments(uri, where, whereArgs, true);
+        String[] normalizedProjection = normalizeProjection(projection);
 
-    /*
-     * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
-     * This setting contains a list of the currently enabled location providers.
-     * But helper functions in android.providers.Settings can enable or disable
-     * a single provider by using a "+" or "-" prefix before the provider name.
-     *
-     * @returns whether the database needs to be updated or not, also modifying
-     *     'initialValues' if needed.
-     */
-    private boolean parseProviderList(Uri url, ContentValues initialValues, int desiredUser) {
-        String value = initialValues.getAsString(Settings.Secure.VALUE);
-        String newProviders = null;
-        if (value != null && value.length() > 1) {
-            char prefix = value.charAt(0);
-            if (prefix == '+' || prefix == '-') {
-                // skip prefix
-                value = value.substring(1);
+        // If a legacy table that is gone, done.
+        if (REMOVED_LEGACY_TABLES.contains(args.table)) {
+            return new MatrixCursor(normalizedProjection, 0);
+        }
 
-                // read list of enabled providers into "providers"
-                String providers = "";
-                String[] columns = {Settings.Secure.VALUE};
-                String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'";
-                Cursor cursor = queryForUser(url, columns, where, null, null, desiredUser);
-                if (cursor != null && cursor.getCount() == 1) {
-                    try {
-                        cursor.moveToFirst();
-                        providers = cursor.getString(0);
-                    } finally {
-                        cursor.close();
-                    }
-                }
-
-                int index = providers.indexOf(value);
-                int end = index + value.length();
-                // check for commas to avoid matching on partial string
-                if (index > 0 && providers.charAt(index - 1) != ',') index = -1;
-                if (end < providers.length() && providers.charAt(end) != ',') index = -1;
-
-                if (prefix == '+' && index < 0) {
-                    // append the provider to the list if not present
-                    if (providers.length() == 0) {
-                        newProviders = value;
+        synchronized (mLock) {
+            switch (args.table) {
+                case TABLE_GLOBAL: {
+                    if (args.name != null) {
+                        Setting setting = getGlobalSettingLocked(args.name);
+                        return packageSettingForQuery(setting, normalizedProjection);
                     } else {
-                        newProviders = providers + ',' + value;
+                        return getAllGlobalSettingsLocked(projection);
                     }
-                } else if (prefix == '-' && index >= 0) {
-                    // remove the provider from the list if present
-                    // remove leading or trailing comma
-                    if (index > 0) {
-                        index--;
-                    } else if (end < providers.length()) {
-                        end++;
-                    }
-
-                    newProviders = providers.substring(0, index);
-                    if (end < providers.length()) {
-                        newProviders += providers.substring(end);
-                    }
-                } else {
-                    // nothing changed, so no need to update the database
-                    return false;
                 }
 
-                if (newProviders != null) {
-                    initialValues.put(Settings.Secure.VALUE, newProviders);
+                case TABLE_SECURE: {
+                    final int userId = UserHandle.getCallingUserId();
+                    if (args.name != null) {
+                        Setting setting = getSecureSettingLocked(args.name, userId);
+                        return packageSettingForQuery(setting, normalizedProjection);
+                    } else {
+                        return getAllSecureSettingsLocked(userId, projection);
+                    }
+                }
+
+                case TABLE_SYSTEM: {
+                    final int userId = UserHandle.getCallingUserId();
+                    if (args.name != null) {
+                        Setting setting = getSystemSettingLocked(args.name, userId);
+                        return packageSettingForQuery(setting, normalizedProjection);
+                    } else {
+                        return getAllSystemSettingsLocked(userId, projection);
+                    }
+                }
+
+                default: {
+                    throw new IllegalArgumentException("Invalid Uri path:" + uri);
                 }
             }
         }
-
-        return true;
     }
 
     @Override
-    public Uri insert(Uri url, ContentValues initialValues) {
-        return insertForUser(url, initialValues, UserHandle.getCallingUserId());
-    }
-
-    // Settings.put*ForUser() always winds up here, so this is where we apply
-    // policy around permission to write settings for other users.
-    private Uri insertForUser(Uri url, ContentValues initialValues, int desiredUserHandle) {
-        final int callingUser = UserHandle.getCallingUserId();
-        if (callingUser != desiredUserHandle) {
-            getContext().enforceCallingOrSelfPermission(
-                    android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
-                    "Not permitted to access settings for other users");
+    public Uri insert(Uri uri, ContentValues values) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "insert() for user: " + UserHandle.getCallingUserId());
         }
 
-        if (LOCAL_LOGV) Slog.v(TAG, "insert(" + url + ") for user " + desiredUserHandle
-                + " by " + callingUser);
+        String table = getValidTableOrThrow(uri);
 
-        SqlArguments args = new SqlArguments(url);
-        if (TABLE_FAVORITES.equals(args.table)) {
+        // If a legacy table that is gone, done.
+        if (REMOVED_LEGACY_TABLES.contains(table)) {
             return null;
         }
 
-        // Special case LOCATION_PROVIDERS_ALLOWED.
-        // Support enabling/disabling a single provider (using "+" or "-" prefix)
-        String name = initialValues.getAsString(Settings.Secure.NAME);
-        if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
-            if (!parseProviderList(url, initialValues, desiredUserHandle)) return null;
+        String name = values.getAsString(Settings.Secure.NAME);
+        if (TextUtils.isEmpty(name)) {
+            return null;
         }
 
-        // If this is an insert() of a key that has been migrated to the global store,
-        // redirect the operation to that store
-        if (name != null) {
-            if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) {
-                if (!TABLE_GLOBAL.equals(args.table)) {
-                    if (LOCAL_LOGV) Slog.i(TAG, "Rewrite of insert() of now-global key " + name);
+        String value = values.getAsString(Settings.Secure.VALUE);
+
+        synchronized (mLock) {
+            switch (table) {
+                case TABLE_GLOBAL: {
+                    if (insertGlobalSettingLocked(name, value, UserHandle.getCallingUserId())) {
+                        return Uri.withAppendedPath(Settings.Global.CONTENT_URI, name);
+                    }
+                } break;
+
+                case TABLE_SECURE: {
+                    if (insertSecureSettingLocked(name, value, UserHandle.getCallingUserId())) {
+                        return Uri.withAppendedPath(Settings.Secure.CONTENT_URI, name);
+                    }
+                } break;
+
+                case TABLE_SYSTEM: {
+                    if (insertSystemSettingLocked(name, value, UserHandle.getCallingUserId())) {
+                        return Uri.withAppendedPath(Settings.System.CONTENT_URI, name);
+                    }
+                } break;
+
+                default: {
+                    throw new IllegalArgumentException("Bad Uri path:" + uri);
                 }
-                args.table = TABLE_GLOBAL;  // next condition will rewrite the user handle
             }
         }
 
-        // Check write permissions only after determining which table the insert will touch
-        checkWritePermissions(args);
-
-        checkUserRestrictions(name, desiredUserHandle);
-
-        // The global table is stored under the owner, always
-        if (TABLE_GLOBAL.equals(args.table)) {
-            desiredUserHandle = UserHandle.USER_OWNER;
-        }
-
-        SettingsCache cache = cacheForTable(desiredUserHandle, args.table);
-        String value = initialValues.getAsString(Settings.NameValueTable.VALUE);
-        if (SettingsCache.isRedundantSetValue(cache, name, value)) {
-            return Uri.withAppendedPath(url, name);
-        }
-
-        final AtomicInteger mutationCount;
-        synchronized (this) {
-            mutationCount = sKnownMutationsInFlight.get(callingUser);
-        }
-        if (mutationCount != null) {
-            mutationCount.incrementAndGet();
-        }
-        DatabaseHelper dbH = getOrEstablishDatabase(desiredUserHandle);
-        SQLiteDatabase db = dbH.getWritableDatabase();
-        final long rowId = db.insert(args.table, null, initialValues);
-        if (mutationCount != null) {
-            mutationCount.decrementAndGet();
-        }
-        if (rowId <= 0) return null;
-
-        SettingsCache.populate(cache, initialValues);  // before we notify
-
-        if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues
-                + " for user " + desiredUserHandle);
-        // Note that we use the original url here, not the potentially-rewritten table name
-        url = getUriFor(url, initialValues, rowId);
-        sendNotify(url, desiredUserHandle);
-        return url;
+        return null;
     }
 
     @Override
-    public int delete(Uri url, String where, String[] whereArgs) {
-        int callingUser = UserHandle.getCallingUserId();
-        if (LOCAL_LOGV) Slog.v(TAG, "delete() for user " + callingUser);
-        SqlArguments args = new SqlArguments(url, where, whereArgs);
-        if (TABLE_FAVORITES.equals(args.table)) {
-            return 0;
-        } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
-            args.table = TABLE_FAVORITES;
-        } else if (TABLE_GLOBAL.equals(args.table)) {
-            callingUser = UserHandle.USER_OWNER;
+    public int bulkInsert(Uri uri, ContentValues[] allValues) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "bulkInsert() for user: " + UserHandle.getCallingUserId());
         }
-        checkWritePermissions(args);
 
-        final AtomicInteger mutationCount;
-        synchronized (this) {
-            mutationCount = sKnownMutationsInFlight.get(callingUser);
+        int insertionCount = 0;
+        final int valuesCount = allValues.length;
+        for (int i = 0; i < valuesCount; i++) {
+            ContentValues values = allValues[i];
+            if (insert(uri, values) != null) {
+                insertionCount++;
+            }
         }
-        if (mutationCount != null) {
-            mutationCount.incrementAndGet();
-        }
-        DatabaseHelper dbH = getOrEstablishDatabase(callingUser);
-        SQLiteDatabase db = dbH.getWritableDatabase();
-        int count = db.delete(args.table, args.where, args.args);
-        if (mutationCount != null) {
-            mutationCount.decrementAndGet();
-        }
-        if (count > 0) {
-            invalidateCache(callingUser, args.table);  // before we notify
-            sendNotify(url, callingUser);
-        }
-        startAsyncCachePopulation(callingUser);
-        if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
-        return count;
+
+        return insertionCount;
     }
 
     @Override
-    public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
-        // NOTE: update() is never called by the front-end Settings API, and updates that
-        // wind up affecting rows in Secure that are globally shared will not have the
-        // intended effect (the update will be invisible to the rest of the system).
-        // This should have no practical effect, since writes to the Secure db can only
-        // be done by system code, and that code should be using the correct API up front.
-        int callingUser = UserHandle.getCallingUserId();
-        if (LOCAL_LOGV) Slog.v(TAG, "update() for user " + callingUser);
-        SqlArguments args = new SqlArguments(url, where, whereArgs);
-        if (TABLE_FAVORITES.equals(args.table)) {
-            return 0;
-        } else if (TABLE_GLOBAL.equals(args.table)) {
-            callingUser = UserHandle.USER_OWNER;
+    public int delete(Uri uri, String where, String[] whereArgs) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "delete() for user: " + UserHandle.getCallingUserId());
         }
-        checkWritePermissions(args);
-        checkUserRestrictions(initialValues.getAsString(Settings.Secure.NAME), callingUser);
 
-        final AtomicInteger mutationCount;
-        synchronized (this) {
-            mutationCount = sKnownMutationsInFlight.get(callingUser);
+        Arguments args = new Arguments(uri, where, whereArgs, false);
+
+        // If a legacy table that is gone, done.
+        if (REMOVED_LEGACY_TABLES.contains(args.table)) {
+            return 0;
         }
-        if (mutationCount != null) {
-            mutationCount.incrementAndGet();
+
+        if (TextUtils.isEmpty(args.name)) {
+            return 0;
         }
-        DatabaseHelper dbH = getOrEstablishDatabase(callingUser);
-        SQLiteDatabase db = dbH.getWritableDatabase();
-        int count = db.update(args.table, initialValues, args.where, args.args);
-        if (mutationCount != null) {
-            mutationCount.decrementAndGet();
+
+        synchronized (mLock) {
+            switch (args.table) {
+                case TABLE_GLOBAL: {
+                    final int userId = UserHandle.getCallingUserId();
+                    return deleteGlobalSettingLocked(args.name, userId) ? 1 : 0;
+                }
+
+                case TABLE_SECURE: {
+                    final int userId = UserHandle.getCallingUserId();
+                    return deleteSecureSettingLocked(args.name, userId) ? 1 : 0;
+                }
+
+                case TABLE_SYSTEM: {
+                    final int userId = UserHandle.getCallingUserId();
+                    return deleteSystemSettingLocked(args.name, userId) ? 1 : 0;
+                }
+
+                default: {
+                    throw new IllegalArgumentException("Bad Uri path:" + uri);
+                }
+            }
         }
-        if (count > 0) {
-            invalidateCache(callingUser, args.table);  // before we notify
-            sendNotify(url, callingUser);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "update() for user: " + UserHandle.getCallingUserId());
         }
-        startAsyncCachePopulation(callingUser);
-        if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
-        return count;
+
+        Arguments args = new Arguments(uri, where, whereArgs, false);
+
+        // If a legacy table that is gone, done.
+        if (REMOVED_LEGACY_TABLES.contains(args.table)) {
+            return 0;
+        }
+
+        String value = values.getAsString(Settings.Secure.VALUE);
+        if (TextUtils.isEmpty(value)) {
+            return 0;
+        }
+
+        synchronized (mLock) {
+            switch (args.table) {
+                case TABLE_GLOBAL: {
+                    final int userId = UserHandle.getCallingUserId();
+                    return updateGlobalSettingLocked(args.name, value, userId) ? 1 : 0;
+                }
+
+                case TABLE_SECURE: {
+                    final int userId = UserHandle.getCallingUserId();
+                    return updateSecureSettingLocked(args.name, value, userId) ? 1 : 0;
+                }
+
+                case TABLE_SYSTEM: {
+                    final int userId = UserHandle.getCallingUserId();
+                    return updateSystemSettingLocked(args.name, value, userId) ? 1 : 0;
+                }
+
+                default: {
+                    throw new IllegalArgumentException("Invalid Uri path:" + uri);
+                }
+            }
+        }
     }
 
     @Override
@@ -1229,102 +479,1360 @@
                 + "ringtone playback is available through android.media.Ringtone");
     }
 
-    /**
-     * In-memory LRU Cache of system and secure settings, along with
-     * associated helper functions to keep cache coherent with the
-     * database.
+    private void registerBroadcastReceivers() {
+        IntentFilter userFilter = new IntentFilter();
+        userFilter.addAction(Intent.ACTION_USER_REMOVED);
+        userFilter.addAction(Intent.ACTION_USER_STOPPED);
+
+        getContext().registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
+                        UserHandle.USER_OWNER);
+
+                switch (intent.getAction()) {
+                    case Intent.ACTION_USER_REMOVED: {
+                        mSettingsRegistry.removeUserStateLocked(userId, true);
+                    } break;
+
+                    case Intent.ACTION_USER_STOPPED: {
+                        mSettingsRegistry.removeUserStateLocked(userId, false);
+                    } break;
+                }
+            }
+        }, userFilter);
+
+        PackageMonitor monitor = new PackageMonitor() {
+            @Override
+            public void onPackageRemoved(String packageName, int uid) {
+                synchronized (mLock) {
+                    mSettingsRegistry.onPackageRemovedLocked(packageName,
+                            UserHandle.getUserId(uid));
+                }
+            }
+        };
+
+        // package changes
+        monitor.register(getContext(), BackgroundThread.getHandler().getLooper(),
+                UserHandle.ALL, true);
+    }
+
+    private Cursor getAllGlobalSettingsLocked(String[] projection) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "getAllGlobalSettingsLocked()");
+        }
+
+        // Get the settings.
+        SettingsState settingsState = mSettingsRegistry.getSettingsLocked(
+                SettingsRegistry.SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+
+        List<String> names = settingsState.getSettingNamesLocked();
+
+        final int nameCount = names.size();
+
+        String[] normalizedProjection = normalizeProjection(projection);
+        MatrixCursor result = new MatrixCursor(normalizedProjection, nameCount);
+
+        // Anyone can get the global settings, so no security checks.
+        for (int i = 0; i < nameCount; i++) {
+            String name = names.get(i);
+            Setting setting = settingsState.getSettingLocked(name);
+            appendSettingToCursor(result, setting);
+        }
+
+        return result;
+    }
+
+    private Setting getGlobalSettingLocked(String name) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "getGlobalSetting(" + name + ")");
+        }
+
+        // Get the value.
+        return mSettingsRegistry.getSettingLocked(SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+                UserHandle.USER_OWNER, name);
+    }
+
+    private boolean updateGlobalSettingLocked(String name, String value, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "updateGlobalSettingLocked(" + name + ", " + value + ")");
+        }
+        return mutateGlobalSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_UPDATE);
+    }
+
+    private boolean insertGlobalSettingLocked(String name, String value, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "insertGlobalSettingLocked(" + name + ", " + value + ")");
+        }
+        return mutateGlobalSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT);
+    }
+
+    private boolean deleteGlobalSettingLocked(String name, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "deleteGlobalSettingLocked(" + name + ")");
+        }
+        return mutateGlobalSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE);
+    }
+
+    private boolean mutateGlobalSettingLocked(String name, String value, int requestingUserId,
+            int operation) {
+        // Make sure the caller can change the settings - treated as secure.
+        enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
+
+        // Verify whether this operation is allowed for the calling package.
+        if (!isAppOpWriteSettingsAllowedForCallingPackage()) {
+            return false;
+        }
+
+        // Resolve the userId on whose behalf the call is made.
+        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+
+        // If this is a setting that is currently restricted for this user, done.
+        if (isGlobalOrSecureSettingRestrictedForUser(name, callingUserId)) {
+            return false;
+        }
+
+        // Perform the mutation.
+        switch (operation) {
+            case MUTATION_OPERATION_INSERT: {
+                return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+                        UserHandle.USER_OWNER, name, value, getCallingPackage());
+            }
+
+            case MUTATION_OPERATION_DELETE: {
+                return mSettingsRegistry.deleteSettingLocked(
+                        SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+                        UserHandle.USER_OWNER, name);
+            }
+
+            case MUTATION_OPERATION_UPDATE: {
+                return mSettingsRegistry.updateSettingLocked(SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+                        UserHandle.USER_OWNER, name, value, getCallingPackage());
+            }
+        }
+
+        return false;
+    }
+
+    private Cursor getAllSecureSettingsLocked(int userId, String[] projection) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "getAllSecureSettings(" + userId + ")");
+        }
+
+        // Resolve the userId on whose behalf the call is made.
+        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(userId);
+
+        List<String> names = mSettingsRegistry.getSettingsNamesLocked(
+                SettingsRegistry.SETTINGS_TYPE_SECURE, callingUserId);
+
+        final int nameCount = names.size();
+
+        String[] normalizedProjection = normalizeProjection(projection);
+        MatrixCursor result = new MatrixCursor(normalizedProjection, nameCount);
+
+        for (int i = 0; i < nameCount; i++) {
+            String name = names.get(i);
+
+            // Determine the owning user as some profile settings are cloned from the parent.
+            final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+
+            // Special case for location (sigh).
+            if (isLocationProvidersAllowedRestricted(name, callingUserId, owningUserId)) {
+                return null;
+            }
+
+            Setting setting = mSettingsRegistry.getSettingLocked(
+                    SettingsRegistry.SETTINGS_TYPE_SECURE, owningUserId, name);
+            appendSettingToCursor(result, setting);
+        }
+
+        return result;
+    }
+
+    private Setting getSecureSettingLocked(String name, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "getSecureSetting(" + name + ", " + requestingUserId + ")");
+        }
+
+        // Resolve the userId on whose behalf the call is made.
+        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+
+        // Determine the owning user as some profile settings are cloned from the parent.
+        final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+
+        // Special case for location (sigh).
+        if (isLocationProvidersAllowedRestricted(name, callingUserId, owningUserId)) {
+            return null;
+        }
+
+        // Get the value.
+        return mSettingsRegistry.getSettingLocked(SettingsRegistry.SETTINGS_TYPE_SECURE,
+                owningUserId, name);
+    }
+
+    private boolean insertSecureSettingLocked(String name, String value, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "insertSecureSettingLocked(" + name + ", " + value + ", "
+                    + requestingUserId + ")");
+        }
+
+        return mutateSecureSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT);
+    }
+
+    private boolean deleteSecureSettingLocked(String name, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "deleteSecureSettingLocked(" + name + ", " + requestingUserId + ")");
+        }
+
+        return mutateSecureSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE);
+    }
+
+    private boolean updateSecureSettingLocked(String name, String value, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "updateSecureSettingLocked(" + name + ", " + value + ", "
+                    + requestingUserId + ")");
+        }
+
+        return mutateSecureSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_UPDATE);
+    }
+
+    private boolean mutateSecureSettingLocked(String name, String value, int requestingUserId,
+            int operation) {
+        // Make sure the caller can change the settings.
+        enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
+
+        // Verify whether this operation is allowed for the calling package.
+        if (!isAppOpWriteSettingsAllowedForCallingPackage()) {
+            return false;
+        }
+
+        // Resolve the userId on whose behalf the call is made.
+        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+
+        // If this is a setting that is currently restricted for this user, done.
+        if (isGlobalOrSecureSettingRestrictedForUser(name, callingUserId)) {
+            return false;
+        }
+
+        // Determine the owning user as some profile settings are cloned from the parent.
+        final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+
+        // Only the owning user can change the setting.
+        if (owningUserId != callingUserId) {
+            return false;
+        }
+
+        // Special cases for location providers (sigh).
+        if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
+            return updateLocationProvidersAllowed(value, owningUserId);
+        }
+
+        // Mutate the value.
+        switch(operation) {
+            case MUTATION_OPERATION_INSERT: {
+                return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_SECURE,
+                        owningUserId, name, value, getCallingPackage());
+            }
+
+            case MUTATION_OPERATION_DELETE: {
+                return mSettingsRegistry.deleteSettingLocked(
+                        SettingsRegistry.SETTINGS_TYPE_SECURE,
+                        owningUserId, name);
+            }
+
+            case MUTATION_OPERATION_UPDATE: {
+                return mSettingsRegistry.updateSettingLocked(SettingsRegistry.SETTINGS_TYPE_SECURE,
+                        owningUserId, name, value, getCallingPackage());
+            }
+        }
+
+        return false;
+    }
+
+    private Cursor getAllSystemSettingsLocked(int userId, String[] projection) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "getAllSecureSystemLocked(" + userId + ")");
+        }
+
+        // Resolve the userId on whose behalf the call is made.
+        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(userId);
+
+        List<String> names = mSettingsRegistry.getSettingsNamesLocked(
+                SettingsRegistry.SETTINGS_TYPE_SYSTEM, callingUserId);
+
+        final int nameCount = names.size();
+
+        String[] normalizedProjection = normalizeProjection(projection);
+        MatrixCursor result = new MatrixCursor(normalizedProjection, nameCount);
+
+        for (int i = 0; i < nameCount; i++) {
+            String name = names.get(i);
+
+            // Determine the owning user as some profile settings are cloned from the parent.
+            final int owningUserId = resolveOwningUserIdForSystemSettingLocked(callingUserId, name);
+
+            Setting setting = mSettingsRegistry.getSettingLocked(
+                    SettingsRegistry.SETTINGS_TYPE_SYSTEM, owningUserId, name);
+            appendSettingToCursor(result, setting);
+        }
+
+        return result;
+    }
+
+    private Setting getSystemSettingLocked(String name, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "getSystemSetting(" + name + ", " + requestingUserId + ")");
+        }
+
+        // Resolve the userId on whose behalf the call is made.
+        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+
+        // Determine the owning user as some profile settings are cloned from the parent.
+        final int owningUserId = resolveOwningUserIdForSystemSettingLocked(callingUserId, name);
+
+        // Get the value.
+        return mSettingsRegistry.getSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+                owningUserId, name);
+    }
+
+    private boolean insertSystemSettingLocked(String name, String value, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "insertSystemSettingLocked(" + name + ", " + value + ", "
+                    + requestingUserId + ")");
+        }
+
+        return mutateSystemSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT);
+    }
+
+    private boolean deleteSystemSettingLocked(String name, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "deleteSystemSettingLocked(" + name + ", " + requestingUserId + ")");
+        }
+
+        return mutateSystemSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE);
+    }
+
+    private boolean updateSystemSettingLocked(String name, String value, int requestingUserId) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "updateSystemSettingLocked(" + name + ", " + value + ", "
+                    + requestingUserId + ")");
+        }
+
+        return mutateSystemSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_UPDATE);
+    }
+
+    private boolean mutateSystemSettingLocked(String name, String value, int runAsUserId,
+            int operation) {
+        // Make sure the caller can change the settings.
+        enforceWritePermission(Manifest.permission.WRITE_SETTINGS);
+
+        // Verify whether this operation is allowed for the calling package.
+        if (!isAppOpWriteSettingsAllowedForCallingPackage()) {
+            return false;
+        }
+
+        // Enforce what the calling package can mutate in the system settings.
+        enforceRestrictedSystemSettingsMutationForCallingPackageLocked(operation, name);
+
+        // Resolve the userId on whose behalf the call is made.
+        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(runAsUserId);
+
+        // Determine the owning user as some profile settings are cloned from the parent.
+        final int owningUserId = resolveOwningUserIdForSystemSettingLocked(callingUserId, name);
+
+        // Only the owning user id can change the setting.
+        if (owningUserId != callingUserId) {
+            return false;
+        }
+
+        // Mutate the value.
+        switch (operation) {
+            case MUTATION_OPERATION_INSERT: {
+                validateSystemSettingValue(name, value);
+                return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+                        owningUserId, name, value, getCallingPackage());
+            }
+
+            case MUTATION_OPERATION_DELETE: {
+                return mSettingsRegistry.deleteSettingLocked(
+                        SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+                        owningUserId, name);
+            }
+
+            case MUTATION_OPERATION_UPDATE: {
+                validateSystemSettingValue(name, value);
+                return mSettingsRegistry.updateSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+                        owningUserId, name, value, getCallingPackage());
+            }
+        }
+
+        return false;
+    }
+
+    private void validateSystemSettingValue(String name, String value) {
+        Settings.System.Validator validator = Settings.System.VALIDATORS.get(name);
+        if (validator != null && !validator.validate(value)) {
+            throw new IllegalArgumentException("Invalid value: " + value
+                    + " for setting: " + name);
+        }
+    }
+
+    private boolean isLocationProvidersAllowedRestricted(String name, int callingUserId,
+            int owningUserId) {
+        // Optimization - location providers are restricted only for managed profiles.
+        if (callingUserId == owningUserId) {
+            return false;
+        }
+        if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)
+                && mUserManager.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION,
+                new UserHandle(callingUserId))) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isGlobalOrSecureSettingRestrictedForUser(String setting, int userId) {
+        String restriction = sSettingToUserRestrictionMap.get(setting);
+        if (restriction == null) {
+            return false;
+        }
+        return mUserManager.hasUserRestriction(restriction, new UserHandle(userId));
+    }
+
+    private int resolveOwningUserIdForSecureSettingLocked(int userId, String setting) {
+        return resolveOwningUserIdLocked(userId, sSecureCloneToManagedSettings, setting);
+    }
+
+    private int resolveOwningUserIdForSystemSettingLocked(int userId, String setting) {
+        return resolveOwningUserIdLocked(userId, sSystemCloneToManagedSettings, setting);
+    }
+
+    private int resolveOwningUserIdLocked(int userId, Set<String> keys, String name) {
+        final int parentId = getGroupParentLocked(userId);
+        if (parentId != userId && keys.contains(name)) {
+            return parentId;
+        }
+        return userId;
+    }
+
+    private void enforceRestrictedSystemSettingsMutationForCallingPackageLocked(int operation,
+            String name) {
+        // System/root/shell can mutate whatever secure settings they want.
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid == android.os.Process.SYSTEM_UID
+                || callingUid == Process.SHELL_UID
+                || callingUid == Process.ROOT_UID) {
+            return;
+        }
+
+        switch (operation) {
+            case MUTATION_OPERATION_INSERT:
+                // Insert updates.
+            case MUTATION_OPERATION_UPDATE: {
+                if (Settings.System.PUBLIC_SETTINGS.contains(name)) {
+                    return;
+                }
+
+                // The calling package is already verified.
+                PackageInfo packageInfo = getCallingPackageInfoOrThrow();
+
+                // Privileged apps can do whatever they want.
+                if ((packageInfo.applicationInfo.privateFlags
+                        & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
+                    return;
+                }
+
+                warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
+                        packageInfo.applicationInfo.targetSdkVersion, name);
+            } break;
+
+            case MUTATION_OPERATION_DELETE: {
+                if (Settings.System.PUBLIC_SETTINGS.contains(name)
+                        || Settings.System.PRIVATE_SETTINGS.contains(name)) {
+                    throw new IllegalArgumentException("You cannot delete system defined"
+                            + " secure settings.");
+                }
+
+                // The calling package is already verified.
+                PackageInfo packageInfo = getCallingPackageInfoOrThrow();
+
+                // Privileged apps can do whatever they want.
+                if ((packageInfo.applicationInfo.privateFlags &
+                        ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
+                    return;
+                }
+
+                warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
+                        packageInfo.applicationInfo.targetSdkVersion, name);
+            } break;
+        }
+    }
+
+    private PackageInfo getCallingPackageInfoOrThrow() {
+        try {
+            return mPackageManager.getPackageInfo(getCallingPackage(), 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new IllegalStateException("Calling package doesn't exist");
+        }
+    }
+
+    private int getGroupParentLocked(int userId) {
+        // Most frequent use case.
+        if (userId == UserHandle.USER_OWNER) {
+            return userId;
+        }
+        // We are in the same process with the user manager and the returned
+        // user info is a cached instance, so just look up instead of cache.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            UserInfo userInfo = mUserManager.getProfileParent(userId);
+            return (userInfo != null) ? userInfo.id : userId;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private boolean isAppOpWriteSettingsAllowedForCallingPackage() {
+        final int callingUid = Binder.getCallingUid();
+
+        mAppOpsManager.checkPackage(Binder.getCallingUid(), getCallingPackage());
+
+        return mAppOpsManager.noteOp(AppOpsManager.OP_WRITE_SETTINGS, callingUid,
+                getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
+    }
+
+    private void enforceWritePermission(String permission) {
+        if (getContext().checkCallingOrSelfPermission(permission)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Permission denial: writing to settings requires:"
+                    + permission);
+        }
+    }
+
+    /*
+     * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
+     * This setting contains a list of the currently enabled location providers.
+     * But helper functions in android.providers.Settings can enable or disable
+     * a single provider by using a "+" or "-" prefix before the provider name.
+     *
+     * @returns whether the enabled location providers changed.
      */
-    private static final class SettingsCache extends LruCache<String, Bundle> {
-
-        private final String mCacheName;
-        private boolean mCacheFullyMatchesDisk = false;  // has the whole database slurped.
-
-        public SettingsCache(String name) {
-            super(MAX_CACHE_ENTRIES);
-            mCacheName = name;
+    private boolean updateLocationProvidersAllowed(String value, int owningUserId) {
+        if (TextUtils.isEmpty(value)) {
+            return false;
         }
 
-        /**
-         * Is the whole database table slurped into this cache?
-         */
-        public boolean fullyMatchesDisk() {
-            synchronized (this) {
-                return mCacheFullyMatchesDisk;
+        final char prefix = value.charAt(0);
+        if (prefix != '+' && prefix != '-') {
+            return false;
+        }
+
+        // skip prefix
+        value = value.substring(1);
+
+        Setting settingValue = getSecureSettingLocked(
+                Settings.Secure.LOCATION_PROVIDERS_ALLOWED, owningUserId);
+
+        String oldProviders = (settingValue != null) ? settingValue.getValue() : "";
+
+        int index = oldProviders.indexOf(value);
+        int end = index + value.length();
+
+        // check for commas to avoid matching on partial string
+        if (index > 0 && oldProviders.charAt(index - 1) != ',') {
+            index = -1;
+        }
+
+        // check for commas to avoid matching on partial string
+        if (end < oldProviders.length() && oldProviders.charAt(end) != ',') {
+            index = -1;
+        }
+
+        String newProviders;
+
+        if (prefix == '+' && index < 0) {
+            // append the provider to the list if not present
+            if (oldProviders.length() == 0) {
+                newProviders = value;
+            } else {
+                newProviders = oldProviders + ',' + value;
+            }
+        } else if (prefix == '-' && index >= 0) {
+            // remove the provider from the list if present
+            // remove leading or trailing comma
+            if (index > 0) {
+                index--;
+            } else if (end < oldProviders.length()) {
+                end++;
+            }
+
+            newProviders = oldProviders.substring(0, index);
+            if (end < oldProviders.length()) {
+                newProviders += oldProviders.substring(end);
+            }
+        } else {
+            // nothing changed, so no need to update the database
+            return false;
+        }
+
+        updateSecureSettingLocked(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+                newProviders, owningUserId);
+
+        return true;
+    }
+
+    private void sendNotify(Uri uri, int userId) {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            getContext().getContentResolver().notifyChange(uri, null, true, userId);
+            if (DEBUG) {
+                Slog.v(LOG_TAG, "Notifying for " + userId + ": " + uri);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private static void warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
+            int targetSdkVersion, String name) {
+        // If the app targets Lollipop MR1 or older SDK we warn, otherwise crash.
+        if (targetSdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+            if (Settings.System.PRIVATE_SETTINGS.contains(name)) {
+                Slog.w(LOG_TAG, "You shouldn't not change private system settings."
+                        + " This will soon become an error.");
+            } else {
+                Slog.w(LOG_TAG, "You shouldn't keep your settings in the secure settings."
+                        + " This will soon become an error.");
+            }
+        } else {
+            if (Settings.System.PRIVATE_SETTINGS.contains(name)) {
+                throw new IllegalArgumentException("You cannot change private secure settings.");
+            } else {
+                throw new IllegalArgumentException("You cannot keep your settings in"
+                        + " the secure settings.");
+            }
+        }
+    }
+
+    private static int resolveCallingUserIdEnforcingPermissionsLocked(int requestingUserId) {
+        if (requestingUserId == UserHandle.getCallingUserId()) {
+            return requestingUserId;
+        }
+        return ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+                Binder.getCallingUid(), requestingUserId, false, true,
+                "get/set setting for user", null);
+    }
+
+    private static Bundle packageValueForCallResult(Setting setting) {
+        if (setting == null) {
+            return NULL_SETTING;
+        }
+        return Bundle.forPair(Settings.NameValueTable.VALUE, setting.getValue());
+    }
+
+    private static int getRequestingUserId(Bundle args) {
+        final int callingUserId = UserHandle.getCallingUserId();
+        return (args != null) ? args.getInt(Settings.CALL_METHOD_USER_KEY, callingUserId)
+                : callingUserId;
+    }
+
+    private static String getSettingValue(Bundle args) {
+        return (args != null) ? args.getString(Settings.NameValueTable.VALUE) : null;
+    }
+
+    private static String getValidTableOrThrow(Uri uri) {
+        if (uri.getPathSegments().size() > 0) {
+            String table = uri.getPathSegments().get(0);
+            if (DatabaseHelper.isValidTable(table)) {
+                return table;
+            }
+            throw new IllegalArgumentException("Bad root path: " + table);
+        }
+        throw new IllegalArgumentException("Invalid URI:" + uri);
+    }
+
+    private static MatrixCursor packageSettingForQuery(Setting setting, String[] projection) {
+        if (setting == null) {
+            return new MatrixCursor(projection, 0);
+        }
+        MatrixCursor cursor = new MatrixCursor(projection, 1);
+        appendSettingToCursor(cursor, setting);
+        return cursor;
+    }
+
+    private static String[] normalizeProjection(String[] projection) {
+        if (projection == null) {
+            return ALL_COLUMNS;
+        }
+
+        final int columnCount = projection.length;
+        for (int i = 0; i < columnCount; i++) {
+            String column = projection[i];
+            if (!ArrayUtils.contains(ALL_COLUMNS, column)) {
+                throw new IllegalArgumentException("Invalid column: " + column);
             }
         }
 
-        public void setFullyMatchesDisk(boolean value) {
-            synchronized (this) {
-                mCacheFullyMatchesDisk = value;
+        return projection;
+    }
+
+    private static void appendSettingToCursor(MatrixCursor cursor, Setting setting) {
+        final int columnCount = cursor.getColumnCount();
+
+        String[] values =  new String[columnCount];
+
+        for (int i = 0; i < columnCount; i++) {
+            String column = cursor.getColumnName(i);
+
+            switch (column) {
+                case Settings.NameValueTable._ID: {
+                    values[i] = setting.getId();
+                } break;
+
+                case Settings.NameValueTable.NAME: {
+                    values[i] = setting.getName();
+                } break;
+
+                case Settings.NameValueTable.VALUE: {
+                    values[i] = setting.getValue();
+                } break;
             }
         }
 
-        @Override
-        protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) {
-            if (evicted) {
-                mCacheFullyMatchesDisk = false;
-            }
-        }
+        cursor.addRow(values);
+    }
 
-        /**
-         * Atomic cache population, conditional on size of value and if
-         * we lost a race.
-         *
-         * @returns a Bundle to send back to the client from call(), even
-         *     if we lost the race.
-         */
-        public Bundle putIfAbsent(String key, String value) {
-            Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value);
-            if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
-                synchronized (this) {
-                    if (get(key) == null) {
-                        put(key, bundle);
+    private static final class Arguments {
+        private static final Pattern WHERE_PATTERN_WITH_PARAM_NO_BRACKETS =
+                Pattern.compile("[\\s]*name[\\s]*=[\\s]*\\?[\\s]*");
+
+        private static final Pattern WHERE_PATTERN_WITH_PARAM_IN_BRACKETS =
+                Pattern.compile("[\\s]*\\([\\s]*name[\\s]*=[\\s]*\\?[\\s]*\\)[\\s]*");
+
+        private static final Pattern WHERE_PATTERN_NO_PARAM_IN_BRACKETS =
+                Pattern.compile("[\\s]*\\([\\s]*name[\\s]*=[\\s]*['\"].*['\"][\\s]*\\)[\\s]*");
+
+        private static final Pattern WHERE_PATTERN_NO_PARAM_NO_BRACKETS =
+                Pattern.compile("[\\s]*name[\\s]*=[\\s]*['\"].*['\"][\\s]*");
+
+        public final String table;
+        public final String name;
+
+        public Arguments(Uri uri, String where, String[] whereArgs, boolean supportAll) {
+            final int segmentSize = uri.getPathSegments().size();
+            switch (segmentSize) {
+                case 1: {
+                    if (where != null
+                            && (WHERE_PATTERN_WITH_PARAM_NO_BRACKETS.matcher(where).matches()
+                                || WHERE_PATTERN_WITH_PARAM_IN_BRACKETS.matcher(where).matches())
+                            && whereArgs.length == 1) {
+                        name = whereArgs[0];
+                        table = computeTableForSetting(uri, name);
+                    } else if (where != null
+                            && (WHERE_PATTERN_NO_PARAM_NO_BRACKETS.matcher(where).matches()
+                                || WHERE_PATTERN_NO_PARAM_IN_BRACKETS.matcher(where).matches())) {
+                        final int startIndex = Math.max(where.indexOf("'"),
+                                where.indexOf("\"")) + 1;
+                        final int endIndex = Math.max(where.lastIndexOf("'"),
+                                where.lastIndexOf("\""));
+                        name = where.substring(startIndex, endIndex);
+                        table = computeTableForSetting(uri, name);
+                    } else if (supportAll && where == null && whereArgs == null) {
+                        name = null;
+                        table = computeTableForSetting(uri, null);
+                    } else if (uri.getPathSegments().size() == 2
+                            && where == null && whereArgs == null) {
+                        name = uri.getPathSegments().get(1);
+                        table = computeTableForSetting(uri, name);
+                    } else {
+                        EventLogTags.writeUnsupportedSettingsQuery(
+                                uri.toSafeString(), where, Arrays.toString(whereArgs));
+                        throw new IllegalArgumentException("Only null where and args"
+                                + " or name=? where and a single arg or name='SOME_SETTING' "
+                                + "are supported uri: " + uri + " where: " + where + " args: "
+                                + Arrays.toString(whereArgs));
                     }
+                } break;
+
+                default: {
+                    throw new IllegalArgumentException("Invalid URI: " + uri);
                 }
             }
-            return bundle;
         }
 
-        /**
-         * Populates a key in a given (possibly-null) cache.
-         */
-        public static void populate(SettingsCache cache, ContentValues contentValues) {
-            if (cache == null) {
-                return;
+        public static String computeTableForSetting(Uri uri, String name) {
+            String table = getValidTableOrThrow(uri);
+
+            if (name != null) {
+                if (sSystemMovedToSecureSettings.contains(name)) {
+                    table = TABLE_SECURE;
+                }
+
+                if (sSystemMovedToGlobalSettings.contains(name)) {
+                    table = TABLE_GLOBAL;
+                }
+
+                if (sSecureMovedToGlobalSettings.contains(name)) {
+                    table = TABLE_GLOBAL;
+                }
+
+                if (sGlobalMovedToSecureSettings.contains(name)) {
+                    table = TABLE_SECURE;
+                }
             }
-            String name = contentValues.getAsString(Settings.NameValueTable.NAME);
-            if (name == null) {
-                Log.w(TAG, "null name populating settings cache.");
-                return;
-            }
-            String value = contentValues.getAsString(Settings.NameValueTable.VALUE);
-            cache.populate(name, value);
+
+            return table;
+        }
+    }
+
+    final class SettingsRegistry {
+        private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid";
+
+        private static final int SETTINGS_TYPE_GLOBAL = 0;
+        private static final int SETTINGS_TYPE_SYSTEM = 1;
+        private static final int SETTINGS_TYPE_SECURE = 2;
+
+        private static final int SETTINGS_TYPE_MASK = 0xF0000000;
+        private static final int SETTINGS_TYPE_SHIFT = 28;
+
+        private static final String SETTINGS_FILE_GLOBAL = "settings_global.xml";
+        private static final String SETTINGS_FILE_SYSTEM = "settings_system.xml";
+        private static final String SETTINGS_FILE_SECURE = "settings_secure.xml";
+
+        private final SparseArray<SettingsState> mSettingsStates = new SparseArray<>();
+
+        private final BackupManager mBackupManager;
+
+        public SettingsRegistry() {
+            mBackupManager = new BackupManager(getContext());
+            migrateAllLegacySettingsIfNeeded();
         }
 
-        public void populate(String name, String value) {
-            synchronized (this) {
-                if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
-                    put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value));
+        public List<String> getSettingsNamesLocked(int type, int userId) {
+            final int key = makeKey(type, userId);
+            SettingsState settingsState = peekSettingsStateLocked(key);
+            return settingsState.getSettingNamesLocked();
+        }
+
+        public SettingsState getSettingsLocked(int type, int userId) {
+            final int key = makeKey(type, userId);
+            return peekSettingsStateLocked(key);
+        }
+
+        public void ensureSettingsForUserLocked(int userId) {
+            // Migrate the setting for this user if needed.
+            migrateLegacySettingsForUserIfNeededLocked(userId);
+
+            // Ensure global settings loaded if owner.
+            if (userId == UserHandle.USER_OWNER) {
+                final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+                ensureSettingsStateLocked(globalKey);
+            }
+
+            // Ensure secure settings loaded.
+            final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+            ensureSettingsStateLocked(secureKey);
+
+            // Make sure the secure settings have an Android id set.
+            SettingsState secureSettings = getSettingsLocked(SETTINGS_TYPE_SECURE, userId);
+            ensureSecureSettingAndroidIdSetLocked(secureSettings);
+
+            // Ensure system settings loaded.
+            final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+            ensureSettingsStateLocked(systemKey);
+
+            // Upgrade the settings to the latest version.
+            UpgradeController upgrader = new UpgradeController(userId);
+            upgrader.upgradeIfNeededLocked();
+        }
+
+        private void ensureSettingsStateLocked(int key) {
+            if (mSettingsStates.get(key) == null) {
+                final int maxBytesPerPackage = getMaxBytesPerPackageForType(getTypeFromKey(key));
+                SettingsState settingsState = new SettingsState(mLock, getSettingsFile(key), key,
+                        maxBytesPerPackage);
+                mSettingsStates.put(key, settingsState);
+            }
+        }
+
+        public void removeUserStateLocked(int userId, boolean permanently) {
+            // We always keep the global settings in memory.
+
+            // Nuke system settings.
+            final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+            final SettingsState systemSettingsState = mSettingsStates.get(systemKey);
+            if (systemSettingsState != null) {
+                if (permanently) {
+                    mSettingsStates.remove(systemKey);
+                    systemSettingsState.destroyLocked(null);
                 } else {
-                    put(name, TOO_LARGE_TO_CACHE_MARKER);
+                    systemSettingsState.destroyLocked(new Runnable() {
+                        @Override
+                        public void run() {
+                            mSettingsStates.remove(systemKey);
+                        }
+                    });
+                }
+            }
+
+            // Nuke secure settings.
+            final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+            final SettingsState secureSettingsState = mSettingsStates.get(secureKey);
+            if (secureSettingsState != null) {
+                if (permanently) {
+                    mSettingsStates.remove(secureKey);
+                    secureSettingsState.destroyLocked(null);
+                } else {
+                    secureSettingsState.destroyLocked(new Runnable() {
+                        @Override
+                        public void run() {
+                            mSettingsStates.remove(secureKey);
+                        }
+                    });
                 }
             }
         }
 
-        /**
-         * For suppressing duplicate/redundant settings inserts early,
-         * checking our cache first (but without faulting it in),
-         * before going to sqlite with the mutation.
-         */
-        public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) {
-            if (cache == null) return false;
-            synchronized (cache) {
-                Bundle bundle = cache.get(name);
-                if (bundle == null) return false;
-                String oldValue = bundle.getPairValue();
-                if (oldValue == null && value == null) return true;
-                if ((oldValue == null) != (value == null)) return false;
-                return oldValue.equals(value);
+        public boolean insertSettingLocked(int type, int userId, String name, String value,
+                String packageName) {
+            final int key = makeKey(type, userId);
+
+            SettingsState settingsState = peekSettingsStateLocked(key);
+            final boolean success = settingsState.insertSettingLocked(name, value, packageName);
+
+            if (success) {
+                notifyForSettingsChange(key, name);
+            }
+            return success;
+        }
+
+        public boolean deleteSettingLocked(int type, int userId, String name) {
+            final int key = makeKey(type, userId);
+
+            SettingsState settingsState = peekSettingsStateLocked(key);
+            final boolean success = settingsState.deleteSettingLocked(name);
+
+            if (success) {
+                notifyForSettingsChange(key, name);
+            }
+            return success;
+        }
+
+        public Setting getSettingLocked(int type, int userId, String name) {
+            final int key = makeKey(type, userId);
+
+            SettingsState settingsState = peekSettingsStateLocked(key);
+            return settingsState.getSettingLocked(name);
+        }
+
+        public boolean updateSettingLocked(int type, int userId, String name, String value,
+                String packageName) {
+            final int key = makeKey(type, userId);
+
+            SettingsState settingsState = peekSettingsStateLocked(key);
+            final boolean success = settingsState.updateSettingLocked(name, value, packageName);
+
+            if (success) {
+                notifyForSettingsChange(key, name);
+            }
+
+            return success;
+        }
+
+        public void onPackageRemovedLocked(String packageName, int userId) {
+            final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+            SettingsState globalSettings = mSettingsStates.get(globalKey);
+            globalSettings.onPackageRemovedLocked(packageName);
+
+            final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+            SettingsState secureSettings = mSettingsStates.get(secureKey);
+            secureSettings.onPackageRemovedLocked(packageName);
+
+            final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+            SettingsState systemSettings = mSettingsStates.get(systemKey);
+            systemSettings.onPackageRemovedLocked(packageName);
+        }
+
+        private SettingsState peekSettingsStateLocked(int key) {
+            SettingsState settingsState = mSettingsStates.get(key);
+            if (settingsState != null) {
+                return settingsState;
+            }
+
+            ensureSettingsForUserLocked(getUserIdFromKey(key));
+            return mSettingsStates.get(key);
+        }
+
+        private void migrateAllLegacySettingsIfNeeded() {
+            synchronized (mLock) {
+                final int key = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+                File globalFile = getSettingsFile(key);
+                if (globalFile.exists()) {
+                    return;
+                }
+
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    List<UserInfo> users = mUserManager.getUsers(true);
+
+                    final int userCount = users.size();
+                    for (int i = 0; i < userCount; i++) {
+                        final int userId = users.get(i).id;
+
+                        DatabaseHelper dbHelper = new DatabaseHelper(getContext(), userId);
+                        SQLiteDatabase database = dbHelper.getWritableDatabase();
+                        migrateLegacySettingsForUserLocked(dbHelper, database, userId);
+
+                        // Upgrade to the latest version.
+                        UpgradeController upgrader = new UpgradeController(userId);
+                        upgrader.upgradeIfNeededLocked();
+
+                        // Drop from memory if not a running user.
+                        if (!mUserManager.isUserRunning(new UserHandle(userId))) {
+                            removeUserStateLocked(userId, false);
+                        }
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+        }
+
+        private void migrateLegacySettingsForUserIfNeededLocked(int userId) {
+            // Every user has secure settings and if no file we need to migrate.
+            final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+            File secureFile = getSettingsFile(secureKey);
+            if (secureFile.exists()) {
+                return;
+            }
+
+            DatabaseHelper dbHelper = new DatabaseHelper(getContext(), userId);
+            SQLiteDatabase database = dbHelper.getWritableDatabase();
+
+            migrateLegacySettingsForUserLocked(dbHelper, database, userId);
+        }
+
+        private void migrateLegacySettingsForUserLocked(DatabaseHelper dbHelper,
+                SQLiteDatabase database, int userId) {
+            // Move over the global settings if owner.
+            if (userId == UserHandle.USER_OWNER) {
+                final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, userId);
+                ensureSettingsStateLocked(globalKey);
+                SettingsState globalSettings = mSettingsStates.get(globalKey);
+                migrateLegacySettingsLocked(globalSettings, database, TABLE_GLOBAL);
+                globalSettings.persistSyncLocked();
+            }
+
+            // Move over the secure settings.
+            final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+            ensureSettingsStateLocked(secureKey);
+            SettingsState secureSettings = mSettingsStates.get(secureKey);
+            migrateLegacySettingsLocked(secureSettings, database, TABLE_SECURE);
+            ensureSecureSettingAndroidIdSetLocked(secureSettings);
+            secureSettings.persistSyncLocked();
+
+            // Move over the system settings.
+            final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+            ensureSettingsStateLocked(systemKey);
+            SettingsState systemSettings = mSettingsStates.get(systemKey);
+            migrateLegacySettingsLocked(systemSettings, database, TABLE_SYSTEM);
+            systemSettings.persistSyncLocked();
+
+            // Drop the database as now all is moved and persisted.
+            if (DROP_DATABASE_ON_MIGRATION) {
+                dbHelper.dropDatabase();
+            } else {
+                dbHelper.backupDatabase();
+            }
+        }
+
+        private void migrateLegacySettingsLocked(SettingsState settingsState,
+                SQLiteDatabase database, String table) {
+            SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+            queryBuilder.setTables(table);
+
+            Cursor cursor = queryBuilder.query(database, ALL_COLUMNS,
+                    null, null, null, null, null);
+
+            if (cursor == null) {
+                return;
+            }
+
+            try {
+                if (!cursor.moveToFirst()) {
+                    return;
+                }
+
+                final int nameColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.NAME);
+                final int valueColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.VALUE);
+
+                settingsState.setVersionLocked(database.getVersion());
+
+                while (!cursor.isAfterLast()) {
+                    String name = cursor.getString(nameColumnIdx);
+                    String value = cursor.getString(valueColumnIdx);
+                    settingsState.insertSettingLocked(name, value,
+                            SettingsState.SYSTEM_PACKAGE_NAME);
+                    cursor.moveToNext();
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        private void ensureSecureSettingAndroidIdSetLocked(SettingsState secureSettings) {
+            Setting value = secureSettings.getSettingLocked(Settings.Secure.ANDROID_ID);
+
+            if (value != null) {
+                return;
+            }
+
+            final int userId = getUserIdFromKey(secureSettings.mKey);
+
+            final UserInfo user;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                user = mUserManager.getUserInfo(userId);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+            if (user == null) {
+                // Can happen due to races when deleting users - treat as benign.
+                return;
+            }
+
+            String androidId = Long.toHexString(new SecureRandom().nextLong());
+            secureSettings.insertSettingLocked(Settings.Secure.ANDROID_ID, androidId,
+                    SettingsState.SYSTEM_PACKAGE_NAME);
+
+            Slog.d(LOG_TAG, "Generated and saved new ANDROID_ID [" + androidId
+                    + "] for user " + userId);
+
+            // Write a drop box entry if it's a restricted profile
+            if (user.isRestricted()) {
+                DropBoxManager dbm = (DropBoxManager) getContext().getSystemService(
+                        Context.DROPBOX_SERVICE);
+                if (dbm != null && dbm.isTagEnabled(DROPBOX_TAG_USERLOG)) {
+                    dbm.addText(DROPBOX_TAG_USERLOG, System.currentTimeMillis()
+                            + "," + DROPBOX_TAG_USERLOG + "," + androidId + "\n");
+                }
+            }
+        }
+
+        private void notifyForSettingsChange(int key, String name) {
+            // Update the system property *first*, so if someone is listening for
+            // a notification and then using the contract class to get their data,
+            // the system property will be updated and they'll get the new data.
+
+            boolean backedUpDataChanged = false;
+            String property = null;
+            if (isGlobalSettingsKey(key)) {
+                property = Settings.Global.SYS_PROP_SETTING_VERSION;
+                backedUpDataChanged = true;
+            } else if (isSecureSettingsKey(key)) {
+                property = Settings.Secure.SYS_PROP_SETTING_VERSION;
+                backedUpDataChanged = true;
+            } else if (isSystemSettingsKey(key)) {
+                property = Settings.System.SYS_PROP_SETTING_VERSION;
+                backedUpDataChanged = true;
+            }
+
+            if (property != null) {
+                final long version = SystemProperties.getLong(property, 0) + 1;
+                SystemProperties.set(property, Long.toString(version));
+                if (DEBUG) {
+                    Slog.v(LOG_TAG, "System property " + property + "=" + version);
+                }
+            }
+
+            // Inform the backup manager about a data change
+            if (backedUpDataChanged) {
+                mBackupManager.dataChanged();
+            }
+
+            // Now send the notification through the content framework.
+
+            final int userId = getUserIdFromKey(key);
+            Uri uri = getNotificationUriFor(key, name);
+
+            sendNotify(uri, userId);
+        }
+
+        private int makeKey(int type, int userId) {
+            return (type << SETTINGS_TYPE_SHIFT) | userId;
+        }
+
+        private int getTypeFromKey(int key) {
+            return key >> SETTINGS_TYPE_SHIFT;
+        }
+
+        private int getUserIdFromKey(int key) {
+            return key & ~SETTINGS_TYPE_MASK;
+        }
+
+        private boolean isGlobalSettingsKey(int key) {
+            return getTypeFromKey(key) == SETTINGS_TYPE_GLOBAL;
+        }
+
+        private boolean isSystemSettingsKey(int key) {
+            return getTypeFromKey(key) == SETTINGS_TYPE_SYSTEM;
+        }
+
+        private boolean isSecureSettingsKey(int key) {
+            return getTypeFromKey(key) == SETTINGS_TYPE_SECURE;
+        }
+
+        private File getSettingsFile(int key) {
+            if (isGlobalSettingsKey(key)) {
+                final int userId = getUserIdFromKey(key);
+                return new File(Environment.getUserSystemDirectory(userId),
+                        SETTINGS_FILE_GLOBAL);
+            } else if (isSystemSettingsKey(key)) {
+                final int userId = getUserIdFromKey(key);
+                return new File(Environment.getUserSystemDirectory(userId),
+                        SETTINGS_FILE_SYSTEM);
+            } else if (isSecureSettingsKey(key)) {
+                final int userId = getUserIdFromKey(key);
+                return new File(Environment.getUserSystemDirectory(userId),
+                        SETTINGS_FILE_SECURE);
+            } else {
+                throw new IllegalArgumentException("Invalid settings key:" + key);
+            }
+        }
+
+        private Uri getNotificationUriFor(int key, String name) {
+            if (isGlobalSettingsKey(key)) {
+                return (name != null) ? Uri.withAppendedPath(Settings.Global.CONTENT_URI, name)
+                        : Settings.Global.CONTENT_URI;
+            } else if (isSecureSettingsKey(key)) {
+                return (name != null) ? Uri.withAppendedPath(Settings.Secure.CONTENT_URI, name)
+                        : Settings.Secure.CONTENT_URI;
+            } else if (isSystemSettingsKey(key)) {
+                return (name != null) ? Uri.withAppendedPath(Settings.System.CONTENT_URI, name)
+                        : Settings.System.CONTENT_URI;
+            } else {
+                throw new IllegalArgumentException("Invalid settings key:" + key);
+            }
+        }
+
+        private int getMaxBytesPerPackageForType(int type) {
+            switch (type) {
+                case SETTINGS_TYPE_GLOBAL:
+                case SETTINGS_TYPE_SECURE: {
+                    return SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED;
+                }
+
+                default: {
+                    return SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED;
+                }
+            }
+        }
+
+        private final class UpgradeController {
+            private static final int SETTINGS_VERSION = 118;
+
+            private final int mUserId;
+
+            public UpgradeController(int userId) {
+                mUserId = userId;
+            }
+
+            public void upgradeIfNeededLocked() {
+                // The version of all settings for a user is the same (all users have secure).
+                SettingsState secureSettings = getSettingsLocked(
+                        SettingsRegistry.SETTINGS_TYPE_SECURE, mUserId);
+
+                // Try an update from the current state.
+                final int oldVersion = secureSettings.getVersionLocked();
+                final int newVersion = SETTINGS_VERSION;
+
+                // If up do data - done.
+                if (oldVersion == newVersion) {
+                    return;
+                }
+
+                // Try to upgrade.
+                final int curVersion = onUpgradeLocked(mUserId, oldVersion, newVersion);
+
+                // If upgrade failed start from scratch and upgrade.
+                if (curVersion != newVersion) {
+                    // Drop state we have for this user.
+                    removeUserStateLocked(mUserId, true);
+
+                    // Recreate the database.
+                    DatabaseHelper dbHelper = new DatabaseHelper(getContext(), mUserId);
+                    SQLiteDatabase database = dbHelper.getWritableDatabase();
+                    dbHelper.recreateDatabase(database, newVersion, curVersion, oldVersion);
+
+                    // Migrate the settings for this user.
+                    migrateLegacySettingsForUserLocked(dbHelper, database, mUserId);
+
+                    // Now upgrade should work fine.
+                    onUpgradeLocked(mUserId, oldVersion, newVersion);
+                }
+
+                // Set the global settings version if owner.
+                if (mUserId == UserHandle.USER_OWNER) {
+                    SettingsState globalSettings = getSettingsLocked(
+                            SettingsRegistry.SETTINGS_TYPE_GLOBAL, mUserId);
+                    globalSettings.setVersionLocked(newVersion);
+                }
+
+                // Set the secure settings version.
+                secureSettings.setVersionLocked(newVersion);
+
+                // Set the system settings version.
+                SettingsState systemSettings = getSettingsLocked(
+                        SettingsRegistry.SETTINGS_TYPE_SYSTEM, mUserId);
+                systemSettings.setVersionLocked(newVersion);
+            }
+
+            private SettingsState getGlobalSettingsLocked() {
+                return getSettingsLocked(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+            }
+
+            private SettingsState getSecureSettingsLocked(int userId) {
+                return getSettingsLocked(SETTINGS_TYPE_SECURE, userId);
+            }
+
+            private SettingsState getSystemSettingsLocked(int userId) {
+                return getSettingsLocked(SETTINGS_TYPE_SYSTEM, userId);
+            }
+
+            private int onUpgradeLocked(int userId, int oldVersion, int newVersion) {
+                if (DEBUG) {
+                    Slog.w(LOG_TAG, "Upgrading settings for user: " + userId + " from version: "
+                            + oldVersion + " to version: " + newVersion);
+                }
+
+                // You must perform all necessary mutations to bring the settings
+                // for this user from the old to the new version. When you add a new
+                // upgrade step you *must* update SETTINGS_VERSION.
+
+                /**
+                 * This is an example of moving a setting from secure to global.
+                 *
+                 * int currentVersion = oldVersion;
+                 * if (currentVersion == 118) {
+                 *     // Remove from the secure settings.
+                 *     SettingsState secureSettings = getSecureSettingsLocked(userId);
+                 *     String name = "example_setting_to_move";
+                 *     String value = secureSettings.getSetting(name);
+                 *     secureSettings.deleteSetting(name);
+                 *
+                 *     // Add to the global settings.
+                 *     SettingsState globalSettings = getGlobalSettingsLocked();
+                 *     globalSettings.insertSetting(name, value, SettingsState.SYSTEM_PACKAGE_NAME);
+                 *
+                 *     // Update the current version.
+                 *     currentVersion = 119;
+                 * }
+                 *
+                 * // Return the current version.
+                 * return currentVersion;
+                 */
+
+                return SettingsState.VERSION_UNDEFINED;
             }
         }
     }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
new file mode 100644
index 0000000..e63d220
--- /dev/null
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.settings;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.Xml;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BackgroundThread;
+import libcore.io.IoUtils;
+import libcore.util.Objects;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class contains the state for one type of settings. It is responsible
+ * for saving the state asynchronously to an XML file after a mutation and
+ * loading the from an XML file on construction.
+ * <p>
+ * This class uses the same lock as the settings provider to ensure that
+ * multiple changes made by the settings provider, e,g, upgrade, bulk insert,
+ * etc, are atomically persisted since the asynchronous persistence is using
+ * the same lock to grab the current state to write to disk.
+ * </p>
+ */
+final class SettingsState {
+    private static final boolean DEBUG = false;
+    private static final boolean DEBUG_PERSISTENCE = false;
+
+    private static final String LOG_TAG = "SettingsState";
+
+    private static final long WRITE_SETTINGS_DELAY_MILLIS = 200;
+    private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000;
+
+    public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1;
+    public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000;
+
+    public static final String SYSTEM_PACKAGE_NAME = "android";
+
+    public static final int VERSION_UNDEFINED = -1;
+
+    private static final String TAG_SETTINGS = "settings";
+    private static final String TAG_SETTING = "setting";
+    private static final String ATTR_PACKAGE = "package";
+
+    private static final String ATTR_VERSION = "version";
+    private static final String ATTR_ID = "id";
+    private static final String ATTR_NAME = "name";
+    private static final String ATTR_VALUE = "value";
+
+    private static final String NULL_VALUE = "null";
+
+    private final Object mLock;
+
+    private final Handler mHandler = new MyHandler();
+
+    @GuardedBy("mLock")
+    private final ArrayMap<String, Setting> mSettings = new ArrayMap<>();
+
+    @GuardedBy("mLock")
+    private final ArrayMap<String, Integer> mPackageToMemoryUsage;
+
+    @GuardedBy("mLock")
+    private final int mMaxBytesPerAppPackage;
+
+    @GuardedBy("mLock")
+    private final File mStatePersistFile;
+
+    public final int mKey;
+
+    @GuardedBy("mLock")
+    private int mVersion = VERSION_UNDEFINED;
+
+    @GuardedBy("mLock")
+    private long mLastNotWrittenMutationTimeMillis;
+
+    @GuardedBy("mLock")
+    private boolean mDirty;
+
+    @GuardedBy("mLock")
+    private boolean mWriteScheduled;
+
+    public SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage) {
+        // It is important that we use the same lock as the settings provider
+        // to ensure multiple mutations on this state are atomicaly persisted
+        // as the async persistence should be blocked while we make changes.
+        mLock = lock;
+        mStatePersistFile = file;
+        mKey = key;
+        if (maxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_LIMITED) {
+            mMaxBytesPerAppPackage = maxBytesPerAppPackage;
+            mPackageToMemoryUsage = new ArrayMap<>();
+        } else {
+            mMaxBytesPerAppPackage = maxBytesPerAppPackage;
+            mPackageToMemoryUsage = null;
+        }
+        synchronized (mLock) {
+            readStateSyncLocked();
+        }
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public int getVersionLocked() {
+        return mVersion;
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public void setVersionLocked(int version) {
+        if (version == mVersion) {
+            return;
+        }
+        mVersion = version;
+
+        scheduleWriteIfNeededLocked();
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public void onPackageRemovedLocked(String packageName) {
+        boolean removedSomething = false;
+
+        final int settingCount = mSettings.size();
+        for (int i = settingCount - 1; i >= 0; i--) {
+            String name = mSettings.keyAt(i);
+            // Settings defined by use are never dropped.
+            if (Settings.System.PUBLIC_SETTINGS.contains(name)
+                    || Settings.System.PRIVATE_SETTINGS.contains(name)) {
+                continue;
+            }
+            Setting setting = mSettings.valueAt(i);
+            if (packageName.equals(setting.packageName)) {
+                mSettings.removeAt(i);
+                removedSomething = true;
+            }
+        }
+
+        if (removedSomething) {
+            scheduleWriteIfNeededLocked();
+        }
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public List<String> getSettingNamesLocked() {
+        ArrayList<String> names = new ArrayList<>();
+        final int settingsCount = mSettings.size();
+        for (int i = 0; i < settingsCount; i++) {
+            String name = mSettings.keyAt(i);
+            names.add(name);
+        }
+        return names;
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public Setting getSettingLocked(String name) {
+        if (TextUtils.isEmpty(name)) {
+            return null;
+        }
+        return mSettings.get(name);
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public boolean updateSettingLocked(String name, String value, String packageName) {
+        if (!hasSettingLocked(name)) {
+            return false;
+        }
+
+        return insertSettingLocked(name, value, packageName);
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public boolean insertSettingLocked(String name, String value, String packageName) {
+        if (TextUtils.isEmpty(name)) {
+            return false;
+        }
+
+        Setting oldState = mSettings.get(name);
+        String oldValue = (oldState != null) ? oldState.value : null;
+
+        if (oldState != null) {
+            if (!oldState.update(value, packageName)) {
+                return false;
+            }
+        } else {
+            Setting state = new Setting(name, value, packageName);
+            mSettings.put(name, state);
+        }
+
+        updateMemoryUsagePerPackageLocked(packageName, oldValue, value);
+
+        scheduleWriteIfNeededLocked();
+
+        return true;
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public void persistSyncLocked() {
+        mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
+        doWriteState();
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public boolean deleteSettingLocked(String name) {
+        if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) {
+            return false;
+        }
+
+        Setting oldState = mSettings.remove(name);
+
+        updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, null);
+
+        scheduleWriteIfNeededLocked();
+
+        return true;
+    }
+
+    // The settings provider must hold its lock when calling here.
+    public void destroyLocked(Runnable callback) {
+        mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
+        if (callback != null) {
+            if (mDirty) {
+                // Do it without a delay.
+                mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS,
+                        callback).sendToTarget();
+                return;
+            }
+            callback.run();
+        }
+    }
+
+    private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue,
+            String newValue) {
+        if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) {
+            return;
+        }
+
+        if (SYSTEM_PACKAGE_NAME.equals(packageName)) {
+            return;
+        }
+
+        final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
+        final int newValueSize = (newValue != null) ? newValue.length() : 0;
+        final int deltaSize = newValueSize - oldValueSize;
+
+        Integer currentSize = mPackageToMemoryUsage.get(packageName);
+        final int newSize = Math.max((currentSize != null)
+                ? currentSize + deltaSize : deltaSize, 0);
+
+        if (newSize > mMaxBytesPerAppPackage) {
+            throw new IllegalStateException("You are adding too many system settings. "
+                    + "You should stop using system settings for app specific data.");
+        }
+
+        if (DEBUG) {
+            Slog.i(LOG_TAG, "Settings for package: " + packageName
+                    + " size: " + newSize + " bytes.");
+        }
+
+        mPackageToMemoryUsage.put(packageName, newSize);
+    }
+
+    private boolean hasSettingLocked(String name) {
+        return mSettings.indexOfKey(name) >= 0;
+    }
+
+    private void scheduleWriteIfNeededLocked() {
+        // If dirty then we have a write already scheduled.
+        if (!mDirty) {
+            mDirty = true;
+            writeStateAsyncLocked();
+        }
+    }
+
+    private void writeStateAsyncLocked() {
+        final long currentTimeMillis = SystemClock.uptimeMillis();
+
+        if (mWriteScheduled) {
+            mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
+
+            // If enough time passed, write without holding off anymore.
+            final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis
+                    - mLastNotWrittenMutationTimeMillis;
+            if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) {
+                mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget();
+                return;
+            }
+
+            // Hold off a bit more as settings are frequently changing.
+            final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis
+                    + MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0);
+            final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis);
+
+            Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
+            mHandler.sendMessageDelayed(message, writeDelayMillis);
+        } else {
+            mLastNotWrittenMutationTimeMillis = currentTimeMillis;
+            Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
+            mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS);
+            mWriteScheduled = true;
+        }
+    }
+
+    private void doWriteState() {
+        if (DEBUG_PERSISTENCE) {
+            Slog.i(LOG_TAG, "[PERSIST START]");
+        }
+
+        AtomicFile destination = new AtomicFile(mStatePersistFile);
+
+        final int version;
+        final ArrayMap<String, Setting> settings;
+
+        synchronized (mLock) {
+            version = mVersion;
+            settings = new ArrayMap<>(mSettings);
+            mDirty = false;
+            mWriteScheduled = false;
+        }
+
+        FileOutputStream out = null;
+        try {
+            out = destination.startWrite();
+
+            XmlSerializer serializer = Xml.newSerializer();
+            serializer.setOutput(out, "utf-8");
+            serializer.startDocument(null, true);
+            serializer.startTag(null, TAG_SETTINGS);
+            serializer.attribute(null, ATTR_VERSION, String.valueOf(version));
+
+            final int settingCount = settings.size();
+            for (int i = 0; i < settingCount; i++) {
+                Setting setting = settings.valueAt(i);
+
+                serializer.startTag(null, TAG_SETTING);
+                serializer.attribute(null, ATTR_ID, setting.getId());
+                serializer.attribute(null, ATTR_NAME, setting.getName());
+                serializer.attribute(null, ATTR_VALUE, packValue(setting.getValue()));
+                serializer.attribute(null, ATTR_PACKAGE, packValue(setting.getPackageName()));
+                serializer.endTag(null, TAG_SETTING);
+
+                if (DEBUG_PERSISTENCE) {
+                    Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue());
+                }
+            }
+
+            serializer.endTag(null, TAG_SETTINGS);
+            serializer.endDocument();
+            destination.finishWrite(out);
+
+            if (DEBUG_PERSISTENCE) {
+                Slog.i(LOG_TAG, "[PERSIST END]");
+            }
+
+        } catch (IOException e) {
+            Slog.w(LOG_TAG, "Failed to write settings, restoring backup", e);
+            destination.failWrite(out);
+        } finally {
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private void readStateSyncLocked() {
+        FileInputStream in;
+        if (!mStatePersistFile.exists()) {
+            return;
+        }
+        try {
+            in = new FileInputStream(mStatePersistFile);
+        } catch (FileNotFoundException fnfe) {
+            Slog.i(LOG_TAG, "No settings state");
+            return;
+        }
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(in, null);
+            parseStateLocked(parser);
+        } catch (XmlPullParserException | IOException ise) {
+            throw new IllegalStateException("Failed parsing settings file: "
+                    + mStatePersistFile , ise);
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    private void parseStateLocked(XmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        parser.next();
+        skipEmptyTextTags(parser);
+        expect(parser, XmlPullParser.START_TAG, TAG_SETTINGS);
+
+        mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION));
+
+        parser.next();
+
+        while (parseSettingLocked(parser)) {
+            parser.next();
+        }
+
+        skipEmptyTextTags(parser);
+        expect(parser, XmlPullParser.END_TAG, TAG_SETTINGS);
+    }
+
+    private boolean parseSettingLocked(XmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        skipEmptyTextTags(parser);
+        if (!accept(parser, XmlPullParser.START_TAG, TAG_SETTING)) {
+            return false;
+        }
+
+        String id = parser.getAttributeValue(null, ATTR_ID);
+        String name = parser.getAttributeValue(null, ATTR_NAME);
+        String value = parser.getAttributeValue(null, ATTR_VALUE);
+        String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
+        mSettings.put(name, new Setting(name, unpackValue(value),
+                unpackValue(packageName), id));
+
+        if (DEBUG_PERSISTENCE) {
+            Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
+        }
+
+        parser.next();
+
+        skipEmptyTextTags(parser);
+        expect(parser, XmlPullParser.END_TAG, TAG_SETTING);
+
+        return true;
+    }
+
+    private void expect(XmlPullParser parser, int type, String tag)
+            throws IOException, XmlPullParserException {
+        if (!accept(parser, type, tag)) {
+            throw new XmlPullParserException("Expected event: " + type
+                    + " and tag: " + tag + " but got event: " + parser.getEventType()
+                    + " and tag:" + parser.getName());
+        }
+    }
+
+    private void skipEmptyTextTags(XmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        while (accept(parser, XmlPullParser.TEXT, null)
+                && "\n".equals(parser.getText())) {
+            parser.next();
+        }
+    }
+
+    private boolean accept(XmlPullParser parser, int type, String tag)
+            throws IOException, XmlPullParserException {
+        if (parser.getEventType() != type) {
+            return false;
+        }
+        if (tag != null) {
+            if (!tag.equals(parser.getName())) {
+                return false;
+            }
+        } else if (parser.getName() != null) {
+            return false;
+        }
+        return true;
+    }
+
+    private final class MyHandler extends Handler {
+        public static final int MSG_PERSIST_SETTINGS = 1;
+
+        public MyHandler() {
+            super(BackgroundThread.getHandler().getLooper());
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_PERSIST_SETTINGS: {
+                    Runnable callback = (Runnable) message.obj;
+                    doWriteState();
+                    if (callback != null) {
+                        callback.run();
+                    }
+                }
+                break;
+            }
+        }
+    }
+
+    private static String packValue(String value) {
+        if (value == null) {
+            return NULL_VALUE;
+        }
+        return value;
+    }
+
+    private static String unpackValue(String value) {
+        if (NULL_VALUE.equals(value)) {
+            return null;
+        }
+        return value;
+    }
+
+    public static final class Setting {
+        private static long sNextId;
+
+        private String name;
+        private String value;
+        private String packageName;
+        private String id;
+
+        public Setting(String name, String value, String packageName) {
+            init(name, value, packageName, String.valueOf(sNextId++));
+        }
+
+        public Setting(String name, String value, String packageName, String id) {
+            sNextId = Math.max(sNextId, Long.valueOf(id));
+            init(name, value, packageName, String.valueOf(sNextId));
+        }
+
+        private void init(String name, String value, String packageName, String id) {
+            this.name = name;
+            this.value = value;
+            this.packageName = packageName;
+            this.id = id;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public String getPackageName() {
+            return packageName;
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public boolean update(String value, String packageName) {
+            if (Objects.equal(value, this.value)) {
+                return false;
+            }
+            this.value = value;
+            this.packageName = packageName;
+            this.id = String.valueOf(sNextId++);
+            return true;
+        }
+    }
+}
diff --git a/packages/SettingsProvider/test/Android.mk b/packages/SettingsProvider/test/Android.mk
new file mode 100644
index 0000000..01c6ccf
--- /dev/null
+++ b/packages/SettingsProvider/test/Android.mk
@@ -0,0 +1,13 @@
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := SettingsProviderTest
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/packages/SettingsProvider/test/AndroidManifest.xml b/packages/SettingsProvider/test/AndroidManifest.xml
new file mode 100644
index 0000000..7a86b5f
--- /dev/null
+++ b/packages/SettingsProvider/test/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.providers.setting.test">
+
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
+
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+    <uses-permission android:name="android.permission.MANAGE_USERS"/>
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.providers.setting.test"
+        android:label="Settings Provider Tests" />
+</manifest>
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java
new file mode 100644
index 0000000..f713c33
--- /dev/null
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.settings;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+
+import java.util.List;
+
+/**
+ * Base class for the SettingContentProvider tests.
+ */
+abstract class BaseSettingsProviderTest extends AndroidTestCase {
+    protected static final int SETTING_TYPE_GLOBAL = 1;
+    protected static final int SETTING_TYPE_SECURE = 2;
+    protected static final int SETTING_TYPE_SYSTEM = 3;
+
+    protected static final String FAKE_SETTING_NAME = "fake_setting_name";
+    protected static final String FAKE_SETTING_NAME_1 = "fake_setting_name1";
+    protected static final String FAKE_SETTING_VALUE = "fake_setting_value";
+    protected static final String FAKE_SETTING_VALUE_1 = "fake_setting_value_1";
+
+    private static final String[] NAME_VALUE_COLUMNS = new String[] {
+            Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE
+    };
+
+    protected int mSecondaryUserId = UserHandle.USER_OWNER;
+
+    @Override
+    public void setContext(Context context) {
+        super.setContext(context);
+
+        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        List<UserInfo> users = userManager.getUsers();
+        final int userCount = users.size();
+        for (int i = 0; i < userCount; i++) {
+            UserInfo user = users.get(i);
+            if (!user.isPrimary() && !user.isManagedProfile()) {
+                mSecondaryUserId = user.id;
+                break;
+            }
+        }
+    }
+
+    protected void setStringViaFrontEndApiSetting(int type, String name, String value, int userId) {
+        ContentResolver contentResolver = getContext().getContentResolver();
+
+        switch (type) {
+            case SETTING_TYPE_GLOBAL: {
+                Settings.Global.putStringForUser(contentResolver, name, value, userId);
+            } break;
+
+            case SETTING_TYPE_SECURE: {
+                Settings.Secure.putStringForUser(contentResolver, name, value, userId);
+            } break;
+
+            case SETTING_TYPE_SYSTEM: {
+                Settings.System.putStringForUser(contentResolver, name, value, userId);
+            } break;
+
+            default: {
+                throw new IllegalArgumentException("Invalid type: " + type);
+            }
+        }
+    }
+
+    protected String getStringViaFrontEndApiSetting(int type, String name, int userId) {
+        ContentResolver contentResolver = getContext().getContentResolver();
+
+        switch (type) {
+            case SETTING_TYPE_GLOBAL: {
+                return Settings.Global.getStringForUser(contentResolver, name, userId);
+            }
+
+            case SETTING_TYPE_SECURE: {
+                return Settings.Secure.getStringForUser(contentResolver, name, userId);
+            }
+
+            case SETTING_TYPE_SYSTEM: {
+                return Settings.System.getStringForUser(contentResolver, name, userId);
+            }
+
+            default: {
+                throw new IllegalArgumentException("Invalid type: " + type);
+            }
+        }
+    }
+
+    protected Uri insertStringViaProviderApi(int type, String name, String value,
+            boolean withTableRowUri) {
+        Uri uri = getBaseUriForType(type);
+        if (withTableRowUri) {
+            uri = Uri.withAppendedPath(uri, name);
+        }
+        ContentValues values = new ContentValues();
+        values.put(Settings.NameValueTable.NAME, name);
+        values.put(Settings.NameValueTable.VALUE, value);
+
+        return getContext().getContentResolver().insert(uri, values);
+    }
+
+    protected int deleteStringViaProviderApi(int type, String name) {
+        Uri uri = getBaseUriForType(type);
+        return getContext().getContentResolver().delete(uri, "name=?", new String[]{name});
+    }
+
+    protected int updateStringViaProviderApiSetting(int type, String name, String value) {
+        Uri uri = getBaseUriForType(type);
+        ContentValues values = new ContentValues();
+        values.put(Settings.NameValueTable.NAME, name);
+        values.put(Settings.NameValueTable.VALUE, value);
+        return getContext().getContentResolver().update(uri, values, "name=?",
+                new String[]{name});
+    }
+
+    protected String queryStringViaProviderApi(int type, String name) {
+        return queryStringViaProviderApi(type, name, false);
+    }
+
+    protected String queryStringViaProviderApi(int type, String name, boolean queryStringInQuotes) {
+        Uri uri = getBaseUriForType(type);
+
+        String queryString = queryStringInQuotes ? "(name=?)" : "name=?";
+
+        Cursor cursor = getContext().getContentResolver().query(uri, NAME_VALUE_COLUMNS,
+                queryString, new String[]{name}, null);
+
+        if (cursor == null) {
+            return null;
+        }
+
+        try {
+            if (cursor.moveToFirst()) {
+                final int valueColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.VALUE);
+                return cursor.getString(valueColumnIdx);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return null;
+    }
+
+    protected static Uri getBaseUriForType(int type) {
+        switch (type) {
+            case SETTING_TYPE_GLOBAL: {
+                return Settings.Global.CONTENT_URI;
+            }
+
+            case SETTING_TYPE_SECURE: {
+                return Settings.Secure.CONTENT_URI;
+            }
+
+            case SETTING_TYPE_SYSTEM: {
+                return Settings.System.CONTENT_URI;
+            }
+
+            default: {
+                throw new IllegalArgumentException("Invalid type: " + type);
+            }
+        }
+    }
+}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java
new file mode 100644
index 0000000..d581f3b
--- /dev/null
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.settings;
+
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+
+/**
+* Performance tests for the SettingContentProvider.
+*/
+public class SettingsProviderPerformanceTest extends BaseSettingsProviderTest {
+    private static final String LOG_TAG = "SettingsProviderPerformanceTest";
+
+    private static final int ITERATION_COUNT = 100;
+
+    private static final int MICRO_SECONDS_IN_MILLISECOND = 1000;
+
+    private static final long MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS = 20;
+
+    public void testSetAndGetPerformanceForGlobalViaFrontEndApi() throws Exception {
+        // Start with a clean slate.
+        insertStringViaProviderApi(SETTING_TYPE_GLOBAL,
+                FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false);
+
+        final long startTimeMicro = SystemClock.currentTimeMicro();
+
+        try {
+            for (int i = 0; i < ITERATION_COUNT; i++) {
+                // Set the setting to its first value.
+                updateStringViaProviderApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME,
+                        FAKE_SETTING_VALUE);
+
+                // Make sure the setting changed.
+                String firstValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL,
+                        FAKE_SETTING_NAME, UserHandle.USER_OWNER);
+                assertEquals("Setting value didn't change", FAKE_SETTING_VALUE, firstValue);
+
+                // Set the setting to its second value.
+                updateStringViaProviderApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME,
+                        FAKE_SETTING_VALUE_1);
+
+                // Make sure the setting changed.
+                String secondValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL,
+                        FAKE_SETTING_NAME, UserHandle.USER_OWNER);
+                assertEquals("Setting value didn't change", FAKE_SETTING_VALUE_1, secondValue);
+            }
+        } finally {
+            // Clean up.
+            deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME);
+        }
+
+        final long elapsedTimeMicro = SystemClock.currentTimeMicro() - startTimeMicro;
+
+        final long averageTimePerIterationMillis = (long) ((((float) elapsedTimeMicro)
+                / ITERATION_COUNT) / MICRO_SECONDS_IN_MILLISECOND);
+
+        Log.i(LOG_TAG, "Average time to set and get setting via provider APIs: "
+                + averageTimePerIterationMillis + " ms");
+
+        assertTrue("Setting and getting a settings takes too long.", averageTimePerIterationMillis
+                < MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS);
+    }
+
+    public void testSetAndGetPerformanceForGlobalViaProviderApi() throws Exception {
+        // Start with a clean slate.
+        deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME);
+
+        final long startTimeMicro = SystemClock.currentTimeMicro();
+
+        try {
+            for (int i = 0; i < ITERATION_COUNT; i++) {
+                // Set the setting to its first value.
+                setStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME,
+                        FAKE_SETTING_VALUE, UserHandle.USER_OWNER);
+
+                // Make sure the setting changed.
+                String firstValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL,
+                        FAKE_SETTING_NAME, UserHandle.USER_OWNER);
+                assertEquals("Setting value didn't change", FAKE_SETTING_VALUE, firstValue);
+
+                // Set the setting to its second value.
+                setStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME,
+                        FAKE_SETTING_VALUE_1, UserHandle.USER_OWNER);
+
+                // Make sure the setting changed.
+                String secondValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL,
+                        FAKE_SETTING_NAME, UserHandle.USER_OWNER);
+                assertEquals("Setting value didn't change", FAKE_SETTING_VALUE_1, secondValue);
+            }
+        } finally {
+            // Clean up.
+            deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME);
+        }
+
+        final long elapsedTimeMicro = SystemClock.currentTimeMicro() - startTimeMicro;
+
+        final long averageTimePerIterationMillis = (long) ((((float) elapsedTimeMicro)
+                / ITERATION_COUNT) / MICRO_SECONDS_IN_MILLISECOND);
+
+        Log.i(LOG_TAG, "Average time to set and get setting via front-eng APIs: "
+                + averageTimePerIterationMillis + " ms");
+
+        assertTrue("Setting and getting a settings takes too long.", averageTimePerIterationMillis
+                < MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS);
+    }
+}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java
new file mode 100644
index 0000000..cbfcbf5
--- /dev/null
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.settings;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Tests for the SettingContentProvider.
+ *
+ * Before you run this test you must add a secondary user.
+ */
+public class SettingsProviderTest extends BaseSettingsProviderTest {
+    private static final String LOG_TAG = "SettingsProviderTest";
+
+    private static final long WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec
+
+    private static final String[] NAME_VALUE_COLUMNS = new String[]{
+            Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE
+    };
+
+    private final Object mLock = new Object();
+
+    public void testSetAndGetGlobalViaFrontEndApiForOwnerUser() throws Exception {
+        performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_GLOBAL, UserHandle.USER_OWNER);
+    }
+
+    public void testSetAndGetGlobalViaFrontEndApiForNonOwnerUser() throws Exception {
+        if (mSecondaryUserId == UserHandle.USER_OWNER) {
+            Log.w(LOG_TAG, "No secondary user. Skipping "
+                    + "testSetAndGetGlobalViaFrontEndApiForNonOwnerUser");
+            return;
+        }
+        performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_GLOBAL, mSecondaryUserId);
+    }
+
+    public void testSetAndGetSecureViaFrontEndApiForOwnerUser() throws Exception {
+        performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SECURE, UserHandle.USER_OWNER);
+    }
+
+    public void testSetAndGetSecureViaFrontEndApiForNonOwnerUser() throws Exception {
+        if (mSecondaryUserId == UserHandle.USER_OWNER) {
+            Log.w(LOG_TAG, "No secondary user. Skipping "
+                    + "testSetAndGetSecureViaFrontEndApiForNonOwnerUser");
+            return;
+        }
+        performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SECURE, mSecondaryUserId);
+    }
+
+    public void testSetAndGetSystemViaFrontEndApiForOwnerUser() throws Exception {
+        performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SYSTEM, UserHandle.USER_OWNER);
+    }
+
+    public void testSetAndGetSystemViaFrontEndApiForNonOwnerUser() throws Exception {
+        if (mSecondaryUserId == UserHandle.USER_OWNER) {
+            Log.w(LOG_TAG, "No secondary user. Skipping "
+                    + "testSetAndGetSystemViaFrontEndApiForNonOwnerUser");
+            return;
+        }
+        performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SYSTEM, mSecondaryUserId);
+    }
+
+    public void testSetAndGetGlobalViaProviderApi() throws Exception {
+        performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_GLOBAL);
+    }
+
+    public void testSetAndGetSecureViaProviderApi() throws Exception {
+        performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_SECURE);
+    }
+
+    public void testSetAndGetSystemViaProviderApi() throws Exception {
+        performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_SYSTEM);
+    }
+
+    public void testSelectAllGlobalViaProviderApi() throws Exception {
+        setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_GLOBAL,
+                FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false);
+        try {
+            queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(SETTING_TYPE_GLOBAL,
+                    FAKE_SETTING_NAME);
+        } finally {
+            deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME);
+        }
+    }
+
+    public void testSelectAllSecureViaProviderApi() throws Exception {
+        setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_SECURE,
+                FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false);
+        try {
+            queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(SETTING_TYPE_SECURE,
+                    FAKE_SETTING_NAME);
+        } finally {
+            deleteStringViaProviderApi(SETTING_TYPE_SECURE, FAKE_SETTING_NAME);
+        }
+    }
+
+    public void testSelectAllSystemViaProviderApi() throws Exception {
+        setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_SYSTEM,
+                FAKE_SETTING_NAME, FAKE_SETTING_VALUE, true);
+        try {
+            queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(SETTING_TYPE_SYSTEM,
+                    FAKE_SETTING_NAME);
+        } finally {
+            deleteStringViaProviderApi(SETTING_TYPE_SYSTEM, FAKE_SETTING_NAME);
+        }
+    }
+
+    public void testQueryUpdateDeleteGlobalViaProviderApi() throws Exception {
+        doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_GLOBAL);
+    }
+
+    public void testQueryUpdateDeleteSecureViaProviderApi() throws Exception {
+        doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_SECURE);
+    }
+
+    public void testQueryUpdateDeleteSystemViaProviderApi() throws Exception {
+        doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_SYSTEM);
+    }
+
+    public void testBulkInsertGlobalViaProviderApi() throws Exception {
+        toTestBulkInsertViaProviderApiForType(SETTING_TYPE_GLOBAL);
+    }
+
+    public void testBulkInsertSystemViaProviderApi() throws Exception {
+        toTestBulkInsertViaProviderApiForType(SETTING_TYPE_SYSTEM);
+    }
+
+    public void testBulkInsertSecureViaProviderApi() throws Exception {
+        toTestBulkInsertViaProviderApiForType(SETTING_TYPE_SECURE);
+    }
+
+    public void testAppCannotRunsSystemOutOfMemoryWritingSystemSettings() throws Exception {
+        int insertedCount = 0;
+        try {
+            for (; insertedCount < 1200; insertedCount++) {
+                Log.w(LOG_TAG, "Adding app specific setting: " + insertedCount);
+                insertStringViaProviderApi(SETTING_TYPE_SYSTEM,
+                        String.valueOf(insertedCount), FAKE_SETTING_VALUE, false);
+            }
+            fail("Adding app specific settings must be bound.");
+        } catch (Exception e) {
+            for (; insertedCount >= 0; insertedCount--) {
+                Log.w(LOG_TAG, "Removing app specific setting: " + insertedCount);
+                deleteStringViaProviderApi(SETTING_TYPE_SYSTEM,
+                        String.valueOf(insertedCount));
+            }
+        }
+    }
+
+    public void testQueryStringInBracketsGlobalViaProviderApiForType() throws Exception {
+        doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_GLOBAL);
+    }
+
+    public void testQueryStringInBracketsSecureViaProviderApiForType() throws Exception {
+        doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_SECURE);
+    }
+
+    public void testQueryStringInBracketsSystemViaProviderApiForType() throws Exception {
+        doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_SYSTEM);
+    }
+
+    private void doTestQueryStringInBracketsViaProviderApiForType(int type) {
+        // Make sure we have a clean slate.
+        deleteStringViaProviderApi(type, FAKE_SETTING_NAME);
+
+        try {
+            // Insert the setting.
+            final Uri uri = insertStringViaProviderApi(type, FAKE_SETTING_NAME,
+                    FAKE_SETTING_VALUE, false);
+            Uri expectUri = Uri.withAppendedPath(getBaseUriForType(type), FAKE_SETTING_NAME);
+            assertEquals("Did not get expected Uri.", expectUri, uri);
+
+            // Make sure the first setting is there.
+            String firstValue = queryStringViaProviderApi(type, FAKE_SETTING_NAME, true);
+            assertEquals("Setting must be present", FAKE_SETTING_VALUE, firstValue);
+        } finally {
+            // Clean up.
+            deleteStringViaProviderApi(type, FAKE_SETTING_NAME);
+        }
+    }
+
+    private void toTestBulkInsertViaProviderApiForType(int type) {
+        // Make sure we have a clean slate.
+        deleteStringViaProviderApi(type, FAKE_SETTING_NAME);
+        deleteStringViaProviderApi(type, FAKE_SETTING_NAME_1);
+
+        try {
+            Uri uri = getBaseUriForType(type);
+            ContentValues[] allValues = new ContentValues[2];
+
+            // Insert the first setting.
+            ContentValues firstValues = new ContentValues();
+            firstValues.put(Settings.NameValueTable.NAME, FAKE_SETTING_NAME);
+            firstValues.put(Settings.NameValueTable.VALUE, FAKE_SETTING_VALUE);
+            allValues[0] = firstValues;
+
+            // Insert the first setting.
+            ContentValues secondValues = new ContentValues();
+            secondValues.put(Settings.NameValueTable.NAME, FAKE_SETTING_NAME_1);
+            secondValues.put(Settings.NameValueTable.VALUE, FAKE_SETTING_VALUE_1);
+            allValues[1] = secondValues;
+
+            // Verify insertion count.
+            final int insertCount = getContext().getContentResolver().bulkInsert(uri, allValues);
+            assertSame("Couldn't insert both values", 2, insertCount);
+
+            // Make sure the first setting is there.
+            String firstValue = queryStringViaProviderApi(type, FAKE_SETTING_NAME);
+            assertEquals("First setting must be present", FAKE_SETTING_VALUE, firstValue);
+
+            // Make sure the second setting is there.
+            String secondValue = queryStringViaProviderApi(type, FAKE_SETTING_NAME_1);
+            assertEquals("Second setting must be present", FAKE_SETTING_VALUE_1, secondValue);
+        } finally {
+            // Clean up.
+            deleteStringViaProviderApi(type, FAKE_SETTING_NAME);
+            deleteStringViaProviderApi(type, FAKE_SETTING_NAME_1);
+        }
+    }
+
+    private void doTestQueryUpdateDeleteGlobalViaProviderApiForType(int type) throws Exception {
+        // Make sure it is not there.
+        deleteStringViaProviderApi(type, FAKE_SETTING_NAME);
+
+        // Now selection should return nothing.
+        String value = queryStringViaProviderApi(type, FAKE_SETTING_NAME);
+        assertNull("Setting should not be present.", value);
+
+        // Insert the setting.
+        Uri uri = insertStringViaProviderApi(type,
+                FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false);
+        Uri expectUri = Uri.withAppendedPath(getBaseUriForType(type), FAKE_SETTING_NAME);
+        assertEquals("Did not get expected Uri.", expectUri, uri);
+
+        // Now selection should return the setting.
+        value = queryStringViaProviderApi(type, FAKE_SETTING_NAME);
+        assertEquals("Setting should be present.", FAKE_SETTING_VALUE, value);
+
+        // Update the setting.
+        final int changeCount = updateStringViaProviderApiSetting(type,
+                FAKE_SETTING_NAME, FAKE_SETTING_VALUE_1);
+        assertEquals("Did not get expected change count.", 1, changeCount);
+
+        // Now selection should return the new setting.
+        value = queryStringViaProviderApi(type, FAKE_SETTING_NAME);
+        assertEquals("Setting should be present.", FAKE_SETTING_VALUE_1, value);
+
+        // Delete the setting.
+        final int deletedCount = deleteStringViaProviderApi(type,
+                FAKE_SETTING_NAME);
+        assertEquals("Did not get expected deleted count", 1, deletedCount);
+
+        // Now selection should return nothing.
+        value = queryStringViaProviderApi(type, FAKE_SETTING_NAME);
+        assertNull("Setting should not be present.", value);
+    }
+
+    private void performSetAndGetSettingTestViaFrontEndApi(int type, int userId)
+            throws Exception {
+        try {
+            // Change the setting and assert a successful change.
+            setSettingViaFrontEndApiAndAssertSuccessfulChange(type, FAKE_SETTING_NAME,
+                    FAKE_SETTING_VALUE, userId);
+        } finally {
+            // Remove the setting.
+            setStringViaFrontEndApiSetting(type, FAKE_SETTING_NAME, null, userId);
+        }
+    }
+
+    private void performSetAndGetSettingTestViaProviderApi(int type)
+            throws Exception {
+        try {
+            // Change the setting and assert a successful change.
+            setSettingViaProviderApiAndAssertSuccessfulChange(type, FAKE_SETTING_NAME,
+                    FAKE_SETTING_VALUE, true);
+        } finally {
+            // Remove the setting.
+            setSettingViaProviderApiAndAssertSuccessfulChange(type, FAKE_SETTING_NAME, null,
+                    true);
+        }
+    }
+
+    private void setSettingViaFrontEndApiAndAssertSuccessfulChange(final int type,
+            final String name, final String value, final int userId) throws Exception {
+        setSettingAndAssertSuccessfulChange(new Runnable() {
+            @Override
+            public void run() {
+                setStringViaFrontEndApiSetting(type, name, value, userId);
+            }
+        }, type, name, value, userId);
+    }
+
+    private void setSettingViaProviderApiAndAssertSuccessfulChange(final int type,
+            final String name, final String value, final boolean withTableRowUri)
+            throws Exception {
+        setSettingAndAssertSuccessfulChange(new Runnable() {
+            @Override
+            public void run() {
+                insertStringViaProviderApi(type, name, value, withTableRowUri);
+            }
+        }, type, name, value, UserHandle.USER_OWNER);
+    }
+
+    private void setSettingAndAssertSuccessfulChange(Runnable setCommand, final int type,
+            final String name, final String value, final int userId) throws Exception {
+        ContentResolver contentResolver = getContext().getContentResolver();
+
+        final Uri settingUri = getBaseUriForType(type);
+
+        final AtomicBoolean success = new AtomicBoolean();
+
+        ContentObserver contentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+            public void onChange(boolean selfChange, Uri changeUri, int changeId) {
+                Log.i(LOG_TAG, "onChange(" + selfChange + ", " + changeUri + ", " + changeId + ")");
+                assertEquals("Wrong change Uri", changeUri, settingUri);
+                assertEquals("Wrong user id", userId, changeId);
+                String changeValue = getStringViaFrontEndApiSetting(type, name, userId);
+                assertEquals("Wrong setting value", value, changeValue);
+
+                success.set(true);
+
+                synchronized (mLock) {
+                    mLock.notifyAll();
+                }
+            }
+        };
+
+        contentResolver.registerContentObserver(settingUri, false, contentObserver, userId);
+
+        try {
+            setCommand.run();
+
+            final long startTimeMillis = SystemClock.uptimeMillis();
+            synchronized (mLock) {
+                if (success.get()) {
+                    return;
+                }
+                final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
+                if (elapsedTimeMillis > WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS) {
+                    fail("Could not change setting for "
+                            + WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS + " ms");
+                }
+                final long remainingTimeMillis = WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS
+                        - elapsedTimeMillis;
+                try {
+                    mLock.wait(remainingTimeMillis);
+                } catch (InterruptedException ie) {
+                    /* ignore */
+                }
+            }
+        } finally {
+            contentResolver.unregisterContentObserver(contentObserver);
+        }
+    }
+
+    private void queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(int type,
+            String name) {
+        Uri uri = getBaseUriForType(type);
+
+        Cursor cursor = getContext().getContentResolver().query(uri, NAME_VALUE_COLUMNS,
+                null, null, null);
+
+        if (cursor == null || !cursor.moveToFirst()) {
+            fail("Nothing selected");
+        }
+
+        try {
+            final int nameColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.NAME);
+
+            while (cursor.moveToNext()) {
+                String currentName = cursor.getString(nameColumnIdx);
+                if (name.equals(currentName)) {
+                    return;
+                }
+            }
+
+            fail("Not found setting: " + name);
+        } finally {
+            cursor.close();
+        }
+    }
+}