aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk4
-rw-r--r--api/cm_current.txt67
-rw-r--r--cm/res/AndroidManifest.xml13
-rw-r--r--cm/res/res/values/strings.xml7
-rw-r--r--packages/CMSettingsProvider/Android.mk36
-rw-r--r--packages/CMSettingsProvider/AndroidManifest.xml42
-rw-r--r--packages/CMSettingsProvider/res/drawable/icon.pngbin0 -> 990 bytes
-rw-r--r--packages/CMSettingsProvider/res/values/strings.xml19
-rw-r--r--packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java126
-rw-r--r--packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java451
-rw-r--r--packages/CMSettingsProvider/tests/Android.mk34
-rw-r--r--packages/CMSettingsProvider/tests/AndroidManifest.xml30
-rw-r--r--packages/CMSettingsProvider/tests/src/org/cyanogenmod/cmsettings/tests/CMSettingsProviderTest.java159
-rw-r--r--src/java/cyanogenmod/providers/CMSettings.java1044
-rw-r--r--system-api/cm_system-current.txt67
-rw-r--r--tests/AndroidManifest.xml2
-rw-r--r--tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java118
17 files changed, 2217 insertions, 2 deletions
diff --git a/Android.mk b/Android.mk
index 1bebef6..fdc06f2 100644
--- a/Android.mk
+++ b/Android.mk
@@ -163,7 +163,7 @@ LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR:= build/tools/droiddoc/templates-sdk
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_MODULE := cm-system-api-stubs
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 @@ package cyanogenmod.platform {
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.profiles {
}
+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 @@ package cyanogenmod.platform {
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.profiles {
}
+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
+}