From 05d01294782115b652a0ef28e5cb35ab3a7ad642 Mon Sep 17 00:00:00 2001 From: Yvonne Wong Date: Thu, 27 Aug 2015 12:19:54 -0700 Subject: [PATCH] Add way to migrate CM specific settings to CMSettingsProvider issue-id: CYNGNOS-829 Change-Id: I08743ebf9ffd3846ae501ed41e396b1556dc41cf --- packages/CMSettingsProvider/Android.mk | 2 +- .../CMSettingsProvider/AndroidManifest.xml | 12 +- .../res/values/defaults.xml | 57 +++ .../cmsettings/CMDatabaseHelper.java | 173 ++++++- .../cmsettings/CMSettingsProvider.java | 444 +++++++++++++++- .../cmsettings/PreBootReceiver.java | 56 +++ .../tests/AndroidManifest.xml | 4 + .../tests/CMSettingsProviderTest.java | 222 +++++--- .../cyanogenmod/providers/CMSettings.java | 472 +++++++++++++++--- tests/AndroidManifest.xml | 1 + .../tests/providers/CMSettingsTest.java | 70 ++- 11 files changed, 1342 insertions(+), 171 deletions(-) create mode 100644 packages/CMSettingsProvider/res/values/defaults.xml create mode 100644 packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/PreBootReceiver.java diff --git a/packages/CMSettingsProvider/Android.mk b/packages/CMSettingsProvider/Android.mk index 1d60321..7994ea0 100644 --- a/packages/CMSettingsProvider/Android.mk +++ b/packages/CMSettingsProvider/Android.mk @@ -27,7 +27,7 @@ LOCAL_PACKAGE_NAME := CMSettingsProvider LOCAL_CERTIFICATE := platform LOCAL_PRIVILEGED_MODULE := true -LOCAL_JAVA_LIBRARIES := \ +LOCAL_STATIC_JAVA_LIBRARIES := \ org.cyanogenmod.platform.sdk include $(BUILD_PACKAGE) diff --git a/packages/CMSettingsProvider/AndroidManifest.xml b/packages/CMSettingsProvider/AndroidManifest.xml index b46fefc..201a6c9 100644 --- a/packages/CMSettingsProvider/AndroidManifest.xml +++ b/packages/CMSettingsProvider/AndroidManifest.xml @@ -31,12 +31,22 @@ android:allowClearUserData="false" android:enabled="true"> - + + + + + + + diff --git a/packages/CMSettingsProvider/res/values/defaults.xml b/packages/CMSettingsProvider/res/values/defaults.xml new file mode 100644 index 0000000..3b2d741 --- /dev/null +++ b/packages/CMSettingsProvider/res/values/defaults.xml @@ -0,0 +1,57 @@ + + + + + + + 0 + + + + + true + + + + + + + + + 0 + + + wifi,bt,cell,airplane,rotation,flashlight,location,cast,visualizer,hotspot,live_display + + + false + + + + + %1$s + + + 1 + + \ 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 index 85fbaa9..2016378 100644 --- a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java +++ b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java @@ -16,12 +16,16 @@ package org.cyanogenmod.cmsettings; +import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; import android.os.Environment; import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; +import cyanogenmod.providers.CMSettings; import java.io.File; @@ -34,7 +38,7 @@ public class CMDatabaseHelper extends SQLiteOpenHelper{ private static final boolean LOCAL_LOGV = false; private static final String DATABASE_NAME = "cmsettings.db"; - private static final int DATABASE_VERSION = 1; + private static final int DATABASE_VERSION = 2; static class CMTableNames { static final String TABLE_SYSTEM = "system"; @@ -50,6 +54,11 @@ public class CMDatabaseHelper extends SQLiteOpenHelper{ private static final String CREATE_INDEX_SQL_FORMAT = "CREATE INDEX %sIndex%d ON %s (name);"; + private static final String DROP_TABLE_SQL_FORMAT = "DROP TABLE IF EXISTS %s;"; + + private static final String DROP_INDEX_SQL_FORMAT = "DROP INDEX IF EXISTS %sIndex%d;"; + + private Context mContext; private int mUserHandle; /** @@ -77,11 +86,13 @@ public class CMDatabaseHelper extends SQLiteOpenHelper{ */ public CMDatabaseHelper(Context context, int userId) { super(context, dbNameForUser(userId), null, DATABASE_VERSION); + mContext = context; mUserHandle = userId; } /** - * Creates System, Secure, and Global tables in the specified {@link SQLiteDatabase} + * Creates System, Secure, and Global tables in the specified {@link SQLiteDatabase} and loads + * default values into the created tables. * @param db The database. */ @Override @@ -96,9 +107,11 @@ public class CMDatabaseHelper extends SQLiteOpenHelper{ createDbTable(db, CMTableNames.TABLE_GLOBAL); } + loadSettings(db); + db.setTransactionSuccessful(); - if (LOCAL_LOGV) Log.v(TAG, "Successfully created tables for cm settings db"); + if (LOCAL_LOGV) Log.d(TAG, "Successfully created tables for cm settings db"); } finally { db.endTransaction(); } @@ -106,11 +119,11 @@ public class CMDatabaseHelper extends SQLiteOpenHelper{ /** * Creates a table and index for the specified database and table name - * @param db - * @param tableName + * @param db The {@link SQLiteDatabase} to create the table and index in. + * @param tableName The name of the database table to create. */ private void createDbTable(SQLiteDatabase db, String tableName) { - if (LOCAL_LOGV) Log.v(TAG, "Creating table and index for: " + tableName); + if (LOCAL_LOGV) Log.d(TAG, "Creating table and index for: " + tableName); String createTableSql = String.format(CREATE_TABLE_SQL_FORMAT, tableName); db.execSQL(createTableSql); @@ -121,6 +134,154 @@ public class CMDatabaseHelper extends SQLiteOpenHelper{ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + int upgradeVersion = oldVersion; + if (upgradeVersion < 2) { + db.beginTransaction(); + try { + loadSettings(db); + + db.setTransactionSuccessful(); + + upgradeVersion = 2; + } finally { + db.endTransaction(); + } + } + + // *** Remember to update DATABASE_VERSION above! + + if (upgradeVersion < newVersion) { + Log.w(TAG, "Got stuck trying to upgrade db. Old version: " + oldVersion + + ", version stuck at: " + upgradeVersion + ", new version: " + + newVersion + ". Must wipe the cm settings provider."); + + dropDbTable(db, CMTableNames.TABLE_SYSTEM); + dropDbTable(db, CMTableNames.TABLE_SECURE); + + if (mUserHandle == UserHandle.USER_OWNER) { + dropDbTable(db, CMTableNames.TABLE_GLOBAL); + } + + onCreate(db); + } + } + + /** + * Drops the table and index for the specified database and table name + * @param db The {@link SQLiteDatabase} to drop the table and index in. + * @param tableName The name of the database table to drop. + */ + private void dropDbTable(SQLiteDatabase db, String tableName) { + if (LOCAL_LOGV) Log.d(TAG, "Dropping table and index for: " + tableName); + + String dropTableSql = String.format(DROP_TABLE_SQL_FORMAT, tableName); + db.execSQL(dropTableSql); + + String dropIndexSql = String.format(DROP_INDEX_SQL_FORMAT, tableName, 1); + db.execSQL(dropIndexSql); + } + + /** + * Loads default values for specific settings into the database. + * @param db The {@link SQLiteDatabase} to insert into. + */ + private void loadSettings(SQLiteDatabase db) { + // System + loadIntegerSetting(db, CMTableNames.TABLE_SYSTEM, CMSettings.System.QS_QUICK_PULLDOWN, + R.integer.def_qs_quick_pulldown); + + // Secure + loadBooleanSetting(db, CMTableNames.TABLE_SECURE, CMSettings.Secure.ADVANCED_MODE, + R.bool.def_advanced_mode); + + loadStringSetting(db, CMTableNames.TABLE_SECURE, CMSettings.Secure.DEFAULT_THEME_COMPONENTS, + R.string.def_theme_components); + + loadStringSetting(db, CMTableNames.TABLE_SECURE, CMSettings.Secure.DEFAULT_THEME_PACKAGE, + R.string.def_theme_package); + + loadIntegerSetting(db, CMTableNames.TABLE_SECURE, CMSettings.Secure.DEV_FORCE_SHOW_NAVBAR, + R.integer.def_force_show_navbar); + + loadStringSetting(db, CMTableNames.TABLE_SECURE, CMSettings.Secure.QS_TILES, + R.string.def_qs_tiles); + + loadBooleanSetting(db, CMTableNames.TABLE_SECURE, CMSettings.Secure.STATS_COLLECTION, + R.bool.def_stats_collection); + + // Global + if (mUserHandle == UserHandle.USER_OWNER) { + loadSettingsForTable(db, CMTableNames.TABLE_GLOBAL, CMSettings.Global.DEVICE_NAME, + getDefaultDeviceName()); + + loadIntegerSetting(db, CMTableNames.TABLE_GLOBAL, + CMSettings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, + R.integer.def_heads_up_enabled); + } + } + + /** + * Loads a string resource into a database table. If a conflict occurs, that value is not + * inserted into the database table. + * @param db The {@link SQLiteDatabase} to insert into. + * @param tableName The name of the table to insert into. + * @param name The name of the value to insert into the table. + * @param resId The name of the string resource. + */ + private void loadStringSetting(SQLiteDatabase db, String tableName, String name, int resId) { + loadSettingsForTable(db, tableName, name, mContext.getResources().getString(resId)); + } + + /** + * Loads a boolean resource into a database table. If a conflict occurs, that value is not + * inserted into the database table. + * @param db The {@link SQLiteDatabase} to insert into. + * @param tableName The name of the table to insert into. + * @param name The name of the value to insert into the table. + * @param resId The name of the boolean resource. + */ + private void loadBooleanSetting(SQLiteDatabase db, String tableName, String name, int resId) { + loadSettingsForTable(db, tableName, name, + mContext.getResources().getBoolean(resId) ? "1" : "0"); + } + + /** + * Loads an integer resource into a database table. If a conflict occurs, that value is not + * inserted into the database table. + * @param db The {@link SQLiteDatabase} to insert into. + * @param tableName The name of the table to insert into. + * @param name The name of the value to insert into the table. + * @param resId The name of the integer resource. + */ + private void loadIntegerSetting(SQLiteDatabase db, String tableName, String name, int resId) { + loadSettingsForTable(db, tableName, name, + Integer.toString(mContext.getResources().getInteger(resId))); + } + + /** + * Loads a name/value pair into a database table. If a conflict occurs, that value is not + * inserted into the database table. + * @param db The {@link SQLiteDatabase} to insert into. + * @param tableName The name of the table to insert into. + * @param name The name of the value to insert into the table. + * @param value The value to insert into the table. + */ + private void loadSettingsForTable(SQLiteDatabase db, String tableName, String name, + String value) { + if (LOCAL_LOGV) Log.d(TAG, "Loading key: " + name + ", value: " + value); + + ContentValues contentValues = new ContentValues(); + contentValues.put(Settings.NameValueTable.NAME, name); + contentValues.put(Settings.NameValueTable.VALUE, value); + + db.insertWithOnConflict(tableName, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE); + } + + /** + * @return Gets the default device name + */ + private String getDefaultDeviceName() { + return mContext.getResources().getString(R.string.def_device_name, Build.MODEL); } } diff --git a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java index a9a96fb..5b1453c 100644 --- a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java +++ b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java @@ -16,26 +16,41 @@ package org.cyanogenmod.cmsettings; +import android.app.ActivityManager; +import android.content.BroadcastReceiver; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.UriMatcher; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.content.res.Configuration; import android.database.AbstractCursor; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Binder; +import android.os.Bundle; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import cyanogenmod.providers.CMSettings; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + /** * The CMSettingsProvider serves as a {@link ContentProvider} for CM specific settings */ @@ -45,6 +60,10 @@ public class CMSettingsProvider extends ContentProvider { private static final boolean USER_CHECK_THROWS = true; + private static final String PREF_HAS_MIGRATED_CM_SETTINGS = "has_migrated_cm_settings"; + + private static final Bundle NULL_SETTING = Bundle.forPair("value", null); + // Each defined user has their own settings protected final SparseArray mDbHelpers = new SparseArray(); @@ -52,6 +71,13 @@ public class CMSettingsProvider extends ContentProvider { private static final int SECURE = 2; private static final int GLOBAL = 3; + private static final int SYSTEM_ITEM_NAME = 4; + private static final int SECURE_ITEM_NAME = 5; + private static final int GLOBAL_ITEM_NAME = 6; + + private static final String ITEM_MATCHER = "/*"; + private static final String NAME_SELECTION = Settings.NameValueTable.NAME + " = ?"; + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { @@ -61,11 +87,17 @@ public class CMSettingsProvider extends ContentProvider { SECURE); sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_GLOBAL, GLOBAL); - // TODO add other paths for getting specific items + sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_SYSTEM + + ITEM_MATCHER, SYSTEM_ITEM_NAME); + sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_SECURE + + ITEM_MATCHER, SECURE_ITEM_NAME); + sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_GLOBAL + + ITEM_MATCHER, GLOBAL_ITEM_NAME); } private UserManager mUserManager; private Uri.Builder mUriBuilder; + private SharedPreferences mSharedPrefs; @Override public boolean onCreate() { @@ -79,36 +111,349 @@ public class CMSettingsProvider extends ContentProvider { mUriBuilder.scheme(ContentResolver.SCHEME_CONTENT); mUriBuilder.authority(CMSettings.AUTHORITY); - // TODO Add migration for cm settings + mSharedPrefs = getContext().getSharedPreferences(TAG, Context.MODE_PRIVATE); + + IntentFilter userFilter = new IntentFilter(); + userFilter.addAction(Intent.ACTION_USER_REMOVED); + getContext().registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_OWNER); + String action = intent.getAction(); + + if (LOCAL_LOGV) Log.d(TAG, "Received intent: " + action + " for user: " + userId); + + if (action.equals(Intent.ACTION_USER_REMOVED)) { + onUserRemoved(userId); + } + } + }, userFilter); return true; } + // region Migration Methods + + /** + * Migrates CM settings for all existing users if this has not been run before. + */ + private void migrateCMSettingsForExistingUsersIfNeeded() { + boolean hasMigratedCMSettings = mSharedPrefs.getBoolean(PREF_HAS_MIGRATED_CM_SETTINGS, + false); + + if (!hasMigratedCMSettings) { + long startTime = System.currentTimeMillis(); + + for (UserInfo user : mUserManager.getUsers()) { + migrateCMSettingsForUser(user.id); + } + + mSharedPrefs.edit().putBoolean(PREF_HAS_MIGRATED_CM_SETTINGS, true).commit(); + + // TODO: Add this as part of a boot message to the UI + long timeDiffMillis = System.currentTimeMillis() - startTime; + if (LOCAL_LOGV) Log.d(TAG, "Migration finished in " + timeDiffMillis + " milliseconds"); + } + } + + /** + * Migrates CM settings for a specific user. + * @param userId The id of the user to run CM settings migration for. + */ + private void migrateCMSettingsForUser(int userId) { + synchronized (this) { + if (LOCAL_LOGV) Log.d(TAG, "CM settings will be migrated for user id: " + userId); + + // Migrate system settings + HashMap systemToCmSettingsMap = new HashMap(); + systemToCmSettingsMap.put(Settings.System.QS_QUICK_PULLDOWN, + CMSettings.System.QS_QUICK_PULLDOWN); + + int rowsMigrated = migrateCMSettingsForTable(userId, + CMDatabaseHelper.CMTableNames.TABLE_SYSTEM, systemToCmSettingsMap); + if (LOCAL_LOGV) Log.d(TAG, "Migrated " + rowsMigrated + " to CM system table"); + + // Migrate secure settings + HashMap secureToCmSettingsMap = new HashMap(); + secureToCmSettingsMap.put(Settings.Secure.ADVANCED_MODE, + CMSettings.Secure.ADVANCED_MODE); + secureToCmSettingsMap.put(Settings.Secure.BUTTON_BACKLIGHT_TIMEOUT, + CMSettings.Secure.BUTTON_BACKLIGHT_TIMEOUT); + secureToCmSettingsMap.put(Settings.Secure.BUTTON_BRIGHTNESS, + CMSettings.Secure.BUTTON_BRIGHTNESS); + secureToCmSettingsMap.put(Settings.Secure.DEFAULT_THEME_COMPONENTS, + CMSettings.Secure.DEFAULT_THEME_COMPONENTS); + secureToCmSettingsMap.put(Settings.Secure.DEFAULT_THEME_PACKAGE, + CMSettings.Secure.DEFAULT_THEME_PACKAGE); + secureToCmSettingsMap.put(Settings.Secure.DEV_FORCE_SHOW_NAVBAR, + CMSettings.Secure.DEV_FORCE_SHOW_NAVBAR); + secureToCmSettingsMap.put( + Configuration.THEME_PKG_CONFIGURATION_PERSISTENCE_PROPERTY, + CMSettings.Secure.NAME_THEME_CONFIG); + secureToCmSettingsMap.put(Settings.Secure.KEYBOARD_BRIGHTNESS, + CMSettings.Secure.KEYBOARD_BRIGHTNESS); + secureToCmSettingsMap.put(Settings.Secure.POWER_MENU_ACTIONS, + CMSettings.Secure.POWER_MENU_ACTIONS); + secureToCmSettingsMap.put(Settings.Secure.STATS_COLLECTION, + CMSettings.Secure.STATS_COLLECTION); + secureToCmSettingsMap.put(Settings.Secure.QS_SHOW_BRIGHTNESS_SLIDER, + CMSettings.Secure.QS_SHOW_BRIGHTNESS_SLIDER); + secureToCmSettingsMap.put(Settings.Secure.QS_TILES, + CMSettings.Secure.QS_TILES); + secureToCmSettingsMap.put(Settings.Secure.QS_USE_MAIN_TILES, + CMSettings.Secure.QS_USE_MAIN_TILES); + secureToCmSettingsMap.put(Settings.Secure.VOLUME_LINK_NOTIFICATION, + CMSettings.Secure.VOLUME_LINK_NOTIFICATION); + + int navRingTargetsLength = Settings.Secure.NAVIGATION_RING_TARGETS.length; + int cmNavRingTargetsLength = CMSettings.Secure.NAVIGATION_RING_TARGETS.length; + int minNavRingTargetsLength = navRingTargetsLength <= cmNavRingTargetsLength ? + navRingTargetsLength : cmNavRingTargetsLength; + + for (int i = 0; i < minNavRingTargetsLength; i++) { + systemToCmSettingsMap.put(Settings.Secure.NAVIGATION_RING_TARGETS[i], + CMSettings.Secure.NAVIGATION_RING_TARGETS[i]); + } + + rowsMigrated = migrateCMSettingsForTable(userId, + CMDatabaseHelper.CMTableNames.TABLE_SECURE, secureToCmSettingsMap); + if (LOCAL_LOGV) Log.d(TAG, "Migrated " + rowsMigrated + " to CM secure table"); + + // Migrate global settings + if (userId == UserHandle.USER_OWNER) { + HashMap globalToCmSettingsMap = new HashMap(); + globalToCmSettingsMap.put(Settings.Global.DEVICE_NAME, + CMSettings.Global.DEVICE_NAME); + globalToCmSettingsMap.put(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, + CMSettings.Global.HEADS_UP_NOTIFICATIONS_ENABLED); + + rowsMigrated = migrateCMSettingsForTable(userId, + CMDatabaseHelper.CMTableNames.TABLE_GLOBAL, globalToCmSettingsMap); + if (LOCAL_LOGV) Log.d(TAG, "Migrated " + rowsMigrated + " to CM global table"); + } + } + } + + /** + * Migrates CM settings for a specific table and user id. + * @param userId The id of the user to run CM settings migration for. + * @param tableName The name of the table to run CM settings migration on. + * @param settingsMap A mapping between key names in {@link Settings} and {@link CMSettings} + * @return Number of rows migrated. + */ + private int migrateCMSettingsForTable(int userId, String tableName, HashMap settingsMap) { + ContentResolver contentResolver = getContext().getContentResolver(); + Set> entrySet = settingsMap.entrySet(); + ContentValues[] contentValues = new ContentValues[settingsMap.size()]; + + int migrateSettingsCount = 0; + for (Map.Entry keyPair : entrySet) { + String settingsKey = keyPair.getKey(); + String cmSettingsKey = keyPair.getValue(); + String settingsValue = null; + + if (tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_SYSTEM)) { + settingsValue = Settings.System.getStringForUser(contentResolver, settingsKey, + userId); + } + else if (tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_SECURE)) { + settingsValue = Settings.Secure.getStringForUser(contentResolver, settingsKey, + userId); + } + else if (tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_GLOBAL)) { + settingsValue = Settings.Global.getStringForUser(contentResolver, settingsKey, + userId); + } + + if (LOCAL_LOGV) Log.d(TAG, "Table: " + tableName + ", Key: " + settingsKey + ", Value: " + + settingsValue); + + ContentValues contentValue = new ContentValues(); + contentValue.put(Settings.NameValueTable.NAME, cmSettingsKey); + contentValue.put(Settings.NameValueTable.VALUE, settingsValue); + contentValues[migrateSettingsCount++] = contentValue; + } + + int rowsInserted = 0; + if (contentValues.length > 0) { + Uri uri = mUriBuilder.build(); + uri = uri.buildUpon().appendPath(tableName).build(); + rowsInserted = bulkInsertForUser(userId, uri, contentValues); + } + + return rowsInserted; + } + + /** + * Performs cleanup for the removed user. + * @param userId The id of the user that is removed. + */ + private void onUserRemoved(int userId) { + synchronized (this) { + // the db file itself will be deleted automatically, but we need to tear down + // our helpers and other internal bookkeeping. + + mDbHelpers.delete(userId); + + if (LOCAL_LOGV) Log.d(TAG, "User " + userId + " is removed"); + } + } + + // endregion Migration Methods + + // region Content Provider Methods + + @Override + public Bundle call(String method, String request, Bundle args) { + if (LOCAL_LOGV) Log.d(TAG, "Call method: " + method); + + int callingUserId = UserHandle.getCallingUserId(); + if (args != null) { + int reqUser = args.getInt(CMSettings.CALL_METHOD_USER_KEY, callingUserId); + if (reqUser != callingUserId) { + callingUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), + Binder.getCallingUid(), reqUser, false, true, + "get/set setting for user", null); + if (LOCAL_LOGV) Log.v(TAG, " access setting for user " + callingUserId); + } + } + + // Migrate methods + if (CMSettings.CALL_METHOD_MIGRATE_SETTINGS.equals(method)) { + migrateCMSettingsForExistingUsersIfNeeded(); + + return null; + } else if (CMSettings.CALL_METHOD_MIGRATE_SETTINGS_FOR_USER.equals(method)) { + migrateCMSettingsForUser(callingUserId); + + return null; + } + + // Get methods + if (CMSettings.CALL_METHOD_GET_SYSTEM.equals(method)) { + return lookupSingleValue(callingUserId, CMSettings.System.CONTENT_URI, request); + } + else if (CMSettings.CALL_METHOD_GET_SECURE.equals(method)) { + return lookupSingleValue(callingUserId, CMSettings.Secure.CONTENT_URI, request); + } + else if (CMSettings.CALL_METHOD_GET_GLOBAL.equals(method)) { + return lookupSingleValue(callingUserId, CMSettings.Global.CONTENT_URI, request); + } + + // Put methods - new value is in the args bundle under the key named by + // the Settings.NameValueTable.VALUE static. + final String newValue = (args == null) + ? null : args.getString(Settings.NameValueTable.VALUE); + + // Framework can't do automatic permission checking for calls, so we need + // to do it here. + if (getContext().checkCallingOrSelfPermission( + cyanogenmod.platform.Manifest.permission.WRITE_SETTINGS) != + PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + String.format("Permission denial: writing to settings requires %1$s", + cyanogenmod.platform.Manifest.permission.WRITE_SETTINGS)); + } + + // Put methods + final ContentValues values = new ContentValues(); + values.put(Settings.NameValueTable.NAME, request); + values.put(Settings.NameValueTable.VALUE, newValue); + + if (CMSettings.CALL_METHOD_PUT_SYSTEM.equals(method)) { + insertForUser(callingUserId, CMSettings.System.CONTENT_URI, values); + } + else if (CMSettings.CALL_METHOD_PUT_SECURE.equals(method)) { + insertForUser(callingUserId, CMSettings.Secure.CONTENT_URI, values); + } + else if (CMSettings.CALL_METHOD_PUT_GLOBAL.equals(method)) { + insertForUser(callingUserId, CMSettings.Global.CONTENT_URI, values); + } + + return null; + } + + /** + * Looks up a single value for a specific user, uri, and key. + * @param userId The id of the user to perform the lookup for. + * @param uri The uri for which table to perform the lookup in. + * @param key The key to perform the lookup with. + * @return A single value stored in a {@link Bundle}. + */ + private Bundle lookupSingleValue(int userId, Uri uri, String key) { + Cursor cursor = null; + try { + cursor = queryForUser(userId, uri, new String[]{ Settings.NameValueTable.VALUE }, + Settings.NameValueTable.NAME + " = ?", new String[]{ key }, null); + + if (cursor != null && cursor.getCount() == 1) { + cursor.moveToFirst(); + String value = cursor.getString(0); + return value == null ? NULL_SETTING : Bundle.forPair(Settings.NameValueTable.VALUE, + value); + } + } catch (SQLiteException e) { + Log.w(TAG, "settings lookup error", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return NULL_SETTING; + } + @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return queryForUser(UserHandle.getCallingUserId(), uri, projection, selection, + selectionArgs, sortOrder); + } + + /** + * Performs a query for a specific user. + * @param userId The id of the user to perform the query for. + * @param uri The uri for which table to perform the query on. Optionally, the uri can end in + * the name of a specific element to query for. + * @param projection The columns that are returned in the {@link Cursor}. + * @param selection The column names that the selection criteria applies to. + * @param selectionArgs The column values that the selection criteria applies to. + * @param sortOrder The ordering of how the values should be returned in the {@link Cursor}. + * @return {@link Cursor} of the results from the query. + */ + private Cursor queryForUser(int userId, Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { if (uri == null) { throw new IllegalArgumentException("Uri cannot be null"); } - String tableName = getTableNameFromUri(uri); + int code = sUriMatcher.match(uri); + String tableName = getTableNameFromUriMatchCode(code); checkWritePermissions(tableName); - int callingUserId = UserHandle.getCallingUserId(); - CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, - callingUserId)); + CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, userId)); SQLiteDatabase db = dbHelper.getReadableDatabase(); SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(tableName); - Cursor returnCursor = queryBuilder.query(db, projection, selection, selectionArgs, null, - null, sortOrder); + Cursor returnCursor; + if (isItemUri(code)) { + // The uri is looking for an element with a specific name + returnCursor = queryBuilder.query(db, projection, NAME_SELECTION, + new String[] { uri.getLastPathSegment() }, null, null, sortOrder); + } else { + 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); + abstractCursor.setNotificationUri(getContext().getContentResolver(), uri, userId); } catch (ClassCastException e) { // details of the concrete Cursor implementation have changed and this code has // not been updated to match -- complain and fail hard. @@ -121,8 +466,14 @@ public class CMSettingsProvider extends ContentProvider { @Override public String getType(Uri uri) { - // TODO: Implement - return null; + int code = sUriMatcher.match(uri); + String tableName = getTableNameFromUriMatchCode(code); + + if (isItemUri(code)) { + return "vnd.android.cursor.item/" + tableName; + } else { + return "vnd.android.cursor.dir/" + tableName; + } } @Override @@ -166,8 +517,6 @@ public class CMSettingsProvider extends ContentProvider { if (rowId >= 0) { numRowsAffected++; - - if (LOCAL_LOGV) Log.d(TAG, tableName + " <- " + values); } else { return 0; } @@ -179,7 +528,6 @@ public class CMSettingsProvider extends ContentProvider { } if (numRowsAffected > 0) { - getContext().getContentResolver().notifyChange(uri, null); notifyChange(uri, tableName, userId); if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) inserted"); } @@ -189,6 +537,18 @@ public class CMSettingsProvider extends ContentProvider { @Override public Uri insert(Uri uri, ContentValues values) { + return insertForUser(UserHandle.getCallingUserId(), uri, values); + } + + /** + * Performs insert for a specific user. + * @param userId The user id to perform the insert for. + * @param uri The content:// URI of the insertion request. + * @param values A sets of column_name/value pairs to add to the database. + * This must not be {@code null}. + * @return + */ + private Uri insertForUser(int userId, Uri uri, ContentValues values) { if (uri == null) { throw new IllegalArgumentException("Uri cannot be null"); } @@ -200,17 +560,16 @@ public class CMSettingsProvider extends ContentProvider { String tableName = getTableNameFromUri(uri); checkWritePermissions(tableName); - int callingUserId = UserHandle.getCallingUserId(); - CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, - callingUserId)); + CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, userId)); SQLiteDatabase db = dbHelper.getWritableDatabase(); long rowId = db.insert(tableName, null, values); Uri returnUri = null; if (rowId > -1) { - returnUri = ContentUris.withAppendedId(uri, rowId); - notifyChange(returnUri, tableName, callingUserId); + String name = values.getAsString(Settings.NameValueTable.NAME); + returnUri = Uri.withAppendedPath(uri, name); + notifyChange(returnUri, tableName, userId); if (LOCAL_LOGV) Log.d(TAG, "Inserted row id: " + rowId + " into tableName: " + tableName); } @@ -235,8 +594,8 @@ public class CMSettingsProvider extends ContentProvider { int callingUserId = UserHandle.getCallingUserId(); CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, callingUserId)); - SQLiteDatabase db = dbHelper.getWritableDatabase(); + SQLiteDatabase db = dbHelper.getWritableDatabase(); numRowsAffected = db.delete(tableName, selection, selectionArgs); if (numRowsAffected > 0) { @@ -250,6 +609,11 @@ public class CMSettingsProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + // NOTE: update() is never called by the front-end CMSettings API, and updates that + // wind up affecting rows in Secure that are globally shared will not have the + // intended effect (the update will be invisible to the rest of the system). + // This should have no practical effect, since writes to the Secure db can only + // be done by system code, and that code should be using the correct API up front. if (uri == null) { throw new IllegalArgumentException("Uri cannot be null"); } @@ -269,13 +633,15 @@ public class CMSettingsProvider extends ContentProvider { int numRowsAffected = db.update(tableName, values, selection, selectionArgs); if (numRowsAffected > 0) { - getContext().getContentResolver().notifyChange(uri, null); + notifyChange(uri, tableName, callingUserId); if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) updated"); } return numRowsAffected; } + // endregion Content Provider Methods + /** * 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. @@ -356,6 +722,26 @@ public class CMSettingsProvider extends ContentProvider { } } + /** + * Returns whether the matched uri code refers to an item in a table + * @param code + * @return + */ + private boolean isItemUri(int code) { + switch (code) { + case SYSTEM: + case SECURE: + case GLOBAL: + return false; + case SYSTEM_ITEM_NAME: + case SECURE_ITEM_NAME: + case GLOBAL_ITEM_NAME: + return true; + default: + throw new IllegalArgumentException("Invalid uri match code: " + code); + } + } + /** * Utilizes an {@link UriMatcher} to check for a valid combination of scheme, authority, and * path and returns the corresponding table name @@ -365,15 +751,27 @@ public class CMSettingsProvider extends ContentProvider { private String getTableNameFromUri(Uri uri) { int code = sUriMatcher.match(uri); + return getTableNameFromUriMatchCode(code); + } + + /** + * Returns the corresponding table name for the matched uri code + * @param code + * @return + */ + private String getTableNameFromUriMatchCode(int code) { switch (code) { case SYSTEM: + case SYSTEM_ITEM_NAME: return CMDatabaseHelper.CMTableNames.TABLE_SYSTEM; case SECURE: + case SECURE_ITEM_NAME: return CMDatabaseHelper.CMTableNames.TABLE_SECURE; case GLOBAL: + case GLOBAL_ITEM_NAME: return CMDatabaseHelper.CMTableNames.TABLE_GLOBAL; default: - throw new IllegalArgumentException("Invalid uri: " + uri); + throw new IllegalArgumentException("Invalid uri match code: " + code); } } diff --git a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/PreBootReceiver.java b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/PreBootReceiver.java new file mode 100644 index 0000000..ef0a6f0 --- /dev/null +++ b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/PreBootReceiver.java @@ -0,0 +1,56 @@ +/** + * 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.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.IContentProvider; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.util.Log; +import cyanogenmod.providers.CMSettings; + +public class PreBootReceiver extends BroadcastReceiver{ + private static final String TAG = "CMSettingsReceiver"; + private static final boolean LOCAL_LOGV = false; + + @Override + public void onReceive(Context context, Intent intent) { + if (LOCAL_LOGV) { + Log.d(TAG, "Received pre boot intent. Attempting to migrate CM settings."); + } + + ContentResolver contentResolver = context.getContentResolver(); + IContentProvider contentProvider = contentResolver.acquireProvider( + CMSettings.AUTHORITY); + + try{ + contentProvider.call(contentResolver.getPackageName(), + CMSettings.CALL_METHOD_MIGRATE_SETTINGS, null, null); + + context.getPackageManager().setComponentEnabledSetting( + new ComponentName(context, getClass()), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to trigger settings migration due to RemoteException"); + } + } +} diff --git a/packages/CMSettingsProvider/tests/AndroidManifest.xml b/packages/CMSettingsProvider/tests/AndroidManifest.xml index e82a7d8..c564317 100644 --- a/packages/CMSettingsProvider/tests/AndroidManifest.xml +++ b/packages/CMSettingsProvider/tests/AndroidManifest.xml @@ -18,6 +18,10 @@ + + + + mMap = new LinkedHashMap(); + private static final LinkedHashMap sMap = new LinkedHashMap(); static { - mMap.put("testKey1", "value1"); - mMap.put("testKey2", "value2"); - mMap.put("testKey3", "value3"); + sMap.put("testKey1", "value1"); + sMap.put("testKey2", "value2"); + sMap.put("testKey3", "value3"); } - private static final String[] PROJECTIONS = new String[] { "name", "value" }; + private static final String[] PROJECTIONS = new String[] { Settings.NameValueTable.NAME, + Settings.NameValueTable.VALUE }; private ContentResolver mContentResolver; + private UserManager mUserManager; + private UserInfo mGuest; @Override public void setUp() { mContentResolver = mContext.getContentResolver(); + mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + } + + @Override + public void tearDown() { + if (mGuest != null) { + mUserManager.removeUser(mGuest.id); + } + } + + @MediumTest + public void testMigrateCMSettingsForOtherUser() { + // Make sure there's an owner + assertTrue(findUser(mUserManager, UserHandle.USER_OWNER)); + + mGuest = mUserManager.createGuest(mContext, "GuestUser1"); + assertNotNull(mGuest); + + testMigrateSettingsForUser(mGuest.id); + } + + private void testMigrateSettingsForUser(int userId) { + // Setup values in Settings + final String expectedPullDownValue = "testQuickPullDownValue"; + Settings.System.putStringForUser(mContentResolver, Settings.System.QS_QUICK_PULLDOWN, + expectedPullDownValue, userId); + + final int expectedKeyboardBrightness = 4; + Settings.Secure.putIntForUser(mContentResolver, Settings.Secure.KEYBOARD_BRIGHTNESS, + expectedKeyboardBrightness, userId); + + Bundle arg = new Bundle(); + arg.putInt(CMSettings.CALL_METHOD_USER_KEY, userId); + IContentProvider contentProvider = mContentResolver.acquireProvider( + CMSettings.AUTHORITY); + + try{ + // Trigger migrate settings for guest + contentProvider.call(mContentResolver.getPackageName(), + CMSettings.CALL_METHOD_MIGRATE_SETTINGS_FOR_USER, null, arg); + } catch (RemoteException ex) { + fail("Failed to trigger settings migration due to RemoteException"); + } + + // Check values + final String actualPullDownValue = CMSettings.System.getStringForUser(mContentResolver, + CMSettings.System.QS_QUICK_PULLDOWN, userId); + assertEquals(expectedPullDownValue, actualPullDownValue); + + final int actualKeyboardBrightness = CMSettings.Secure.getIntForUser(mContentResolver, + CMSettings.Secure.KEYBOARD_BRIGHTNESS, -1, userId); + assertEquals(expectedKeyboardBrightness, actualKeyboardBrightness); + } + + private boolean findUser(UserManager userManager, int userHandle) { + for (UserInfo user : userManager.getUsers()) { + if (user.id == userHandle) { + return true; + } + } + return false; } @MediumTest public void testBulkInsertSuccess() { - Log.d(TAG, "Starting bulk insert test"); - - ContentValues[] contentValues = new ContentValues[mMap.size()]; + ContentValues[] contentValues = new ContentValues[sMap.size()]; + String[] keyValues = new String[sMap.size()]; int count = 0; - for (Map.Entry kVPair : mMap.entrySet()) { + for (Map.Entry kVPair : sMap.entrySet()) { ContentValues contentValue = new ContentValues(); - contentValue.put(PROJECTIONS[0], kVPair.getKey()); - contentValue.put(PROJECTIONS[1], kVPair.getValue()); + + final String key = kVPair.getKey(); + contentValue.put(Settings.NameValueTable.NAME, key); + keyValues[count] = key; + + contentValue.put(Settings.NameValueTable.VALUE, 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"); + testBulkInsertForUri(CMSettings.System.CONTENT_URI, contentValues, keyValues); + testBulkInsertForUri(CMSettings.Secure.CONTENT_URI, contentValues, keyValues); + testBulkInsertForUri(CMSettings.Global.CONTENT_URI, contentValues, keyValues); } - private void testBulkInsertForUri(Uri uri, ContentValues[] contentValues) { + private void testBulkInsertForUri(Uri uri, ContentValues[] contentValues, String[] keyValues) { int rowsInserted = mContentResolver.bulkInsert(uri, contentValues); - assertEquals(mMap.size(), rowsInserted); + assertEquals(sMap.size(), rowsInserted); - Cursor queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); + final String placeholderSymbol = "?"; + String[] placeholders = new String[contentValues.length]; + for (int i = 0; i < placeholders.length; i++) { + placeholders[i] = placeholderSymbol; + } + + final String placeholdersString = TextUtils.join(",", placeholders); + + Cursor queryCursor = mContentResolver.query(uri, PROJECTIONS, + Settings.NameValueTable.NAME + " IN (" + placeholdersString + ")", keyValues, + null); + assertEquals(contentValues.length, queryCursor.getCount()); try { while (queryCursor.moveToNext()) { assertEquals(PROJECTIONS.length, queryCursor.getColumnCount()); String actualKey = queryCursor.getString(0); - assertTrue(mMap.containsKey(actualKey)); + assertTrue(sMap.containsKey(actualKey)); - assertEquals(mMap.get(actualKey), queryCursor.getString(1)); + assertEquals(sMap.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 }); + for (String key : sMap.keySet()) { + mContentResolver.delete(uri, Settings.NameValueTable.NAME + " = ?", + 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 key = "key"; String value1 = "value1"; String value2 = "value2"; // test insert ContentValues contentValue = new ContentValues(); - contentValue.put(PROJECTIONS[0], key1); - contentValue.put(PROJECTIONS[1], value1); + contentValue.put(Settings.NameValueTable.NAME, key); + contentValue.put(Settings.NameValueTable.VALUE, value1); - mContentResolver.insert(uri, contentValue); + Uri expectedUri = uri.withAppendedPath(uri, key); + Uri returnUri = mContentResolver.insert(uri, contentValue); + assertEquals(expectedUri, returnUri); + + Cursor queryCursor = null; + try { + // check insert + queryCursor = mContentResolver.query(uri, PROJECTIONS, Settings.NameValueTable.NAME + + " = ?", new String[]{ key }, null); + assertEquals(1, queryCursor.getCount()); - // check insert - Cursor queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); - assertEquals(1, queryCursor.getCount()); + assertExpectedKeyValuePair(queryCursor, key, value1); - queryCursor.moveToNext(); - assertEquals(PROJECTIONS.length, queryCursor.getColumnCount()); + // check insert with returned uri + queryCursor = mContentResolver.query(returnUri, PROJECTIONS, null, null, null); + assertEquals(1, queryCursor.getCount()); - String actualKey = queryCursor.getString(0); - assertEquals(key1, actualKey); - assertEquals(value1, queryCursor.getString(1)); + assertExpectedKeyValuePair(queryCursor, key, value1); - // test update - contentValue.clear(); - contentValue.put(PROJECTIONS[1], value2); + // test update + contentValue.clear(); + contentValue.put(Settings.NameValueTable.VALUE, value2); - int rowsAffected = mContentResolver.update(uri, contentValue, PROJECTIONS[0] + " = ?", - new String[]{key1}); - assertEquals(1, rowsAffected); + int rowsAffected = mContentResolver.update(uri, contentValue, + Settings.NameValueTable.NAME + " = ?", new String[]{ key }); + assertEquals(1, rowsAffected); - // check update - queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); - assertEquals(1, queryCursor.getCount()); + // check update + queryCursor = mContentResolver.query(uri, PROJECTIONS, Settings.NameValueTable.NAME + + " = ?", new String[]{ key }, null); + assertEquals(1, queryCursor.getCount()); - queryCursor.moveToNext(); - assertEquals(PROJECTIONS.length, queryCursor.getColumnCount()); + assertExpectedKeyValuePair(queryCursor, key, value2); - actualKey = queryCursor.getString(0); - assertEquals(key1, actualKey); - assertEquals(value2, queryCursor.getString(1)); + // test delete + rowsAffected = mContentResolver.delete(uri, Settings.NameValueTable.NAME + " = ?", + new String[]{ key }); + assertEquals(1, rowsAffected); - // test delete - rowsAffected = mContentResolver.delete(uri, PROJECTIONS[0] + " = ?", new String[]{key1}); - assertEquals(1, rowsAffected); + // check delete + queryCursor = mContentResolver.query(uri, PROJECTIONS, Settings.NameValueTable.NAME + + " = ?", new String[]{ key }, null); + assertEquals(0, queryCursor.getCount()); + } finally { + if (queryCursor != null) { + queryCursor.close(); + } + } + } - // check delete - queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); - assertEquals(0, queryCursor.getCount()); + private void assertExpectedKeyValuePair(Cursor cursor, String expectedKey, + String expectedValue) { + cursor.moveToNext(); + assertEquals(PROJECTIONS.length, cursor.getColumnCount()); + + String actualKey = cursor.getString(0); + assertEquals(expectedKey, actualKey); + assertEquals(expectedValue, cursor.getString(1)); } } diff --git a/src/java/cyanogenmod/providers/CMSettings.java b/src/java/cyanogenmod/providers/CMSettings.java index afeacc8..3178378 100644 --- a/src/java/cyanogenmod/providers/CMSettings.java +++ b/src/java/cyanogenmod/providers/CMSettings.java @@ -20,6 +20,7 @@ import android.content.ContentResolver; import android.content.IContentProvider; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; @@ -44,10 +45,57 @@ public final class CMSettings { } } + // region Call Methods + + /** + * @hide - User handle argument extra to the fast-path call()-based requests + */ + public static final String CALL_METHOD_USER_KEY = "_user"; + + /** + * @hide - Private call() method on SettingsProvider to read from 'system' table. + */ + public static final String CALL_METHOD_GET_SYSTEM = "GET_system"; + + /** + * @hide - Private call() method on SettingsProvider to read from 'secure' table. + */ + public static final String CALL_METHOD_GET_SECURE = "GET_secure"; + + /** + * @hide - Private call() method on SettingsProvider to read from 'global' table. + */ + public static final String CALL_METHOD_GET_GLOBAL = "GET_global"; + + /** + * @hide - Private call() method to write to 'system' table + */ + public static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; + + /** + * @hide - Private call() method to write to 'secure' table + */ + public static final String CALL_METHOD_PUT_SECURE = "PUT_secure"; + + /** + * @hide - Private call() method to write to 'global' table + */ + public static final String CALL_METHOD_PUT_GLOBAL= "PUT_global"; + + /** + * @hide - Private call() method on CMSettingsProvider to migrate CM settings + */ + public static final String CALL_METHOD_MIGRATE_SETTINGS = "migrate_settings"; + + /** + * @hide - Private call() method on CMSettingsProvider to migrate CM settings for a user + */ + public static final String CALL_METHOD_MIGRATE_SETTINGS_FOR_USER = "migrate_settings_for_user"; + + // endregion + // 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; @@ -62,9 +110,17 @@ public final class CMSettings { // Initially null; set lazily and held forever. Synchronized on 'this'. private IContentProvider mContentProvider = null; - public NameValueCache(String versionSystemProperty, Uri uri) { + // The method we'll call (or null, to not use) on the provider + // for the fast path of retrieving settings. + private final String mCallGetCommand; + private final String mCallSetCommand; + + public NameValueCache(String versionSystemProperty, Uri uri, + String getCommand, String setCommand) { mVersionSystemProperty = versionSystemProperty; mUri = uri; + mCallGetCommand = getCommand; + mCallSetCommand = setCommand; } private IContentProvider lazyGetProvider(ContentResolver cr) { @@ -79,7 +135,30 @@ public final class CMSettings { } /** - * Gets a a string value with the specified name from the name/value cache if possible. If + * Puts a string name/value pair into the content provider for the specified user. + * @param cr The content resolver to use. + * @param name The name of the key to put into the content provider. + * @param value The value to put into the content provider. + * @param userId The user id to use for the content provider. + * @return Whether the put was successful. + */ + public boolean putStringForUser(ContentResolver cr, String name, String value, + final int userId) { + try { + Bundle arg = new Bundle(); + arg.putString(Settings.NameValueTable.VALUE, value); + arg.putInt(CALL_METHOD_USER_KEY, userId); + IContentProvider cp = lazyGetProvider(cr); + cp.call(cr.getPackageName(), mCallSetCommand, name, arg); + } catch (RemoteException e) { + Log.w(TAG, "Can't set key " + name + " in " + mUri, e); + return false; + } + return true; + } + + /** + * Gets 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. @@ -90,6 +169,7 @@ public final class CMSettings { public String getStringForUser(ContentResolver cr, String name, final int userId) { final boolean isSelf = (userId == UserHandle.myUserId()); if (isSelf) { + if (LOCAL_LOGV) Log.d(TAG, "get setting for self"); long newValuesVersion = SystemProperties.getLong(mVersionSystemProperty, 0); // Our own user's settings data uses a client-side cache @@ -115,6 +195,40 @@ public final class CMSettings { IContentProvider cp = lazyGetProvider(cr); + // Try the fast path first, not using query(). If this + // fails (alternate Settings provider that doesn't support + // this interface?) then we fall back to the query/table + // interface. + if (mCallGetCommand != null) { + try { + Bundle args = null; + if (!isSelf) { + args = new Bundle(); + args.putInt(CALL_METHOD_USER_KEY, userId); + } + Bundle b = cp.call(cr.getPackageName(), mCallGetCommand, name, args); + if (b != null) { + String value = b.getPairValue(); + // Don't update our cache for reads of other users' data + if (isSelf) { + synchronized (this) { + mValues.put(name, value); + } + } else { + if (LOCAL_LOGV) Log.i(TAG, "call-query of user " + userId + + " by " + UserHandle.myUserId() + + " so not updating cache"); + } + return value; + } + // If the response Bundle is null, we fall through + // to the query interface below. + } catch (RemoteException e) { + // Not supported by the remote side? Fall through + // to query(). + } + } + Cursor c = null; try { c = cp.query(cr.getPackageName(), mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER, @@ -143,9 +257,8 @@ public final class CMSettings { } /** - * System settings, containing miscellaneous CM system preferences. This - * table holds simple name/value pairs. There are convenience - * functions for accessing individual settings entries. + * 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"); @@ -154,10 +267,22 @@ public final class CMSettings { private static final NameValueCache sNameValueCache = new NameValueCache( SYS_PROP_CM_SETTING_VERSION, - CONTENT_URI); + CONTENT_URI, + CALL_METHOD_GET_SYSTEM, + CALL_METHOD_PUT_SYSTEM); // region Methods + /** + * Construct the content URI for a particular name/value pair, useful for monitoring changes + * with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI + */ + public static Uri getUriFor(String name) { + return Settings.NameValueTable.getUriFor(CONTENT_URI, name); + } + /** * Look up a name in the database. * @param resolver to access the database with @@ -170,8 +295,8 @@ public final class CMSettings { /** @hide */ public static String getStringForUser(ContentResolver resolver, String name, - int userHandle) { - return sNameValueCache.getStringForUser(resolver, name, userHandle); + int userId) { + return sNameValueCache.getStringForUser(resolver, name, userId); } /** @@ -182,11 +307,17 @@ public final class CMSettings { * @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); + return putStringForUser(resolver, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putStringForUser(ContentResolver resolver, String name, String value, + int userId) { + return sNameValueCache.putStringForUser(resolver, name, value, userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -200,7 +331,12 @@ public final class CMSettings { * or not a valid integer. */ public static int getInt(ContentResolver cr, String name, int def) { - String v = getString(cr, name); + return getIntForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static int getIntForUser(ContentResolver cr, String name, int def, int userId) { + String v = getStringForUser(cr, name, userId); try { return v != null ? Integer.parseInt(v) : def; } catch (NumberFormatException e) { @@ -209,7 +345,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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. @@ -228,7 +364,13 @@ public final class CMSettings { */ public static int getInt(ContentResolver cr, String name) throws CMSettingNotFoundException { - String v = getString(cr, name); + return getIntForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static int getIntForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String v = getStringForUser(cr, name, userId); try { return Integer.parseInt(v); } catch (NumberFormatException e) { @@ -250,11 +392,17 @@ public final class CMSettings { * @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)); + return putIntForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putIntForUser(ContentResolver cr, String name, int value, + int userId) { + return putStringForUser(cr, name, Integer.toString(value), userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -268,7 +416,13 @@ public final class CMSettings { * or not a valid {@code long}. */ public static long getLong(ContentResolver cr, String name, long def) { - String valString = getString(cr, name); + return getLongForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static long getLongForUser(ContentResolver cr, String name, long def, + int userId) { + String valString = getStringForUser(cr, name, userId); long value; try { value = valString != null ? Long.parseLong(valString) : def; @@ -279,7 +433,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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. @@ -297,7 +451,13 @@ public final class CMSettings { */ public static long getLong(ContentResolver cr, String name) throws CMSettingNotFoundException { - String valString = getString(cr, name); + return getLongForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static long getLongForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String valString = getStringForUser(cr, name, userId); try { return Long.parseLong(valString); } catch (NumberFormatException e) { @@ -306,7 +466,7 @@ public final class CMSettings { } /** - * Convenience function for updating a secure settings value as a long + * Convenience function for updating a single 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 @@ -319,11 +479,17 @@ public final class CMSettings { * @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)); + return putLongForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putLongForUser(ContentResolver cr, String name, long value, + int userId) { + return putStringForUser(cr, name, Long.toString(value), userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -337,7 +503,13 @@ public final class CMSettings { * or not a valid float. */ public static float getFloat(ContentResolver cr, String name, float def) { - String v = getString(cr, name); + return getFloatForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static float getFloatForUser(ContentResolver cr, String name, float def, + int userId) { + String v = getStringForUser(cr, name, userId); try { return v != null ? Float.parseFloat(v) : def; } catch (NumberFormatException e) { @@ -346,7 +518,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single system 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. @@ -365,7 +537,13 @@ public final class CMSettings { */ public static float getFloat(ContentResolver cr, String name) throws CMSettingNotFoundException { - String v = getString(cr, name); + return getFloatForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static float getFloatForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String v = getStringForUser(cr, name, userId); if (v == null) { throw new CMSettingNotFoundException(name); } @@ -390,7 +568,13 @@ public final class CMSettings { * @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)); + return putFloatForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putFloatForUser(ContentResolver cr, String name, float value, + int userId) { + return putStringForUser(cr, name, Float.toString(value), userId); } // endregion @@ -408,8 +592,8 @@ public final class CMSettings { } /** - * Secure settings, containing miscellaneous CM secure preferences. This - * table holds simple name/value pairs. There are convenience + * 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 { @@ -419,7 +603,21 @@ public final class CMSettings { private static final NameValueCache sNameValueCache = new NameValueCache( SYS_PROP_CM_SETTING_VERSION, - CONTENT_URI); + CONTENT_URI, + CALL_METHOD_GET_SECURE, + CALL_METHOD_PUT_SECURE); + + // region Methods + + /** + * Construct the content URI for a particular name/value pair, useful for monitoring changes + * with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI + */ + public static Uri getUriFor(String name) { + return Settings.NameValueTable.getUriFor(CONTENT_URI, name); + } /** * Look up a name in the database. @@ -433,8 +631,8 @@ public final class CMSettings { /** @hide */ public static String getStringForUser(ContentResolver resolver, String name, - int userHandle) { - return sNameValueCache.getStringForUser(resolver, name, userHandle); + int userId) { + return sNameValueCache.getStringForUser(resolver, name, userId); } /** @@ -445,11 +643,17 @@ public final class CMSettings { * @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); + return putStringForUser(resolver, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putStringForUser(ContentResolver resolver, String name, String value, + int userId) { + return sNameValueCache.putStringForUser(resolver, name, value, userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -463,7 +667,12 @@ public final class CMSettings { * or not a valid integer. */ public static int getInt(ContentResolver cr, String name, int def) { - String v = getString(cr, name); + return getIntForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static int getIntForUser(ContentResolver cr, String name, int def, int userId) { + String v = getStringForUser(cr, name, userId); try { return v != null ? Integer.parseInt(v) : def; } catch (NumberFormatException e) { @@ -472,7 +681,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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. @@ -491,7 +700,13 @@ public final class CMSettings { */ public static int getInt(ContentResolver cr, String name) throws CMSettingNotFoundException { - String v = getString(cr, name); + return getIntForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static int getIntForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String v = getStringForUser(cr, name, userId); try { return Integer.parseInt(v); } catch (NumberFormatException e) { @@ -513,11 +728,17 @@ public final class CMSettings { * @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)); + return putIntForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putIntForUser(ContentResolver cr, String name, int value, + int userId) { + return putStringForUser(cr, name, Integer.toString(value), userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -531,7 +752,13 @@ public final class CMSettings { * or not a valid {@code long}. */ public static long getLong(ContentResolver cr, String name, long def) { - String valString = getString(cr, name); + return getLongForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static long getLongForUser(ContentResolver cr, String name, long def, + int userId) { + String valString = getStringForUser(cr, name, userId); long value; try { value = valString != null ? Long.parseLong(valString) : def; @@ -542,7 +769,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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. @@ -560,7 +787,13 @@ public final class CMSettings { */ public static long getLong(ContentResolver cr, String name) throws CMSettingNotFoundException { - String valString = getString(cr, name); + return getLongForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static long getLongForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String valString = getStringForUser(cr, name, userId); try { return Long.parseLong(valString); } catch (NumberFormatException e) { @@ -569,7 +802,7 @@ public final class CMSettings { } /** - * Convenience function for updating a secure settings value as a long + * Convenience function for updating a single 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 @@ -582,11 +815,17 @@ public final class CMSettings { * @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)); + return putLongForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putLongForUser(ContentResolver cr, String name, long value, + int userId) { + return putStringForUser(cr, name, Long.toString(value), userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -600,7 +839,13 @@ public final class CMSettings { * or not a valid float. */ public static float getFloat(ContentResolver cr, String name, float def) { - String v = getString(cr, name); + return getFloatForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static float getFloatForUser(ContentResolver cr, String name, float def, + int userId) { + String v = getStringForUser(cr, name, userId); try { return v != null ? Float.parseFloat(v) : def; } catch (NumberFormatException e) { @@ -609,7 +854,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single system 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. @@ -628,7 +873,13 @@ public final class CMSettings { */ public static float getFloat(ContentResolver cr, String name) throws CMSettingNotFoundException { - String v = getString(cr, name); + return getFloatForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static float getFloatForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String v = getStringForUser(cr, name, userId); if (v == null) { throw new CMSettingNotFoundException(name); } @@ -653,7 +904,13 @@ public final class CMSettings { * @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)); + return putFloatForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putFloatForUser(ContentResolver cr, String name, float value, + int userId) { + return putStringForUser(cr, name, Float.toString(value), userId); } // endregion @@ -770,8 +1027,8 @@ public final class CMSettings { } /** - * Global settings, containing miscellaneous CM global preferences. This - * table holds simple name/value pairs. There are convenience + * 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 { @@ -781,10 +1038,22 @@ public final class CMSettings { private static final NameValueCache sNameValueCache = new NameValueCache( SYS_PROP_CM_SETTING_VERSION, - CONTENT_URI); + CONTENT_URI, + CALL_METHOD_GET_GLOBAL, + CALL_METHOD_PUT_GLOBAL); // region Methods + /** + * Construct the content URI for a particular name/value pair, useful for monitoring changes + * with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI + */ + public static Uri getUriFor(String name) { + return Settings.NameValueTable.getUriFor(CONTENT_URI, name); + } + /** * Look up a name in the database. * @param resolver to access the database with @@ -797,8 +1066,8 @@ public final class CMSettings { /** @hide */ public static String getStringForUser(ContentResolver resolver, String name, - int userHandle) { - return sNameValueCache.getStringForUser(resolver, name, userHandle); + int userId) { + return sNameValueCache.getStringForUser(resolver, name, userId); } /** @@ -809,11 +1078,17 @@ public final class CMSettings { * @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); + return putStringForUser(resolver, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putStringForUser(ContentResolver resolver, String name, String value, + int userId) { + return sNameValueCache.putStringForUser(resolver, name, value, userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -827,7 +1102,12 @@ public final class CMSettings { * or not a valid integer. */ public static int getInt(ContentResolver cr, String name, int def) { - String v = getString(cr, name); + return getIntForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static int getIntForUser(ContentResolver cr, String name, int def, int userId) { + String v = getStringForUser(cr, name, userId); try { return v != null ? Integer.parseInt(v) : def; } catch (NumberFormatException e) { @@ -836,7 +1116,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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. @@ -855,7 +1135,13 @@ public final class CMSettings { */ public static int getInt(ContentResolver cr, String name) throws CMSettingNotFoundException { - String v = getString(cr, name); + return getIntForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static int getIntForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String v = getStringForUser(cr, name, userId); try { return Integer.parseInt(v); } catch (NumberFormatException e) { @@ -877,11 +1163,17 @@ public final class CMSettings { * @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)); + return putIntForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putIntForUser(ContentResolver cr, String name, int value, + int userId) { + return putStringForUser(cr, name, Integer.toString(value), userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -895,7 +1187,13 @@ public final class CMSettings { * or not a valid {@code long}. */ public static long getLong(ContentResolver cr, String name, long def) { - String valString = getString(cr, name); + return getLongForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static long getLongForUser(ContentResolver cr, String name, long def, + int userId) { + String valString = getStringForUser(cr, name, userId); long value; try { value = valString != null ? Long.parseLong(valString) : def; @@ -906,7 +1204,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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. @@ -924,7 +1222,13 @@ public final class CMSettings { */ public static long getLong(ContentResolver cr, String name) throws CMSettingNotFoundException { - String valString = getString(cr, name); + return getLongForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static long getLongForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String valString = getStringForUser(cr, name, userId); try { return Long.parseLong(valString); } catch (NumberFormatException e) { @@ -933,7 +1237,7 @@ public final class CMSettings { } /** - * Convenience function for updating a secure settings value as a long + * Convenience function for updating a single 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 @@ -946,11 +1250,17 @@ public final class CMSettings { * @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)); + return putLongForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putLongForUser(ContentResolver cr, String name, long value, + int userId) { + return putStringForUser(cr, name, Long.toString(value), userId); } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single 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 @@ -964,7 +1274,13 @@ public final class CMSettings { * or not a valid float. */ public static float getFloat(ContentResolver cr, String name, float def) { - String v = getString(cr, name); + return getFloatForUser(cr, name, def, UserHandle.myUserId()); + } + + /** @hide */ + public static float getFloatForUser(ContentResolver cr, String name, float def, + int userId) { + String v = getStringForUser(cr, name, userId); try { return v != null ? Float.parseFloat(v) : def; } catch (NumberFormatException e) { @@ -973,7 +1289,7 @@ public final class CMSettings { } /** - * Convenience function for retrieving a single secure settings value + * Convenience function for retrieving a single system 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. @@ -992,7 +1308,13 @@ public final class CMSettings { */ public static float getFloat(ContentResolver cr, String name) throws CMSettingNotFoundException { - String v = getString(cr, name); + return getFloatForUser(cr, name, UserHandle.myUserId()); + } + + /** @hide */ + public static float getFloatForUser(ContentResolver cr, String name, int userId) + throws CMSettingNotFoundException { + String v = getStringForUser(cr, name, userId); if (v == null) { throw new CMSettingNotFoundException(name); } @@ -1017,7 +1339,13 @@ public final class CMSettings { * @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)); + return putFloatForUser(cr, name, value, UserHandle.myUserId()); + } + + /** @hide */ + public static boolean putFloatForUser(ContentResolver cr, String name, float value, + int userId) { + return putStringForUser(cr, name, Float.toString(value), userId); } // endregion @@ -1032,7 +1360,7 @@ public final class CMSettings { public static final String DEVICE_NAME = "device_name"; /** - * Defines global heads up toggle. One of HEADS_UP_OFF, HEADS_UP_ON. + * Defines global heads up toggle. One of HEADS_UP_OFF, HEADS_UP_ON. * * @hide */ diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index 5657441..c345576 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -10,6 +10,7 @@ + diff --git a/tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java b/tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java index d179af8..77299db 100644 --- a/tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java +++ b/tests/src/org/cyanogenmod/tests/providers/CMSettingsTest.java @@ -17,6 +17,10 @@ package org.cyanogenmod.tests.providers; import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.UserHandle; import android.provider.Settings; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.MediumTest; @@ -24,10 +28,20 @@ import cyanogenmod.providers.CMSettings; public class CMSettingsTest extends AndroidTestCase{ private ContentResolver mContentResolver; + private CMSettingsTestObserver mTestObserver; + + private static boolean sIsOnChangedCalled = false; + private static Uri sExpectedUriChange = null; @Override public void setUp() { mContentResolver = getContext().getContentResolver(); + mTestObserver = new CMSettingsTestObserver(null); + } + + @Override + public void tearDown() { + mContentResolver.unregisterContentObserver(mTestObserver); } @MediumTest @@ -43,6 +57,12 @@ public class CMSettingsTest extends AndroidTestCase{ String actualValue = CMSettings.System.getString(mContentResolver, key); assertEquals(expectedValue, actualValue); + // setup observer + sIsOnChangedCalled = false; + sExpectedUriChange = CMSettings.System.getUriFor(key); + mContentResolver.registerContentObserver(sExpectedUriChange, false, mTestObserver, + UserHandle.USER_ALL); + // replace final String expectedReplaceValue = "systemTestValue2"; isPutSuccessful = CMSettings.System.putString(mContentResolver, key, expectedReplaceValue); @@ -53,9 +73,13 @@ public class CMSettingsTest extends AndroidTestCase{ assertEquals(expectedReplaceValue, actualValue); // delete to clean up - int rowsAffected = mContentResolver.delete(CMSettings.System.CONTENT_URI, Settings.NameValueTable.NAME + " = ?", - new String[]{ key }); + int rowsAffected = mContentResolver.delete(CMSettings.System.CONTENT_URI, + Settings.NameValueTable.NAME + " = ?", new String[]{ key }); assertEquals(1, rowsAffected); + + if (!sIsOnChangedCalled) { + fail("On change was never called or was called with the wrong uri"); + } } @MediumTest @@ -71,6 +95,12 @@ public class CMSettingsTest extends AndroidTestCase{ String actualValue = CMSettings.Secure.getString(mContentResolver, key); assertEquals(expectedValue, actualValue); + // setup observer + sIsOnChangedCalled = false; + sExpectedUriChange = CMSettings.Secure.getUriFor(key); + mContentResolver.registerContentObserver(sExpectedUriChange, false, mTestObserver, + UserHandle.USER_ALL); + // replace final String expectedReplaceValue = "secureTestValue2"; isPutSuccessful = CMSettings.Secure.putString(mContentResolver, key, expectedReplaceValue); @@ -81,9 +111,13 @@ public class CMSettingsTest extends AndroidTestCase{ assertEquals(expectedReplaceValue, actualValue); // delete to clean up - int rowsAffected = mContentResolver.delete(CMSettings.Secure.CONTENT_URI, Settings.NameValueTable.NAME + " = ?", - new String[]{ key }); + int rowsAffected = mContentResolver.delete(CMSettings.Secure.CONTENT_URI, + Settings.NameValueTable.NAME + " = ?", new String[]{ key }); assertEquals(1, rowsAffected); + + if (!sIsOnChangedCalled) { + fail("On change was never called or was called with the wrong uri"); + } } @MediumTest @@ -99,6 +133,12 @@ public class CMSettingsTest extends AndroidTestCase{ String actualValue = CMSettings.Global.getString(mContentResolver, key); assertEquals(expectedValue, actualValue); + // setup observer + sIsOnChangedCalled = false; + sExpectedUriChange = CMSettings.Global.getUriFor(key); + mContentResolver.registerContentObserver(sExpectedUriChange, false, mTestObserver, + UserHandle.USER_OWNER); + // replace final String expectedReplaceValue = "globalTestValue2"; isPutSuccessful = CMSettings.Global.putString(mContentResolver, key, expectedReplaceValue); @@ -109,9 +149,27 @@ public class CMSettingsTest extends AndroidTestCase{ assertEquals(expectedReplaceValue, actualValue); // delete to clean up - int rowsAffected = mContentResolver.delete(CMSettings.Global.CONTENT_URI, Settings.NameValueTable.NAME + " = ?", - new String[]{ key }); + int rowsAffected = mContentResolver.delete(CMSettings.Global.CONTENT_URI, + Settings.NameValueTable.NAME + " = ?", new String[]{ key }); assertEquals(1, rowsAffected); + + if (!sIsOnChangedCalled) { + fail("On change was never called or was called with the wrong uri"); + } + } + + private class CMSettingsTestObserver extends ContentObserver { + + public CMSettingsTestObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (sExpectedUriChange.equals(uri)) { + sIsOnChangedCalled = true; + } + } } // TODO Add tests for other users