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" /> + + + + + + use hardware framework Allows an app access to the CM hardware framework. + + modify CM system settings + Allows an app to modify CM system settings. + + + modify CM secure system settings + Allows an app to modify CM secure system settings. Not for use by normal apps. Custom tile listener 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 @@ + + + + + + + + + + + + + diff --git a/packages/CMSettingsProvider/res/drawable/icon.png b/packages/CMSettingsProvider/res/drawable/icon.png new file mode 100644 index 0000000..08ee50d Binary files /dev/null and b/packages/CMSettingsProvider/res/drawable/icon.png 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 @@ + + + + CMSettingsProvider + \ 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 mDbHelpers = new SparseArray(); + + 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 @@ + + + + + + + + + + + + + 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 mMap = new LinkedHashMap(); + + 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 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 mValues = new HashMap(); + 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 @@ + + 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 +}