Add CMSettingsProvider and CMDatabaseHelper

issue-id: CYNGNOS-828

Change-Id: I01c08c0e432d6a941950a565e5ab6664664e2a7f
diff --git a/Android.mk b/Android.mk
index 1bebef6..fdc06f2 100644
--- a/Android.mk
+++ b/Android.mk
@@ -163,7 +163,7 @@
 
 LOCAL_DROIDDOC_OPTIONS:= \
         -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/cmsdk_stubs_current_intermediates/src \
-        -stubpackages cyanogenmod.alarmclock:cyanogenmod.app:cyanogenmod.hardware:cyanogenmod.os:cyanogenmod.profiles:cyanogenmod.platform:org.cyanogenmod.platform \
+        -stubpackages cyanogenmod.alarmclock:cyanogenmod.app:cyanogenmod.hardware:cyanogenmod.os:cyanogenmod.profiles:cyanogenmod.providers:cyanogenmod.platform:org.cyanogenmod.platform \
         -api $(INTERNAL_CM_PLATFORM_API_FILE) \
         -removedApi $(INTERNAL_CM_PLATFORM_REMOVED_API_FILE) \
         -nodocs \
@@ -192,7 +192,7 @@
 
 LOCAL_DROIDDOC_OPTIONS:=\
         -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/cmsdk_system_stubs_current_intermediates/src \
-        -stubpackages cyanogenmod.alarmclock:cyanogenmod.app:cyanogenmod.hardware:cyanogenmod.os:cyanogenmod.profiles:cyanogenmod.platform:org.cyanogenmod.platform \
+        -stubpackages cyanogenmod.alarmclock:cyanogenmod.app:cyanogenmod.hardware:cyanogenmod.os:cyanogenmod.profiles:cyanogenmod.providers:cyanogenmod.platform:org.cyanogenmod.platform \
         -showAnnotation android.annotation.SystemApi \
         -api $(INTERNAL_CM_PLATFORM_SYSTEM_API_FILE) \
         -removedApi $(INTERNAL_CM_PLATFORM_SYSTEM_REMOVED_API_FILE) \
diff --git a/api/cm_current.txt b/api/cm_current.txt
index 02aaa62..e7e6011 100644
--- a/api/cm_current.txt
+++ b/api/cm_current.txt
@@ -442,6 +442,8 @@
     field public static final java.lang.String MODIFY_SOUND_SETTINGS = "cyanogenmod.permission.MODIFY_SOUND_SETTINGS";
     field public static final java.lang.String PUBLISH_CUSTOM_TILE = "cyanogenmod.permission.PUBLISH_CUSTOM_TILE";
     field public static final java.lang.String READ_MSIM_PHONE_STATE = "cyanogenmod.permission.READ_MSIM_PHONE_STATE";
+    field public static final java.lang.String WRITE_SECURE_SETTINGS = "cyanogenmod.permission.WRITE_SECURE_SETTINGS";
+    field public static final java.lang.String WRITE_SETTINGS = "cyanogenmod.permission.WRITE_SETTINGS";
   }
 
   public final class R {
@@ -566,3 +568,68 @@
 
 }
 
+package cyanogenmod.providers {
+
+  public final class CMSettings {
+    ctor public CMSettings();
+    field public static final java.lang.String AUTHORITY = "cmsettings";
+  }
+
+  public static class CMSettings.CMSettingNotFoundException extends android.util.AndroidException {
+    ctor public CMSettings.CMSettingNotFoundException(java.lang.String);
+  }
+
+  public static final class CMSettings.Global extends android.provider.Settings.NameValueTable {
+    ctor public CMSettings.Global();
+    method public static float getFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static float getFloat(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static int getInt(android.content.ContentResolver, java.lang.String, int);
+    method public static int getInt(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static long getLong(android.content.ContentResolver, java.lang.String, long);
+    method public static long getLong(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static java.lang.String getString(android.content.ContentResolver, java.lang.String);
+    method public static boolean putFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
+    method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
+    method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+    field public static final android.net.Uri CONTENT_URI;
+    field public static final java.lang.String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_global_version";
+  }
+
+  public static final class CMSettings.Secure extends android.provider.Settings.NameValueTable {
+    ctor public CMSettings.Secure();
+    method public static float getFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static float getFloat(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static int getInt(android.content.ContentResolver, java.lang.String, int);
+    method public static int getInt(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static long getLong(android.content.ContentResolver, java.lang.String, long);
+    method public static long getLong(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static java.lang.String getString(android.content.ContentResolver, java.lang.String);
+    method public static boolean putFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
+    method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
+    method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+    field public static final android.net.Uri CONTENT_URI;
+    field public static final java.lang.String NAME_THEME_CONFIG = "name_theme_config";
+    field public static final java.lang.String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_secure_version";
+  }
+
+  public static final class CMSettings.System extends android.provider.Settings.NameValueTable {
+    ctor public CMSettings.System();
+    method public static float getFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static float getFloat(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static int getInt(android.content.ContentResolver, java.lang.String, int);
+    method public static int getInt(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static long getLong(android.content.ContentResolver, java.lang.String, long);
+    method public static long getLong(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static java.lang.String getString(android.content.ContentResolver, java.lang.String);
+    method public static boolean putFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
+    method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
+    method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+    field public static final android.net.Uri CONTENT_URI;
+    field public static final java.lang.String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_system_version";
+  }
+
+}
+
diff --git a/cm/res/AndroidManifest.xml b/cm/res/AndroidManifest.xml
index e664f54..5817946 100644
--- a/cm/res/AndroidManifest.xml
+++ b/cm/res/AndroidManifest.xml
@@ -69,6 +69,19 @@
                 android:description="@string/permdesc_useHardwareFramework"
                 android:protectionLevel="system|signature" />
 
+    <!-- Allows an application to write to CM system settings -->
+    <permission android:name="cyanogenmod.permission.WRITE_SETTINGS"
+                android:label="@string/permlab_writeSettings"
+                android:description="@string/permdesc_writeSettings"
+                android:protectionLevel="normal" />
+
+    <!-- Allows an application to write to secure CM system settings.
+        <p>Not for use by third-party applications. -->
+    <permission android:name="cyanogenmod.permission.WRITE_SECURE_SETTINGS"
+                android:label="@string/permlab_writeSecureSettings"
+                android:description="@string/permdesc_writeSecureSettings"
+                android:protectionLevel="signature|system|development" />
+
     <application android:process="system"
                  android:persistent="true"
                  android:hasCode="false"
diff --git a/cm/res/res/values/strings.xml b/cm/res/res/values/strings.xml
index 6634214..43e49c6 100644
--- a/cm/res/res/values/strings.xml
+++ b/cm/res/res/values/strings.xml
@@ -42,6 +42,13 @@
     <string name="permlab_useHardwareFramework">use hardware framework</string>
     <string name="permdesc_useHardwareFramework">Allows an app access to the CM hardware framework.</string>
 
+    <!-- Labels for the WRITE_SETTINGS permission -->
+    <string name="permlab_writeSettings">modify CM system settings</string>
+    <string name="permdesc_writeSettings">Allows an app to modify CM system settings.</string>
+
+    <!-- Labels for the WRITE_SECURE_SETTINGS permission -->
+    <string name="permlab_writeSecureSettings">modify CM secure system settings</string>
+    <string name="permdesc_writeSecureSettings">Allows an app to modify CM secure system settings. Not for use by normal apps.</string>
 
     <!-- Label to show for a service that is running because it is observing the user's custom tiles. -->
     <string name="custom_tile_listener_binding_label">Custom tile listener</string>
diff --git a/packages/CMSettingsProvider/Android.mk b/packages/CMSettingsProvider/Android.mk
new file mode 100644
index 0000000..8659c70
--- /dev/null
+++ b/packages/CMSettingsProvider/Android.mk
@@ -0,0 +1,36 @@
+#
+# Copyright (C) 2015 The CyanogenMod 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.
+#
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+src_dir := src
+res_dir := res
+
+LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dir))
+LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
+
+LOCAL_PACKAGE_NAME := CMSettingsProvider
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_JAVA_LIBRARIES := \
+    org.cyanogenmod.platform.sdk
+
+include $(BUILD_PACKAGE)
+
+########################
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/packages/CMSettingsProvider/AndroidManifest.xml b/packages/CMSettingsProvider/AndroidManifest.xml
new file mode 100644
index 0000000..b46fefc
--- /dev/null
+++ b/packages/CMSettingsProvider/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The CyanogenMod 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="org.cyanogenmod.cmsettings"
+          coreApp="true"
+          android:sharedUserId="android.uid.system">
+    <!-- It is necessary to be a system app in order to update table versions in SystemProperties for
+         CMSettings to know whether or not the client side cache is up to date. It is also necessary
+         to run in the system process in order to start the content provider prior to running migration
+         for CM settings on user starting -->
+
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+
+    <application android:icon="@drawable/icon"
+                 android:label="@string/app_name"
+                 android:process="system"
+                 android:killAfterRestore="false"
+                 android:allowClearUserData="false"
+                 android:enabled="true">
+
+        <provider android:name="CMSettingsProvider" android:authorities="cmsettings"
+                  android:multiprocess="false"
+                  android:exported="true"
+                  android:writePermission="cyanogenmod.permission.WRITE_SETTINGS"
+                  android:singleUser="true"
+                  android:initOrder="100" />
+
+    </application>
+</manifest>
diff --git a/packages/CMSettingsProvider/res/drawable/icon.png b/packages/CMSettingsProvider/res/drawable/icon.png
new file mode 100644
index 0000000..08ee50d
--- /dev/null
+++ b/packages/CMSettingsProvider/res/drawable/icon.png
Binary files differ
diff --git a/packages/CMSettingsProvider/res/values/strings.xml b/packages/CMSettingsProvider/res/values/strings.xml
new file mode 100644
index 0000000..4037a54
--- /dev/null
+++ b/packages/CMSettingsProvider/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014-2015 The CyanogenMod 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.
+-->
+<resources>
+    <string name="app_name">CMSettingsProvider</string>
+</resources>
\ No newline at end of file
diff --git a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java
new file mode 100644
index 0000000..85fbaa9
--- /dev/null
+++ b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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 org.cyanogenmod.cmsettings;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * The CMDatabaseHelper allows creation of a database to store CM specific settings for a user
+ * in System, Secure, and Global tables.
+ */
+public class CMDatabaseHelper extends SQLiteOpenHelper{
+    private static final String TAG = "CMDatabaseHelper";
+    private static final boolean LOCAL_LOGV = false;
+
+    private static final String DATABASE_NAME = "cmsettings.db";
+    private static final int DATABASE_VERSION = 1;
+
+    static class CMTableNames {
+        static final String TABLE_SYSTEM = "system";
+        static final String TABLE_SECURE = "secure";
+        static final String TABLE_GLOBAL = "global";
+    }
+
+    private static final String CREATE_TABLE_SQL_FORMAT = "CREATE TABLE %s (" +
+            "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
+            "name TEXT UNIQUE ON CONFLICT REPLACE," +
+            "value TEXT" +
+            ");)";
+
+    private static final String CREATE_INDEX_SQL_FORMAT = "CREATE INDEX %sIndex%d ON %s (name);";
+
+    private int mUserHandle;
+
+    /**
+     * Gets the appropriate database path for a specific user
+     * @param userId The database path for this user
+     * @return The database path string
+     */
+    static String dbNameForUser(final int userId) {
+        // The owner gets the unadorned db name;
+        if (userId == UserHandle.USER_OWNER) {
+            return DATABASE_NAME;
+        } else {
+            // Place the database in the user-specific data tree so that it's
+            // cleaned up automatically when the user is deleted.
+            File databaseFile = new File(
+                    Environment.getUserSystemDirectory(userId), DATABASE_NAME);
+            return databaseFile.getPath();
+        }
+    }
+
+    /**
+     * Creates an instance of {@link CMDatabaseHelper}
+     * @param context
+     * @param userId
+     */
+    public CMDatabaseHelper(Context context, int userId) {
+        super(context, dbNameForUser(userId), null, DATABASE_VERSION);
+        mUserHandle = userId;
+    }
+
+    /**
+     * Creates System, Secure, and Global tables in the specified {@link SQLiteDatabase}
+     * @param db The database.
+     */
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.beginTransaction();
+
+        try {
+            createDbTable(db, CMTableNames.TABLE_SYSTEM);
+            createDbTable(db, CMTableNames.TABLE_SECURE);
+
+            if (mUserHandle == UserHandle.USER_OWNER) {
+                createDbTable(db, CMTableNames.TABLE_GLOBAL);
+            }
+
+            db.setTransactionSuccessful();
+
+            if (LOCAL_LOGV) Log.v(TAG, "Successfully created tables for cm settings db");
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * Creates a table and index for the specified database and table name
+     * @param db
+     * @param tableName
+     */
+    private void createDbTable(SQLiteDatabase db, String tableName) {
+        if (LOCAL_LOGV) Log.v(TAG, "Creating table and index for: " + tableName);
+
+        String createTableSql = String.format(CREATE_TABLE_SQL_FORMAT, tableName);
+        db.execSQL(createTableSql);
+
+        String createIndexSql = String.format(CREATE_INDEX_SQL_FORMAT, tableName, 1, tableName);
+        db.execSQL(createIndexSql);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+
+    }
+}
diff --git a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java
new file mode 100644
index 0000000..eeabba3
--- /dev/null
+++ b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java
@@ -0,0 +1,451 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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 org.cyanogenmod.cmsettings;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.content.pm.PackageManager;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import cyanogenmod.providers.CMSettings;
+
+/**
+ * The CMSettingsProvider serves as a {@link ContentProvider} for CM specific settings
+ */
+public class CMSettingsProvider extends ContentProvider {
+    private static final String TAG = "CMSettingsProvider";
+    private static final boolean LOCAL_LOGV = false;
+
+    private static final boolean USER_CHECK_THROWS = true;
+
+    // Each defined user has their own settings
+    protected final SparseArray<CMDatabaseHelper> mDbHelpers = new SparseArray<CMDatabaseHelper>();
+
+    private static final int SYSTEM = 1;
+    private static final int SECURE = 2;
+    private static final int GLOBAL = 3;
+
+    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    static {
+        sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_SYSTEM,
+                SYSTEM);
+        sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_SECURE,
+                SECURE);
+        sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_GLOBAL,
+                GLOBAL);
+        // TODO add other paths for getting specific items
+    }
+
+    private UserManager mUserManager;
+    private Uri.Builder mUriBuilder;
+
+    @Override
+    public boolean onCreate() {
+        if (LOCAL_LOGV) Log.d(TAG, "Creating CMSettingsProvider");
+
+        mUserManager = UserManager.get(getContext());
+
+        establishDbTracking(UserHandle.USER_OWNER);
+
+        mUriBuilder = new Uri.Builder();
+        mUriBuilder.scheme(ContentResolver.SCHEME_CONTENT);
+        mUriBuilder.authority(CMSettings.AUTHORITY);
+
+        // TODO Add migration for cm settings
+
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        if (uri == null) {
+            throw new IllegalArgumentException("Uri cannot be null");
+        }
+
+        String tableName = getTableNameFromUri(uri);
+        checkWritePermissions(tableName);
+
+        int callingUserId = UserHandle.getCallingUserId();
+        CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName,
+                callingUserId));
+        SQLiteDatabase db = dbHelper.getReadableDatabase();
+
+        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+        queryBuilder.setTables(tableName);
+
+        Cursor returnCursor = queryBuilder.query(db, projection, selection, selectionArgs, null,
+                null, sortOrder);
+        // the default Cursor interface does not support per-user observation
+        try {
+            AbstractCursor abstractCursor = (AbstractCursor) returnCursor;
+            abstractCursor.setNotificationUri(getContext().getContentResolver(), uri,
+                    callingUserId);
+        } 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 returnCursor;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        return bulkInsertForUser(UserHandle.getCallingUserId(), uri, values);
+    }
+
+    /**
+     * Performs a bulk insert for a specific user.
+     * @param userId The user id to perform the bulk insert for.
+     * @param uri The content:// URI of the insertion request.
+     * @param values An array of sets of column_name/value pairs to add to the database.
+     *    This must not be {@code null}.
+     * @return Number of rows inserted.
+     */
+    int bulkInsertForUser(int userId, Uri uri, ContentValues[] values) {
+        if (uri == null) {
+            throw new IllegalArgumentException("Uri cannot be null");
+        }
+
+        if (values == null) {
+            throw new IllegalArgumentException("ContentValues cannot be null");
+        }
+
+        int numRowsAffected = 0;
+
+        String tableName = getTableNameFromUri(uri);
+        checkWritePermissions(tableName);
+
+        CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, userId));
+        SQLiteDatabase db = dbHelper.getWritableDatabase();
+
+        db.beginTransaction();
+        try {
+            for (ContentValues value : values) {
+                if (value == null) {
+                    continue;
+                }
+
+                long rowId = db.insert(tableName, null, value);
+
+                if (rowId >= 0) {
+                    numRowsAffected++;
+
+                    if (LOCAL_LOGV) Log.d(TAG, tableName + " <- " + values);
+                } else {
+                    return 0;
+                }
+            }
+
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+            db.close();
+        }
+
+        if (numRowsAffected > 0) {
+            getContext().getContentResolver().notifyChange(uri, null);
+            notifyChange(uri, tableName, userId);
+            if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) inserted");
+        }
+
+        return numRowsAffected;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (uri == null) {
+            throw new IllegalArgumentException("Uri cannot be null");
+        }
+
+        if (values == null) {
+            throw new IllegalArgumentException("ContentValues cannot be null");
+        }
+
+        String tableName = getTableNameFromUri(uri);
+        checkWritePermissions(tableName);
+
+        int callingUserId = UserHandle.getCallingUserId();
+        CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName,
+                callingUserId));
+
+        long rowId = -1;
+
+        SQLiteDatabase db = dbHelper.getWritableDatabase();
+        try {
+            rowId = db.insert(tableName, null, values);
+        } finally {
+            db.close();
+        }
+
+        Uri returnUri = null;
+        if (rowId != -1) {
+            returnUri = ContentUris.withAppendedId(uri, rowId);
+            notifyChange(returnUri, tableName, callingUserId);
+            if (LOCAL_LOGV) Log.d(TAG, "Inserted row id: " + rowId + " into tableName: " +
+                    tableName);
+        }
+
+        return returnUri;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        if (uri == null) {
+            throw new IllegalArgumentException("Uri cannot be null");
+        }
+
+        int numRowsAffected = 0;
+
+        // Allow only selection by key; a null/empty selection string will cause all rows in the
+        // table to be deleted
+        if (!TextUtils.isEmpty(selection) && selectionArgs.length > 0) {
+            String tableName = getTableNameFromUri(uri);
+            checkWritePermissions(tableName);
+
+            int callingUserId = UserHandle.getCallingUserId();
+            CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName,
+                    callingUserId));
+            SQLiteDatabase db = dbHelper.getWritableDatabase();
+
+            try {
+                numRowsAffected = db.delete(tableName, selection, selectionArgs);
+            } finally {
+                db.close();
+            }
+
+            if (numRowsAffected > 0) {
+                notifyChange(uri, tableName, callingUserId);
+                if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) deleted");
+            }
+        }
+
+        return numRowsAffected;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        if (uri == null) {
+            throw new IllegalArgumentException("Uri cannot be null");
+        }
+
+        if (values == null) {
+            throw new IllegalArgumentException("ContentValues cannot be null");
+        }
+
+        String tableName = getTableNameFromUri(uri);
+        checkWritePermissions(tableName);
+
+        int callingUserId = UserHandle.getCallingUserId();
+        CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName,
+                callingUserId));
+
+        int numRowsAffected = 0;
+
+        SQLiteDatabase db = dbHelper.getWritableDatabase();
+        try {
+            numRowsAffected = db.update(tableName, values, selection, selectionArgs);
+        } finally {
+            db.close();
+        }
+
+        if (numRowsAffected > 0) {
+            getContext().getContentResolver().notifyChange(uri, null);
+            if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) updated");
+        }
+
+        return numRowsAffected;
+    }
+
+    /**
+     * Tries to get a {@link CMDatabaseHelper} for the specified user and if it does not exist, a
+     * new instance of {@link CMDatabaseHelper} is created for the specified user and returned.
+     * @param callingUser
+     * @return
+     */
+    private CMDatabaseHelper getOrEstablishDatabase(int callingUser) {
+        if (callingUser >= android.os.Process.SYSTEM_UID) {
+            if (USER_CHECK_THROWS) {
+                throw new IllegalArgumentException("Uid rather than user handle: " + callingUser);
+            } else {
+                Log.wtf(TAG, "Establish db for uid rather than user: " + callingUser);
+            }
+        }
+
+        long oldId = Binder.clearCallingIdentity();
+        try {
+            CMDatabaseHelper dbHelper;
+            synchronized (this) {
+                dbHelper = mDbHelpers.get(callingUser);
+            }
+            if (null == dbHelper) {
+                establishDbTracking(callingUser);
+                synchronized (this) {
+                    dbHelper = mDbHelpers.get(callingUser);
+                }
+            }
+            return dbHelper;
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /**
+     * Check if a {@link CMDatabaseHelper} exists for a user and if it doesn't, a new helper is
+     * created and added to the list of tracked database helpers
+     * @param userId
+     */
+    private void establishDbTracking(int userId) {
+        CMDatabaseHelper dbHelper;
+
+        synchronized (this) {
+            dbHelper = mDbHelpers.get(userId);
+            if (LOCAL_LOGV) {
+                Log.i(TAG, "Checking cm settings db helper for user " + userId);
+            }
+            if (dbHelper == null) {
+                if (LOCAL_LOGV) {
+                    Log.i(TAG, "Installing new cm settings db helper for user " + userId);
+                }
+                dbHelper = new CMDatabaseHelper(getContext(), userId);
+                mDbHelpers.append(userId, dbHelper);
+            }
+        }
+
+        // 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 = null;
+        try {
+            db = dbHelper.getWritableDatabase();
+        } catch (SQLiteCantOpenDatabaseException ex){
+            Log.e(TAG, "Unable to open writable database for user: " + userId, ex);
+        } finally {
+            db.close();
+        }
+    }
+
+    /**
+     * Makes sure the caller has permission to write this data.
+     * @param tableName supplied by the caller
+     * @throws SecurityException if the caller is forbidden to write.
+     */
+    private void checkWritePermissions(String tableName) {
+        if ((CMDatabaseHelper.CMTableNames.TABLE_SECURE.equals(tableName) ||
+                CMDatabaseHelper.CMTableNames.TABLE_GLOBAL.equals(tableName)) &&
+                getContext().checkCallingOrSelfPermission(
+                        cyanogenmod.platform.Manifest.permission.WRITE_SECURE_SETTINGS) !=
+                        PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    String.format("Permission denial: writing to cm secure settings requires %1$s",
+                            cyanogenmod.platform.Manifest.permission.WRITE_SECURE_SETTINGS));
+        }
+    }
+
+    /**
+     * Utilizes an {@link UriMatcher} to check for a valid combination of scheme, authority, and
+     * path and returns the corresponding table name
+     * @param uri
+     * @return Table name
+     */
+    private String getTableNameFromUri(Uri uri) {
+        int code = sUriMatcher.match(uri);
+
+        switch (code) {
+            case SYSTEM:
+                return CMDatabaseHelper.CMTableNames.TABLE_SYSTEM;
+            case SECURE:
+                return CMDatabaseHelper.CMTableNames.TABLE_SECURE;
+            case GLOBAL:
+                return CMDatabaseHelper.CMTableNames.TABLE_GLOBAL;
+            default:
+                throw new IllegalArgumentException("Invalid uri: " + uri);
+        }
+    }
+
+    /**
+     * If the table is Global, the owner's user id is returned. Otherwise, the original user id
+     * is returned.
+     * @param tableName
+     * @param userId
+     * @return User id
+     */
+    private int getUserIdForTable(String tableName, int userId) {
+        return CMDatabaseHelper.CMTableNames.TABLE_GLOBAL.equals(tableName) ?
+                UserHandle.USER_OWNER : userId;
+    }
+
+    /**
+     * Modify setting version for an updated table before notifying of change. The
+     * {@link CMSettings} class uses these to provide client-side caches.
+     * @param uri to send notifications for
+     * @param userId
+     */
+    private void notifyChange(Uri uri, String tableName, int userId) {
+        String property = null;
+        final boolean isGlobal = tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_GLOBAL);
+        if (tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_SYSTEM)) {
+            property = CMSettings.System.SYS_PROP_CM_SETTING_VERSION;
+        } else if (tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_SECURE)) {
+            property = CMSettings.Secure.SYS_PROP_CM_SETTING_VERSION;
+        } else if (isGlobal) {
+            property = CMSettings.Global.SYS_PROP_CM_SETTING_VERSION;
+        }
+
+        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));
+        }
+
+        final int notifyTarget = isGlobal ? UserHandle.USER_ALL : userId;
+        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);
+    }
+
+    // TODO Add caching
+}
diff --git a/packages/CMSettingsProvider/tests/Android.mk b/packages/CMSettingsProvider/tests/Android.mk
new file mode 100644
index 0000000..52c3e4e
--- /dev/null
+++ b/packages/CMSettingsProvider/tests/Android.mk
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2015 The CyanogenMod 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.
+#
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_PACKAGE_NAME := CMSettingsProviderTests
+LOCAL_INSTRUMENTATION_FOR := CMSettingsProvider
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_CERTIFICATE := platform
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    org.cyanogenmod.platform.sdk
+
+include $(BUILD_PACKAGE)
diff --git a/packages/CMSettingsProvider/tests/AndroidManifest.xml b/packages/CMSettingsProvider/tests/AndroidManifest.xml
new file mode 100644
index 0000000..e82a7d8
--- /dev/null
+++ b/packages/CMSettingsProvider/tests/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The CyanogenMod 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="org.cyanogenmod.cmsettings.tests">
+
+    <uses-permission android:name="cyanogenmod.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="cyanogenmod.permission.WRITE_SECURE_SETTINGS"/>
+
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="org.cyanogenmod.cmsettings.tests"
+            android:label="CM Settings Provider Tests" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+</manifest>
diff --git a/packages/CMSettingsProvider/tests/src/org/cyanogenmod/cmsettings/tests/CMSettingsProviderTest.java b/packages/CMSettingsProvider/tests/src/org/cyanogenmod/cmsettings/tests/CMSettingsProviderTest.java
new file mode 100644
index 0000000..7ec446c
--- /dev/null
+++ b/packages/CMSettingsProvider/tests/src/org/cyanogenmod/cmsettings/tests/CMSettingsProviderTest.java
@@ -0,0 +1,159 @@
+ /**
+ * Copyright (c) 2015, The CyanogenMod 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 org.cyanogenmod.cmsettings.tests;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+import cyanogenmod.providers.CMSettings;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+ public class CMSettingsProviderTest extends AndroidTestCase {
+     private static final String TAG = "CMSettingsProviderTest";
+
+     private static final LinkedHashMap<String, String> mMap = new LinkedHashMap<String, String>();
+
+     static {
+         mMap.put("testKey1", "value1");
+         mMap.put("testKey2", "value2");
+         mMap.put("testKey3", "value3");
+     }
+
+     private static final String[] PROJECTIONS = new String[] { "name", "value" };
+
+     private ContentResolver mContentResolver;
+
+     @Override
+     public void setUp() {
+         mContentResolver = mContext.getContentResolver();
+     }
+
+     @MediumTest
+     public void testBulkInsertSuccess() {
+         Log.d(TAG, "Starting bulk insert test");
+
+         ContentValues[] contentValues = new ContentValues[mMap.size()];
+         int count = 0;
+         for (Map.Entry<String, String> kVPair : mMap.entrySet()) {
+             ContentValues contentValue = new ContentValues();
+             contentValue.put(PROJECTIONS[0], kVPair.getKey());
+             contentValue.put(PROJECTIONS[1], kVPair.getValue());
+             contentValues[count++] = contentValue;
+         }
+
+         testBulkInsertForUri(CMSettings.System.CONTENT_URI, contentValues);
+         testBulkInsertForUri(CMSettings.Secure.CONTENT_URI, contentValues);
+         testBulkInsertForUri(CMSettings.Global.CONTENT_URI, contentValues);
+
+         Log.d(TAG, "Finished bulk insert test");
+     }
+
+     private void testBulkInsertForUri(Uri uri, ContentValues[] contentValues) {
+         int rowsInserted = mContentResolver.bulkInsert(uri, contentValues);
+         assertEquals(mMap.size(), rowsInserted);
+
+         Cursor queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null);
+         try {
+             while (queryCursor.moveToNext()) {
+                 assertEquals(PROJECTIONS.length, queryCursor.getColumnCount());
+
+                 String actualKey = queryCursor.getString(0);
+                 assertTrue(mMap.containsKey(actualKey));
+
+                 assertEquals(mMap.get(actualKey), queryCursor.getString(1));
+             }
+
+             Log.d(TAG, "Test successful");
+         }
+         finally {
+             queryCursor.close();
+         }
+
+         // TODO: Find a better way to cleanup database/use ProviderTestCase2 without process crash
+         for (String key : mMap.keySet()) {
+             mContentResolver.delete(uri, PROJECTIONS[0] + " = ?", new String[]{ key });
+         }
+     }
+
+     @MediumTest
+     public void testInsertUpdateDeleteSuccess() {
+         Log.d(TAG, "Starting insert/update/delete test");
+
+         testInsertUpdateDeleteForUri(CMSettings.System.CONTENT_URI);
+         testInsertUpdateDeleteForUri(CMSettings.Secure.CONTENT_URI);
+         testInsertUpdateDeleteForUri(CMSettings.Global.CONTENT_URI);
+
+         Log.d(TAG, "Finished insert/update/delete test");
+     }
+
+     private void testInsertUpdateDeleteForUri(Uri uri) {
+         String key1 = "testKey1";
+         String value1 = "value1";
+         String value2 = "value2";
+
+         // test insert
+         ContentValues contentValue = new ContentValues();
+         contentValue.put(PROJECTIONS[0], key1);
+         contentValue.put(PROJECTIONS[1], value1);
+
+         mContentResolver.insert(uri, contentValue);
+
+         // check insert
+         Cursor queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null);
+         assertEquals(1, queryCursor.getCount());
+
+         queryCursor.moveToNext();
+         assertEquals(PROJECTIONS.length, queryCursor.getColumnCount());
+
+         String actualKey = queryCursor.getString(0);
+         assertEquals(key1, actualKey);
+         assertEquals(value1, queryCursor.getString(1));
+
+         // test update
+         contentValue.clear();
+         contentValue.put(PROJECTIONS[1], value2);
+
+         int rowsAffected = mContentResolver.update(uri, contentValue, PROJECTIONS[0] + " = ?",
+                 new String[]{key1});
+         assertEquals(1, rowsAffected);
+
+         // check update
+         queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null);
+         assertEquals(1, queryCursor.getCount());
+
+         queryCursor.moveToNext();
+         assertEquals(PROJECTIONS.length, queryCursor.getColumnCount());
+
+         actualKey = queryCursor.getString(0);
+         assertEquals(key1, actualKey);
+         assertEquals(value2, queryCursor.getString(1));
+
+         // test delete
+         rowsAffected = mContentResolver.delete(uri, PROJECTIONS[0] + " = ?", new String[]{key1});
+         assertEquals(1, rowsAffected);
+
+         // check delete
+         queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null);
+         assertEquals(0, queryCursor.getCount());
+     }
+ }
diff --git a/src/java/cyanogenmod/providers/CMSettings.java b/src/java/cyanogenmod/providers/CMSettings.java
new file mode 100644
index 0000000..afeacc8
--- /dev/null
+++ b/src/java/cyanogenmod/providers/CMSettings.java
@@ -0,0 +1,1044 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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 cyanogenmod.providers;
+
+import android.content.ContentResolver;
+import android.content.IContentProvider;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.AndroidException;
+import android.util.Log;
+
+import java.util.HashMap;
+
+/**
+ * CMSettings contains CM specific preferences in System, Secure, and Global.
+ */
+public final class CMSettings {
+    private static final String TAG = "CMSettings";
+    private static final boolean LOCAL_LOGV = false;
+
+    public static final String AUTHORITY = "cmsettings";
+
+    public static class CMSettingNotFoundException extends AndroidException {
+        public CMSettingNotFoundException(String msg) {
+            super(msg);
+        }
+    }
+
+    // Thread-safe.
+    private static class NameValueCache {
+        // TODO Add call options for fast path at insert and get
+
+        private final String mVersionSystemProperty;
+        private final Uri mUri;
+
+        private static final String[] SELECT_VALUE =
+                new String[] { Settings.NameValueTable.VALUE };
+        private static final String NAME_EQ_PLACEHOLDER = "name=?";
+
+        // Must synchronize on 'this' to access mValues and mValuesVersion.
+        private final HashMap<String, String> mValues = new HashMap<String, String>();
+        private long mValuesVersion = 0;
+
+        // Initially null; set lazily and held forever.  Synchronized on 'this'.
+        private IContentProvider mContentProvider = null;
+
+        public NameValueCache(String versionSystemProperty, Uri uri) {
+            mVersionSystemProperty = versionSystemProperty;
+            mUri = uri;
+        }
+
+        private IContentProvider lazyGetProvider(ContentResolver cr) {
+            IContentProvider cp;
+            synchronized (this) {
+                cp = mContentProvider;
+                if (cp == null) {
+                    cp = mContentProvider = cr.acquireProvider(mUri.getAuthority());
+                }
+            }
+            return cp;
+        }
+
+        /**
+         * Gets a a string value with the specified name from the name/value cache if possible. If
+         * not, it will use the content resolver and perform a query.
+         * @param cr Content resolver to use if name/value cache does not contain the name or if
+         *           the cache version is older than the current version.
+         * @param name The name of the key to search for.
+         * @param userId The user id of the cache to look in.
+         * @return The string value of the specified key.
+         */
+        public String getStringForUser(ContentResolver cr, String name, final int userId) {
+            final boolean isSelf = (userId == UserHandle.myUserId());
+            if (isSelf) {
+                long newValuesVersion = SystemProperties.getLong(mVersionSystemProperty, 0);
+
+                // Our own user's settings data uses a client-side cache
+                synchronized (this) {
+                    if (mValuesVersion != newValuesVersion) {
+                        if (LOCAL_LOGV || false) {
+                            Log.v(TAG, "invalidate [" + mUri.getLastPathSegment() + "]: current "
+                                    + newValuesVersion + " != cached " + mValuesVersion);
+                        }
+
+                        mValues.clear();
+                        mValuesVersion = newValuesVersion;
+                    }
+
+                    if (mValues.containsKey(name)) {
+                        return mValues.get(name);  // Could be null, that's OK -- negative caching
+                    }
+                }
+            } else {
+                if (LOCAL_LOGV) Log.v(TAG, "get setting for user " + userId
+                        + " by user " + UserHandle.myUserId() + " so skipping cache");
+            }
+
+            IContentProvider cp = lazyGetProvider(cr);
+
+            Cursor c = null;
+            try {
+                c = cp.query(cr.getPackageName(), mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER,
+                        new String[]{name}, null, null);
+                if (c == null) {
+                    Log.w(TAG, "Can't get key " + name + " from " + mUri);
+                    return null;
+                }
+
+                String value = c.moveToNext() ? c.getString(0) : null;
+                synchronized (this) {
+                    mValues.put(name, value);
+                }
+                if (LOCAL_LOGV) {
+                    Log.v(TAG, "cache miss [" + mUri.getLastPathSegment() + "]: " +
+                            name + " = " + (value == null ? "(null)" : value));
+                }
+                return value;
+            } catch (RemoteException e) {
+                Log.w(TAG, "Can't get key " + name + " from " + mUri, e);
+                return null;  // Return null, but don't cache it.
+            } finally {
+                if (c != null) c.close();
+            }
+        }
+    }
+
+    /**
+     * System settings, containing miscellaneous CM system preferences.  This
+     * table holds simple name/value pairs.  There are convenience
+     * functions for accessing individual settings entries.
+     */
+    public static final class System extends Settings.NameValueTable {
+        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/system");
+
+        public static final String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_system_version";
+
+        private static final NameValueCache sNameValueCache = new NameValueCache(
+                SYS_PROP_CM_SETTING_VERSION,
+                CONTENT_URI);
+
+        // region Methods
+
+        /**
+         * Look up a name in the database.
+         * @param resolver to access the database with
+         * @param name to look up in the table
+         * @return the corresponding value, or null if not present
+         */
+        public static String getString(ContentResolver resolver, String name) {
+            return getStringForUser(resolver, name, UserHandle.myUserId());
+        }
+
+        /** @hide */
+        public static String getStringForUser(ContentResolver resolver, String name,
+                int userHandle) {
+            return sNameValueCache.getStringForUser(resolver, name, userHandle);
+        }
+
+        /**
+         * Store a name/value pair into the database.
+         * @param resolver to access the database with
+         * @param name to store
+         * @param value to associate with the name
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putString(ContentResolver resolver, String name, String value) {
+            return putString(resolver, CONTENT_URI, name, value);
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as an integer.  Note that internally setting values are always
+         * stored as strings; this function converts the string to an integer
+         * for you.  The default value will be returned if the setting is
+         * not defined or not an integer.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid integer.
+         */
+        public static int getInt(ContentResolver cr, String name, int def) {
+            String v = getString(cr, name);
+            try {
+                return v != null ? Integer.parseInt(v) : def;
+            } catch (NumberFormatException e) {
+                return def;
+            }
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as an integer.  Note that internally setting values are always
+         * stored as strings; this function converts the string to an integer
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not an integer.
+         *
+         * @return The setting's current value.
+         */
+        public static int getInt(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String v = getString(cr, name);
+            try {
+                return Integer.parseInt(v);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a single settings value as an
+         * integer. This will either create a new entry in the table if the
+         * given name does not exist, or modify the value of the existing row
+         * with that name.  Note that internally setting values are always
+         * stored as strings, so this function converts the given value to a
+         * string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putInt(ContentResolver cr, String name, int value) {
+            return putString(cr, name, Integer.toString(value));
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a {@code long}.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a {@code long}
+         * for you.  The default value will be returned if the setting is
+         * not defined or not a {@code long}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid {@code long}.
+         */
+        public static long getLong(ContentResolver cr, String name, long def) {
+            String valString = getString(cr, name);
+            long value;
+            try {
+                value = valString != null ? Long.parseLong(valString) : def;
+            } catch (NumberFormatException e) {
+                value = def;
+            }
+            return value;
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a {@code long}.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a {@code long}
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @return The setting's current value.
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not an integer.
+         */
+        public static long getLong(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String valString = getString(cr, name);
+            try {
+                return Long.parseLong(valString);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a secure settings value as a long
+         * integer. This will either create a new entry in the table if the
+         * given name does not exist, or modify the value of the existing row
+         * with that name.  Note that internally setting values are always
+         * stored as strings, so this function converts the given value to a
+         * string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putLong(ContentResolver cr, String name, long value) {
+            return putString(cr, name, Long.toString(value));
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a floating point number.  Note that internally setting values are
+         * always stored as strings; this function converts the string to an
+         * float for you. The default value will be returned if the setting
+         * is not defined or not a valid float.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid float.
+         */
+        public static float getFloat(ContentResolver cr, String name, float def) {
+            String v = getString(cr, name);
+            try {
+                return v != null ? Float.parseFloat(v) : def;
+            } catch (NumberFormatException e) {
+                return def;
+            }
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a float.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a float
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not a float.
+         *
+         * @return The setting's current value.
+         */
+        public static float getFloat(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String v = getString(cr, name);
+            if (v == null) {
+                throw new CMSettingNotFoundException(name);
+            }
+            try {
+                return Float.parseFloat(v);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a single settings value as a
+         * floating point number. This will either create a new entry in the
+         * table if the given name does not exist, or modify the value of the
+         * existing row with that name.  Note that internally setting values
+         * are always stored as strings, so this function converts the given
+         * value to a string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putFloat(ContentResolver cr, String name, float value) {
+            return putString(cr, name, Float.toString(value));
+        }
+
+        // endregion
+
+        // region System Settings
+
+        /**
+         * Quick Settings Quick Pulldown
+         * 0 = off, 1 = right, 2 = left
+         * @hide
+         */
+        public static final String QS_QUICK_PULLDOWN = "qs_quick_pulldown";
+
+        // endregion
+    }
+
+    /**
+     * Secure settings, containing miscellaneous CM secure preferences.  This
+     * table holds simple name/value pairs.  There are convenience
+     * functions for accessing individual settings entries.
+     */
+    public static final class Secure extends Settings.NameValueTable {
+        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/secure");
+
+        public static final String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_secure_version";
+
+        private static final NameValueCache sNameValueCache = new NameValueCache(
+                SYS_PROP_CM_SETTING_VERSION,
+                CONTENT_URI);
+
+        /**
+         * Look up a name in the database.
+         * @param resolver to access the database with
+         * @param name to look up in the table
+         * @return the corresponding value, or null if not present
+         */
+        public static String getString(ContentResolver resolver, String name) {
+            return getStringForUser(resolver, name, UserHandle.myUserId());
+        }
+
+        /** @hide */
+        public static String getStringForUser(ContentResolver resolver, String name,
+                int userHandle) {
+            return sNameValueCache.getStringForUser(resolver, name, userHandle);
+        }
+
+        /**
+         * Store a name/value pair into the database.
+         * @param resolver to access the database with
+         * @param name to store
+         * @param value to associate with the name
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putString(ContentResolver resolver, String name, String value) {
+            return putString(resolver, CONTENT_URI, name, value);
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as an integer.  Note that internally setting values are always
+         * stored as strings; this function converts the string to an integer
+         * for you.  The default value will be returned if the setting is
+         * not defined or not an integer.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid integer.
+         */
+        public static int getInt(ContentResolver cr, String name, int def) {
+            String v = getString(cr, name);
+            try {
+                return v != null ? Integer.parseInt(v) : def;
+            } catch (NumberFormatException e) {
+                return def;
+            }
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as an integer.  Note that internally setting values are always
+         * stored as strings; this function converts the string to an integer
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not an integer.
+         *
+         * @return The setting's current value.
+         */
+        public static int getInt(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String v = getString(cr, name);
+            try {
+                return Integer.parseInt(v);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a single settings value as an
+         * integer. This will either create a new entry in the table if the
+         * given name does not exist, or modify the value of the existing row
+         * with that name.  Note that internally setting values are always
+         * stored as strings, so this function converts the given value to a
+         * string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putInt(ContentResolver cr, String name, int value) {
+            return putString(cr, name, Integer.toString(value));
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a {@code long}.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a {@code long}
+         * for you.  The default value will be returned if the setting is
+         * not defined or not a {@code long}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid {@code long}.
+         */
+        public static long getLong(ContentResolver cr, String name, long def) {
+            String valString = getString(cr, name);
+            long value;
+            try {
+                value = valString != null ? Long.parseLong(valString) : def;
+            } catch (NumberFormatException e) {
+                value = def;
+            }
+            return value;
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a {@code long}.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a {@code long}
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @return The setting's current value.
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not an integer.
+         */
+        public static long getLong(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String valString = getString(cr, name);
+            try {
+                return Long.parseLong(valString);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a secure settings value as a long
+         * integer. This will either create a new entry in the table if the
+         * given name does not exist, or modify the value of the existing row
+         * with that name.  Note that internally setting values are always
+         * stored as strings, so this function converts the given value to a
+         * string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putLong(ContentResolver cr, String name, long value) {
+            return putString(cr, name, Long.toString(value));
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a floating point number.  Note that internally setting values are
+         * always stored as strings; this function converts the string to an
+         * float for you. The default value will be returned if the setting
+         * is not defined or not a valid float.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid float.
+         */
+        public static float getFloat(ContentResolver cr, String name, float def) {
+            String v = getString(cr, name);
+            try {
+                return v != null ? Float.parseFloat(v) : def;
+            } catch (NumberFormatException e) {
+                return def;
+            }
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a float.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a float
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not a float.
+         *
+         * @return The setting's current value.
+         */
+        public static float getFloat(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String v = getString(cr, name);
+            if (v == null) {
+                throw new CMSettingNotFoundException(name);
+            }
+            try {
+                return Float.parseFloat(v);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a single settings value as a
+         * floating point number. This will either create a new entry in the
+         * table if the given name does not exist, or modify the value of the
+         * existing row with that name.  Note that internally setting values
+         * are always stored as strings, so this function converts the given
+         * value to a string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putFloat(ContentResolver cr, String name, float value) {
+            return putString(cr, name, Float.toString(value));
+        }
+
+        // endregion
+
+        // region Secure Settings
+
+        /**
+         * Whether to enable "advanced mode" for the current user.
+         * Boolean setting. 0 = no, 1 = yes.
+         * @hide
+         */
+        public static final String ADVANCED_MODE = "advanced_mode";
+
+        /**
+         * The time in ms to keep the button backlight on after pressing a button.
+         * A value of 0 will keep the buttons on for as long as the screen is on.
+         * @hide
+         */
+        public static final String BUTTON_BACKLIGHT_TIMEOUT = "button_backlight_timeout";
+
+        /**
+         * The button brightness to be used while the screen is on or after a button press,
+         * depending on the value of {@link BUTTON_BACKLIGHT_TIMEOUT}.
+         * Valid value range is between 0 and {@link PowerManager#getMaximumButtonBrightness()}
+         * @hide
+         */
+        public static final String BUTTON_BRIGHTNESS = "button_brightness";
+
+        /**
+         * A '|' delimited list of theme components to apply from the default theme on first boot.
+         * Components can be one or more of the "mods_XXXXXXX" found in
+         * {@link ThemesContract$ThemesColumns}.  Leaving this field blank assumes all components
+         * will be applied.
+         *
+         * ex: mods_icons|mods_overlays|mods_homescreen
+         *
+         * @hide
+         */
+        public static final String DEFAULT_THEME_COMPONENTS = "default_theme_components";
+
+        /**
+         * Default theme to use.  If empty, use holo.
+         * @hide
+         */
+        public static final String DEFAULT_THEME_PACKAGE = "default_theme_package";
+
+        /**
+         * Developer options - Navigation Bar show switch
+         * @hide
+         */
+        public static final String DEV_FORCE_SHOW_NAVBAR = "dev_force_show_navbar";
+
+        /**
+         * The keyboard brightness to be used while the screen is on.
+         * Valid value range is between 0 and {@link PowerManager#getMaximumKeyboardBrightness()}
+         * @hide
+         */
+        public static final String KEYBOARD_BRIGHTNESS = "keyboard_brightness";
+
+        /**
+         * Default theme config name
+         */
+        public static final String NAME_THEME_CONFIG = "name_theme_config";
+
+        /**
+         * Custom navring actions
+         * @hide
+         */
+        public static final String[] NAVIGATION_RING_TARGETS = new String[] {
+                "navigation_ring_targets_0",
+                "navigation_ring_targets_1",
+                "navigation_ring_targets_2",
+        };
+
+        /**
+         * String to contain power menu actions
+         * @hide
+         */
+        public static final String POWER_MENU_ACTIONS = "power_menu_actions";
+
+        /**
+         * Whether to show the brightness slider in quick settings panel.
+         * @hide
+         */
+        public static final String QS_SHOW_BRIGHTNESS_SLIDER = "qs_show_brightness_slider";
+
+        /**
+         * List of QS tile names
+         * @hide
+         */
+        public static final String QS_TILES = "sysui_qs_tiles";
+
+        /**
+         * Use "main" tiles on the first row of the quick settings panel
+         * 0 = no, 1 = yes
+         * @hide
+         */
+        public static final String QS_USE_MAIN_TILES = "sysui_qs_main_tiles";
+
+        /**
+         * Global stats collection
+         * @hide
+         */
+        public static final String STATS_COLLECTION = "stats_collection";
+
+        /**
+         * Boolean value whether to link ringtone and notification volume
+         *
+         * @hide
+         */
+        public static final String VOLUME_LINK_NOTIFICATION = "volume_link_notification";
+
+        // endregion
+    }
+
+    /**
+     * Global settings, containing miscellaneous CM global preferences.  This
+     * table holds simple name/value pairs.  There are convenience
+     * functions for accessing individual settings entries.
+     */
+    public static final class Global extends Settings.NameValueTable {
+        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/global");
+
+        public static final String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_global_version";
+
+        private static final NameValueCache sNameValueCache = new NameValueCache(
+                SYS_PROP_CM_SETTING_VERSION,
+                CONTENT_URI);
+
+        // region Methods
+
+        /**
+         * Look up a name in the database.
+         * @param resolver to access the database with
+         * @param name to look up in the table
+         * @return the corresponding value, or null if not present
+         */
+        public static String getString(ContentResolver resolver, String name) {
+            return getStringForUser(resolver, name, UserHandle.myUserId());
+        }
+
+        /** @hide */
+        public static String getStringForUser(ContentResolver resolver, String name,
+                int userHandle) {
+            return sNameValueCache.getStringForUser(resolver, name, userHandle);
+        }
+
+        /**
+         * Store a name/value pair into the database.
+         * @param resolver to access the database with
+         * @param name to store
+         * @param value to associate with the name
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putString(ContentResolver resolver, String name, String value) {
+            return putString(resolver, CONTENT_URI, name, value);
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as an integer.  Note that internally setting values are always
+         * stored as strings; this function converts the string to an integer
+         * for you.  The default value will be returned if the setting is
+         * not defined or not an integer.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid integer.
+         */
+        public static int getInt(ContentResolver cr, String name, int def) {
+            String v = getString(cr, name);
+            try {
+                return v != null ? Integer.parseInt(v) : def;
+            } catch (NumberFormatException e) {
+                return def;
+            }
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as an integer.  Note that internally setting values are always
+         * stored as strings; this function converts the string to an integer
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not an integer.
+         *
+         * @return The setting's current value.
+         */
+        public static int getInt(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String v = getString(cr, name);
+            try {
+                return Integer.parseInt(v);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a single settings value as an
+         * integer. This will either create a new entry in the table if the
+         * given name does not exist, or modify the value of the existing row
+         * with that name.  Note that internally setting values are always
+         * stored as strings, so this function converts the given value to a
+         * string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putInt(ContentResolver cr, String name, int value) {
+            return putString(cr, name, Integer.toString(value));
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a {@code long}.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a {@code long}
+         * for you.  The default value will be returned if the setting is
+         * not defined or not a {@code long}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid {@code long}.
+         */
+        public static long getLong(ContentResolver cr, String name, long def) {
+            String valString = getString(cr, name);
+            long value;
+            try {
+                value = valString != null ? Long.parseLong(valString) : def;
+            } catch (NumberFormatException e) {
+                value = def;
+            }
+            return value;
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a {@code long}.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a {@code long}
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @return The setting's current value.
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not an integer.
+         */
+        public static long getLong(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String valString = getString(cr, name);
+            try {
+                return Long.parseLong(valString);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a secure settings value as a long
+         * integer. This will either create a new entry in the table if the
+         * given name does not exist, or modify the value of the existing row
+         * with that name.  Note that internally setting values are always
+         * stored as strings, so this function converts the given value to a
+         * string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putLong(ContentResolver cr, String name, long value) {
+            return putString(cr, name, Long.toString(value));
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a floating point number.  Note that internally setting values are
+         * always stored as strings; this function converts the string to an
+         * float for you. The default value will be returned if the setting
+         * is not defined or not a valid float.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         * @param def Value to return if the setting is not defined.
+         *
+         * @return The setting's current value, or 'def' if it is not defined
+         * or not a valid float.
+         */
+        public static float getFloat(ContentResolver cr, String name, float def) {
+            String v = getString(cr, name);
+            try {
+                return v != null ? Float.parseFloat(v) : def;
+            } catch (NumberFormatException e) {
+                return def;
+            }
+        }
+
+        /**
+         * Convenience function for retrieving a single secure settings value
+         * as a float.  Note that internally setting values are always
+         * stored as strings; this function converts the string to a float
+         * for you.
+         * <p>
+         * This version does not take a default value.  If the setting has not
+         * been set, or the string value is not a number,
+         * it throws {@link CMSettingNotFoundException}.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to retrieve.
+         *
+         * @throws CMSettingNotFoundException Thrown if a setting by the given
+         * name can't be found or the setting value is not a float.
+         *
+         * @return The setting's current value.
+         */
+        public static float getFloat(ContentResolver cr, String name)
+                throws CMSettingNotFoundException {
+            String v = getString(cr, name);
+            if (v == null) {
+                throw new CMSettingNotFoundException(name);
+            }
+            try {
+                return Float.parseFloat(v);
+            } catch (NumberFormatException e) {
+                throw new CMSettingNotFoundException(name);
+            }
+        }
+
+        /**
+         * Convenience function for updating a single settings value as a
+         * floating point number. This will either create a new entry in the
+         * table if the given name does not exist, or modify the value of the
+         * existing row with that name.  Note that internally setting values
+         * are always stored as strings, so this function converts the given
+         * value to a string before storing it.
+         *
+         * @param cr The ContentResolver to access.
+         * @param name The name of the setting to modify.
+         * @param value The new value for the setting.
+         * @return true if the value was set, false on database errors
+         */
+        public static boolean putFloat(ContentResolver cr, String name, float value) {
+            return putString(cr, name, Float.toString(value));
+        }
+
+        // endregion
+
+        // region Global Settings
+
+        /**
+         * The name of the device
+         *
+         * @hide
+         */
+        public static final String DEVICE_NAME = "device_name";
+
+        /**
+         * Defines global heads up toggle.  One of HEADS_UP_OFF, HEADS_UP_ON.
+         *
+         * @hide
+         */
+        public static final String HEADS_UP_NOTIFICATIONS_ENABLED =
+                "heads_up_notifications_enabled";
+
+        // endregion
+    }
+}
\ No newline at end of file
diff --git a/system-api/cm_system-current.txt b/system-api/cm_system-current.txt
index 02aaa62..e7e6011 100644
--- a/system-api/cm_system-current.txt
+++ b/system-api/cm_system-current.txt
@@ -442,6 +442,8 @@
     field public static final java.lang.String MODIFY_SOUND_SETTINGS = "cyanogenmod.permission.MODIFY_SOUND_SETTINGS";
     field public static final java.lang.String PUBLISH_CUSTOM_TILE = "cyanogenmod.permission.PUBLISH_CUSTOM_TILE";
     field public static final java.lang.String READ_MSIM_PHONE_STATE = "cyanogenmod.permission.READ_MSIM_PHONE_STATE";
+    field public static final java.lang.String WRITE_SECURE_SETTINGS = "cyanogenmod.permission.WRITE_SECURE_SETTINGS";
+    field public static final java.lang.String WRITE_SETTINGS = "cyanogenmod.permission.WRITE_SETTINGS";
   }
 
   public final class R {
@@ -566,3 +568,68 @@
 
 }
 
+package cyanogenmod.providers {
+
+  public final class CMSettings {
+    ctor public CMSettings();
+    field public static final java.lang.String AUTHORITY = "cmsettings";
+  }
+
+  public static class CMSettings.CMSettingNotFoundException extends android.util.AndroidException {
+    ctor public CMSettings.CMSettingNotFoundException(java.lang.String);
+  }
+
+  public static final class CMSettings.Global extends android.provider.Settings.NameValueTable {
+    ctor public CMSettings.Global();
+    method public static float getFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static float getFloat(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static int getInt(android.content.ContentResolver, java.lang.String, int);
+    method public static int getInt(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static long getLong(android.content.ContentResolver, java.lang.String, long);
+    method public static long getLong(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static java.lang.String getString(android.content.ContentResolver, java.lang.String);
+    method public static boolean putFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
+    method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
+    method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+    field public static final android.net.Uri CONTENT_URI;
+    field public static final java.lang.String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_global_version";
+  }
+
+  public static final class CMSettings.Secure extends android.provider.Settings.NameValueTable {
+    ctor public CMSettings.Secure();
+    method public static float getFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static float getFloat(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static int getInt(android.content.ContentResolver, java.lang.String, int);
+    method public static int getInt(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static long getLong(android.content.ContentResolver, java.lang.String, long);
+    method public static long getLong(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static java.lang.String getString(android.content.ContentResolver, java.lang.String);
+    method public static boolean putFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
+    method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
+    method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+    field public static final android.net.Uri CONTENT_URI;
+    field public static final java.lang.String NAME_THEME_CONFIG = "name_theme_config";
+    field public static final java.lang.String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_secure_version";
+  }
+
+  public static final class CMSettings.System extends android.provider.Settings.NameValueTable {
+    ctor public CMSettings.System();
+    method public static float getFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static float getFloat(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static int getInt(android.content.ContentResolver, java.lang.String, int);
+    method public static int getInt(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static long getLong(android.content.ContentResolver, java.lang.String, long);
+    method public static long getLong(android.content.ContentResolver, java.lang.String) throws cyanogenmod.providers.CMSettings.CMSettingNotFoundException;
+    method public static java.lang.String getString(android.content.ContentResolver, java.lang.String);
+    method public static boolean putFloat(android.content.ContentResolver, java.lang.String, float);
+    method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
+    method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
+    method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+    field public static final android.net.Uri CONTENT_URI;
+    field public static final java.lang.String SYS_PROP_CM_SETTING_VERSION = "sys.cm_settings_system_version";
+  }
+
+}
+
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 720777c..b999e4d 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -6,6 +6,8 @@
 
     <uses-permission android:name="cyanogenmod.permission.PUBLISH_CUSTOM_TILE" />
     <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="cyanogenmod.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="cyanogenmod.permission.WRITE_SECURE_SETTINGS"/>
     <uses-permission android:name="cyanogenmod.permission.MODIFY_NETWORK_SETTINGS" />
     <uses-permission android:name="cyanogenmod.permission.MODIFY_SOUND_SETTINGS" />
     <uses-permission android:name="android.permission.REBOOT" />
diff --git a/tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java b/tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java
new file mode 100644
index 0000000..d179af8
--- /dev/null
+++ b/tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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 org.cyanogenmod.tests.providers;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import cyanogenmod.providers.CMSettings;
+
+public class CMSettingsTest extends AndroidTestCase{
+    private ContentResolver mContentResolver;
+
+    @Override
+    public void setUp() {
+        mContentResolver = getContext().getContentResolver();
+    }
+
+    @MediumTest
+    public void testPutAndGetSystemString() {
+        final String key = "key";
+
+        // put
+        final String expectedValue = "systemTestValue1";
+        boolean isPutSuccessful = CMSettings.System.putString(mContentResolver, key, expectedValue);
+        assertTrue(isPutSuccessful);
+
+        // get
+        String actualValue = CMSettings.System.getString(mContentResolver, key);
+        assertEquals(expectedValue, actualValue);
+
+        // replace
+        final String expectedReplaceValue = "systemTestValue2";
+        isPutSuccessful = CMSettings.System.putString(mContentResolver, key, expectedReplaceValue);
+        assertTrue(isPutSuccessful);
+
+        // get
+        actualValue = CMSettings.System.getString(mContentResolver, key);
+        assertEquals(expectedReplaceValue, actualValue);
+
+        // delete to clean up
+        int rowsAffected = mContentResolver.delete(CMSettings.System.CONTENT_URI, Settings.NameValueTable.NAME + " = ?",
+                new String[]{ key });
+        assertEquals(1, rowsAffected);
+    }
+
+    @MediumTest
+    public void testPutAndGetSecureString() {
+        final String key = "key";
+
+        // put
+        final String expectedValue = "secureTestValue1";
+        boolean isPutSuccessful = CMSettings.Secure.putString(mContentResolver, key, expectedValue);
+        assertTrue(isPutSuccessful);
+
+        // get
+        String actualValue = CMSettings.Secure.getString(mContentResolver, key);
+        assertEquals(expectedValue, actualValue);
+
+        // replace
+        final String expectedReplaceValue = "secureTestValue2";
+        isPutSuccessful = CMSettings.Secure.putString(mContentResolver, key, expectedReplaceValue);
+        assertTrue(isPutSuccessful);
+
+        // get
+        actualValue = CMSettings.Secure.getString(mContentResolver, key);
+        assertEquals(expectedReplaceValue, actualValue);
+
+        // delete to clean up
+        int rowsAffected = mContentResolver.delete(CMSettings.Secure.CONTENT_URI, Settings.NameValueTable.NAME + " = ?",
+                new String[]{ key });
+        assertEquals(1, rowsAffected);
+    }
+
+    @MediumTest
+    public void testPutAndGetGlobalString() {
+        final String key = "key";
+
+        // put
+        final String expectedValue = "globalTestValue1";
+        boolean isPutSuccessful = CMSettings.Global.putString(mContentResolver, key, expectedValue);
+        assertTrue(isPutSuccessful);
+
+        // get
+        String actualValue = CMSettings.Global.getString(mContentResolver, key);
+        assertEquals(expectedValue, actualValue);
+
+        // replace
+        final String expectedReplaceValue = "globalTestValue2";
+        isPutSuccessful = CMSettings.Global.putString(mContentResolver, key, expectedReplaceValue);
+        assertTrue(isPutSuccessful);
+
+        // get
+        actualValue = CMSettings.Global.getString(mContentResolver, key);
+        assertEquals(expectedReplaceValue, actualValue);
+
+        // delete to clean up
+        int rowsAffected = mContentResolver.delete(CMSettings.Global.CONTENT_URI, Settings.NameValueTable.NAME + " = ?",
+                new String[]{ key });
+        assertEquals(1, rowsAffected);
+    }
+
+    // TODO Add tests for other users
+}