diff options
author | Yvonne Wong <ywong@cyngn.com> | 2015-08-20 16:02:08 -0700 |
---|---|---|
committer | Yvonne Wong <ywong@cyngn.com> | 2015-08-31 13:52:27 -0700 |
commit | 0eb2999091c19e8c9b9d9a33c96e33776b69121f (patch) | |
tree | 171c9abc9619e3ac00754259de06eb7113062682 /packages | |
parent | e949433c08d67a9e5d8cbd4e67ba4d8c167a8ef3 (diff) | |
download | vendor_cmsdk-0eb2999091c19e8c9b9d9a33c96e33776b69121f.zip vendor_cmsdk-0eb2999091c19e8c9b9d9a33c96e33776b69121f.tar.gz vendor_cmsdk-0eb2999091c19e8c9b9d9a33c96e33776b69121f.tar.bz2 |
Add CMSettingsProvider and CMDatabaseHelper
issue-id: CYNGNOS-828
Change-Id: I01c08c0e432d6a941950a565e5ab6664664e2a7f
Diffstat (limited to 'packages')
9 files changed, 897 insertions, 0 deletions
diff --git a/packages/CMSettingsProvider/Android.mk b/packages/CMSettingsProvider/Android.mk new file mode 100644 index 0000000..8659c70 --- /dev/null +++ b/packages/CMSettingsProvider/Android.mk @@ -0,0 +1,36 @@ +# +# Copyright (C) 2015 The CyanogenMod Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +src_dir := src +res_dir := res + +LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dir)) +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) + +LOCAL_PACKAGE_NAME := CMSettingsProvider +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true + +LOCAL_JAVA_LIBRARIES := \ + org.cyanogenmod.platform.sdk + +include $(BUILD_PACKAGE) + +######################## +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/packages/CMSettingsProvider/AndroidManifest.xml b/packages/CMSettingsProvider/AndroidManifest.xml new file mode 100644 index 0000000..b46fefc --- /dev/null +++ b/packages/CMSettingsProvider/AndroidManifest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.cyanogenmod.cmsettings" + coreApp="true" + android:sharedUserId="android.uid.system"> + <!-- It is necessary to be a system app in order to update table versions in SystemProperties for + CMSettings to know whether or not the client side cache is up to date. It is also necessary + to run in the system process in order to start the content provider prior to running migration + for CM settings on user starting --> + + <uses-permission android:name="android.permission.MANAGE_USERS" /> + + <application android:icon="@drawable/icon" + android:label="@string/app_name" + android:process="system" + android:killAfterRestore="false" + android:allowClearUserData="false" + android:enabled="true"> + + <provider android:name="CMSettingsProvider" android:authorities="cmsettings" + android:multiprocess="false" + android:exported="true" + android:writePermission="cyanogenmod.permission.WRITE_SETTINGS" + android:singleUser="true" + android:initOrder="100" /> + + </application> +</manifest> diff --git a/packages/CMSettingsProvider/res/drawable/icon.png b/packages/CMSettingsProvider/res/drawable/icon.png Binary files differnew file mode 100644 index 0000000..08ee50d --- /dev/null +++ b/packages/CMSettingsProvider/res/drawable/icon.png diff --git a/packages/CMSettingsProvider/res/values/strings.xml b/packages/CMSettingsProvider/res/values/strings.xml new file mode 100644 index 0000000..4037a54 --- /dev/null +++ b/packages/CMSettingsProvider/res/values/strings.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014-2015 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <string name="app_name">CMSettingsProvider</string> +</resources>
\ No newline at end of file diff --git a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java new file mode 100644 index 0000000..85fbaa9 --- /dev/null +++ b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMDatabaseHelper.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.cmsettings; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Environment; +import android.os.UserHandle; +import android.util.Log; + +import java.io.File; + +/** + * The CMDatabaseHelper allows creation of a database to store CM specific settings for a user + * in System, Secure, and Global tables. + */ +public class CMDatabaseHelper extends SQLiteOpenHelper{ + private static final String TAG = "CMDatabaseHelper"; + private static final boolean LOCAL_LOGV = false; + + private static final String DATABASE_NAME = "cmsettings.db"; + private static final int DATABASE_VERSION = 1; + + static class CMTableNames { + static final String TABLE_SYSTEM = "system"; + static final String TABLE_SECURE = "secure"; + static final String TABLE_GLOBAL = "global"; + } + + private static final String CREATE_TABLE_SQL_FORMAT = "CREATE TABLE %s (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT UNIQUE ON CONFLICT REPLACE," + + "value TEXT" + + ");)"; + + private static final String CREATE_INDEX_SQL_FORMAT = "CREATE INDEX %sIndex%d ON %s (name);"; + + private int mUserHandle; + + /** + * Gets the appropriate database path for a specific user + * @param userId The database path for this user + * @return The database path string + */ + static String dbNameForUser(final int userId) { + // The owner gets the unadorned db name; + if (userId == UserHandle.USER_OWNER) { + return DATABASE_NAME; + } else { + // Place the database in the user-specific data tree so that it's + // cleaned up automatically when the user is deleted. + File databaseFile = new File( + Environment.getUserSystemDirectory(userId), DATABASE_NAME); + return databaseFile.getPath(); + } + } + + /** + * Creates an instance of {@link CMDatabaseHelper} + * @param context + * @param userId + */ + public CMDatabaseHelper(Context context, int userId) { + super(context, dbNameForUser(userId), null, DATABASE_VERSION); + mUserHandle = userId; + } + + /** + * Creates System, Secure, and Global tables in the specified {@link SQLiteDatabase} + * @param db The database. + */ + @Override + public void onCreate(SQLiteDatabase db) { + db.beginTransaction(); + + try { + createDbTable(db, CMTableNames.TABLE_SYSTEM); + createDbTable(db, CMTableNames.TABLE_SECURE); + + if (mUserHandle == UserHandle.USER_OWNER) { + createDbTable(db, CMTableNames.TABLE_GLOBAL); + } + + db.setTransactionSuccessful(); + + if (LOCAL_LOGV) Log.v(TAG, "Successfully created tables for cm settings db"); + } finally { + db.endTransaction(); + } + } + + /** + * Creates a table and index for the specified database and table name + * @param db + * @param tableName + */ + private void createDbTable(SQLiteDatabase db, String tableName) { + if (LOCAL_LOGV) Log.v(TAG, "Creating table and index for: " + tableName); + + String createTableSql = String.format(CREATE_TABLE_SQL_FORMAT, tableName); + db.execSQL(createTableSql); + + String createIndexSql = String.format(CREATE_INDEX_SQL_FORMAT, tableName, 1, tableName); + db.execSQL(createIndexSql); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } +} diff --git a/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java new file mode 100644 index 0000000..eeabba3 --- /dev/null +++ b/packages/CMSettingsProvider/src/org/cyanogenmod/cmsettings/CMSettingsProvider.java @@ -0,0 +1,451 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.cmsettings; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.content.pm.PackageManager; +import android.database.AbstractCursor; +import android.database.Cursor; +import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Binder; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import cyanogenmod.providers.CMSettings; + +/** + * The CMSettingsProvider serves as a {@link ContentProvider} for CM specific settings + */ +public class CMSettingsProvider extends ContentProvider { + private static final String TAG = "CMSettingsProvider"; + private static final boolean LOCAL_LOGV = false; + + private static final boolean USER_CHECK_THROWS = true; + + // Each defined user has their own settings + protected final SparseArray<CMDatabaseHelper> mDbHelpers = new SparseArray<CMDatabaseHelper>(); + + private static final int SYSTEM = 1; + private static final int SECURE = 2; + private static final int GLOBAL = 3; + + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + static { + sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_SYSTEM, + SYSTEM); + sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_SECURE, + SECURE); + sUriMatcher.addURI(CMSettings.AUTHORITY, CMDatabaseHelper.CMTableNames.TABLE_GLOBAL, + GLOBAL); + // TODO add other paths for getting specific items + } + + private UserManager mUserManager; + private Uri.Builder mUriBuilder; + + @Override + public boolean onCreate() { + if (LOCAL_LOGV) Log.d(TAG, "Creating CMSettingsProvider"); + + mUserManager = UserManager.get(getContext()); + + establishDbTracking(UserHandle.USER_OWNER); + + mUriBuilder = new Uri.Builder(); + mUriBuilder.scheme(ContentResolver.SCHEME_CONTENT); + mUriBuilder.authority(CMSettings.AUTHORITY); + + // TODO Add migration for cm settings + + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + if (uri == null) { + throw new IllegalArgumentException("Uri cannot be null"); + } + + String tableName = getTableNameFromUri(uri); + checkWritePermissions(tableName); + + int callingUserId = UserHandle.getCallingUserId(); + CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, + callingUserId)); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + queryBuilder.setTables(tableName); + + Cursor returnCursor = queryBuilder.query(db, projection, selection, selectionArgs, null, + null, sortOrder); + // the default Cursor interface does not support per-user observation + try { + AbstractCursor abstractCursor = (AbstractCursor) returnCursor; + abstractCursor.setNotificationUri(getContext().getContentResolver(), uri, + callingUserId); + } catch (ClassCastException e) { + // details of the concrete Cursor implementation have changed and this code has + // not been updated to match -- complain and fail hard. + Log.wtf(TAG, "Incompatible cursor derivation"); + throw e; + } + + return returnCursor; + } + + @Override + public String getType(Uri uri) { + // TODO: Implement + return null; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + return bulkInsertForUser(UserHandle.getCallingUserId(), uri, values); + } + + /** + * Performs a bulk insert for a specific user. + * @param userId The user id to perform the bulk insert for. + * @param uri The content:// URI of the insertion request. + * @param values An array of sets of column_name/value pairs to add to the database. + * This must not be {@code null}. + * @return Number of rows inserted. + */ + int bulkInsertForUser(int userId, Uri uri, ContentValues[] values) { + if (uri == null) { + throw new IllegalArgumentException("Uri cannot be null"); + } + + if (values == null) { + throw new IllegalArgumentException("ContentValues cannot be null"); + } + + int numRowsAffected = 0; + + String tableName = getTableNameFromUri(uri); + checkWritePermissions(tableName); + + CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, userId)); + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + db.beginTransaction(); + try { + for (ContentValues value : values) { + if (value == null) { + continue; + } + + long rowId = db.insert(tableName, null, value); + + if (rowId >= 0) { + numRowsAffected++; + + if (LOCAL_LOGV) Log.d(TAG, tableName + " <- " + values); + } else { + return 0; + } + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + db.close(); + } + + if (numRowsAffected > 0) { + getContext().getContentResolver().notifyChange(uri, null); + notifyChange(uri, tableName, userId); + if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) inserted"); + } + + return numRowsAffected; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + if (uri == null) { + throw new IllegalArgumentException("Uri cannot be null"); + } + + if (values == null) { + throw new IllegalArgumentException("ContentValues cannot be null"); + } + + String tableName = getTableNameFromUri(uri); + checkWritePermissions(tableName); + + int callingUserId = UserHandle.getCallingUserId(); + CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, + callingUserId)); + + long rowId = -1; + + SQLiteDatabase db = dbHelper.getWritableDatabase(); + try { + rowId = db.insert(tableName, null, values); + } finally { + db.close(); + } + + Uri returnUri = null; + if (rowId != -1) { + returnUri = ContentUris.withAppendedId(uri, rowId); + notifyChange(returnUri, tableName, callingUserId); + if (LOCAL_LOGV) Log.d(TAG, "Inserted row id: " + rowId + " into tableName: " + + tableName); + } + + return returnUri; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + if (uri == null) { + throw new IllegalArgumentException("Uri cannot be null"); + } + + int numRowsAffected = 0; + + // Allow only selection by key; a null/empty selection string will cause all rows in the + // table to be deleted + if (!TextUtils.isEmpty(selection) && selectionArgs.length > 0) { + String tableName = getTableNameFromUri(uri); + checkWritePermissions(tableName); + + int callingUserId = UserHandle.getCallingUserId(); + CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, + callingUserId)); + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + try { + numRowsAffected = db.delete(tableName, selection, selectionArgs); + } finally { + db.close(); + } + + if (numRowsAffected > 0) { + notifyChange(uri, tableName, callingUserId); + if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) deleted"); + } + } + + return numRowsAffected; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + if (uri == null) { + throw new IllegalArgumentException("Uri cannot be null"); + } + + if (values == null) { + throw new IllegalArgumentException("ContentValues cannot be null"); + } + + String tableName = getTableNameFromUri(uri); + checkWritePermissions(tableName); + + int callingUserId = UserHandle.getCallingUserId(); + CMDatabaseHelper dbHelper = getOrEstablishDatabase(getUserIdForTable(tableName, + callingUserId)); + + int numRowsAffected = 0; + + SQLiteDatabase db = dbHelper.getWritableDatabase(); + try { + numRowsAffected = db.update(tableName, values, selection, selectionArgs); + } finally { + db.close(); + } + + if (numRowsAffected > 0) { + getContext().getContentResolver().notifyChange(uri, null); + if (LOCAL_LOGV) Log.d(TAG, tableName + ": " + numRowsAffected + " row(s) updated"); + } + + return numRowsAffected; + } + + /** + * Tries to get a {@link CMDatabaseHelper} for the specified user and if it does not exist, a + * new instance of {@link CMDatabaseHelper} is created for the specified user and returned. + * @param callingUser + * @return + */ + private CMDatabaseHelper getOrEstablishDatabase(int callingUser) { + if (callingUser >= android.os.Process.SYSTEM_UID) { + if (USER_CHECK_THROWS) { + throw new IllegalArgumentException("Uid rather than user handle: " + callingUser); + } else { + Log.wtf(TAG, "Establish db for uid rather than user: " + callingUser); + } + } + + long oldId = Binder.clearCallingIdentity(); + try { + CMDatabaseHelper dbHelper; + synchronized (this) { + dbHelper = mDbHelpers.get(callingUser); + } + if (null == dbHelper) { + establishDbTracking(callingUser); + synchronized (this) { + dbHelper = mDbHelpers.get(callingUser); + } + } + return dbHelper; + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + /** + * Check if a {@link CMDatabaseHelper} exists for a user and if it doesn't, a new helper is + * created and added to the list of tracked database helpers + * @param userId + */ + private void establishDbTracking(int userId) { + CMDatabaseHelper dbHelper; + + synchronized (this) { + dbHelper = mDbHelpers.get(userId); + if (LOCAL_LOGV) { + Log.i(TAG, "Checking cm settings db helper for user " + userId); + } + if (dbHelper == null) { + if (LOCAL_LOGV) { + Log.i(TAG, "Installing new cm settings db helper for user " + userId); + } + dbHelper = new CMDatabaseHelper(getContext(), userId); + mDbHelpers.append(userId, dbHelper); + } + } + + // Initialization of the db *outside* the locks. It's possible that racing + // threads might wind up here, the second having read the cache entries + // written by the first, but that's benign: the SQLite helper implementation + // manages concurrency itself, and it's important that we not run the db + // initialization with any of our own locks held, so we're fine. + SQLiteDatabase db = null; + try { + db = dbHelper.getWritableDatabase(); + } catch (SQLiteCantOpenDatabaseException ex){ + Log.e(TAG, "Unable to open writable database for user: " + userId, ex); + } finally { + db.close(); + } + } + + /** + * Makes sure the caller has permission to write this data. + * @param tableName supplied by the caller + * @throws SecurityException if the caller is forbidden to write. + */ + private void checkWritePermissions(String tableName) { + if ((CMDatabaseHelper.CMTableNames.TABLE_SECURE.equals(tableName) || + CMDatabaseHelper.CMTableNames.TABLE_GLOBAL.equals(tableName)) && + getContext().checkCallingOrSelfPermission( + cyanogenmod.platform.Manifest.permission.WRITE_SECURE_SETTINGS) != + PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + String.format("Permission denial: writing to cm secure settings requires %1$s", + cyanogenmod.platform.Manifest.permission.WRITE_SECURE_SETTINGS)); + } + } + + /** + * Utilizes an {@link UriMatcher} to check for a valid combination of scheme, authority, and + * path and returns the corresponding table name + * @param uri + * @return Table name + */ + private String getTableNameFromUri(Uri uri) { + int code = sUriMatcher.match(uri); + + switch (code) { + case SYSTEM: + return CMDatabaseHelper.CMTableNames.TABLE_SYSTEM; + case SECURE: + return CMDatabaseHelper.CMTableNames.TABLE_SECURE; + case GLOBAL: + return CMDatabaseHelper.CMTableNames.TABLE_GLOBAL; + default: + throw new IllegalArgumentException("Invalid uri: " + uri); + } + } + + /** + * If the table is Global, the owner's user id is returned. Otherwise, the original user id + * is returned. + * @param tableName + * @param userId + * @return User id + */ + private int getUserIdForTable(String tableName, int userId) { + return CMDatabaseHelper.CMTableNames.TABLE_GLOBAL.equals(tableName) ? + UserHandle.USER_OWNER : userId; + } + + /** + * Modify setting version for an updated table before notifying of change. The + * {@link CMSettings} class uses these to provide client-side caches. + * @param uri to send notifications for + * @param userId + */ + private void notifyChange(Uri uri, String tableName, int userId) { + String property = null; + final boolean isGlobal = tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_GLOBAL); + if (tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_SYSTEM)) { + property = CMSettings.System.SYS_PROP_CM_SETTING_VERSION; + } else if (tableName.equals(CMDatabaseHelper.CMTableNames.TABLE_SECURE)) { + property = CMSettings.Secure.SYS_PROP_CM_SETTING_VERSION; + } else if (isGlobal) { + property = CMSettings.Global.SYS_PROP_CM_SETTING_VERSION; + } + + if (property != null) { + long version = SystemProperties.getLong(property, 0) + 1; + if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version); + SystemProperties.set(property, Long.toString(version)); + } + + final int notifyTarget = isGlobal ? UserHandle.USER_ALL : userId; + final long oldId = Binder.clearCallingIdentity(); + try { + getContext().getContentResolver().notifyChange(uri, null, true, notifyTarget); + } finally { + Binder.restoreCallingIdentity(oldId); + } + if (LOCAL_LOGV) Log.v(TAG, "notifying for " + notifyTarget + ": " + uri); + } + + // TODO Add caching +} diff --git a/packages/CMSettingsProvider/tests/Android.mk b/packages/CMSettingsProvider/tests/Android.mk new file mode 100644 index 0000000..52c3e4e --- /dev/null +++ b/packages/CMSettingsProvider/tests/Android.mk @@ -0,0 +1,34 @@ +# +# Copyright (C) 2015 The CyanogenMod Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_PACKAGE_NAME := CMSettingsProviderTests +LOCAL_INSTRUMENTATION_FOR := CMSettingsProvider + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_CERTIFICATE := platform +LOCAL_JAVA_LIBRARIES := android.test.runner +LOCAL_PROGUARD_ENABLED := disabled + +LOCAL_STATIC_JAVA_LIBRARIES := \ + org.cyanogenmod.platform.sdk + +include $(BUILD_PACKAGE) diff --git a/packages/CMSettingsProvider/tests/AndroidManifest.xml b/packages/CMSettingsProvider/tests/AndroidManifest.xml new file mode 100644 index 0000000..e82a7d8 --- /dev/null +++ b/packages/CMSettingsProvider/tests/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.cyanogenmod.cmsettings.tests"> + + <uses-permission android:name="cyanogenmod.permission.WRITE_SETTINGS"/> + <uses-permission android:name="cyanogenmod.permission.WRITE_SECURE_SETTINGS"/> + + <instrumentation + android:name="android.test.InstrumentationTestRunner" + android:targetPackage="org.cyanogenmod.cmsettings.tests" + android:label="CM Settings Provider Tests" /> + + <application> + <uses-library android:name="android.test.runner" /> + </application> +</manifest> diff --git a/packages/CMSettingsProvider/tests/src/org/cyanogenmod/cmsettings/tests/CMSettingsProviderTest.java b/packages/CMSettingsProvider/tests/src/org/cyanogenmod/cmsettings/tests/CMSettingsProviderTest.java new file mode 100644 index 0000000..7ec446c --- /dev/null +++ b/packages/CMSettingsProvider/tests/src/org/cyanogenmod/cmsettings/tests/CMSettingsProviderTest.java @@ -0,0 +1,159 @@ + /** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.cmsettings.tests; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; +import android.util.Log; +import cyanogenmod.providers.CMSettings; + +import java.util.LinkedHashMap; +import java.util.Map; + + public class CMSettingsProviderTest extends AndroidTestCase { + private static final String TAG = "CMSettingsProviderTest"; + + private static final LinkedHashMap<String, String> mMap = new LinkedHashMap<String, String>(); + + static { + mMap.put("testKey1", "value1"); + mMap.put("testKey2", "value2"); + mMap.put("testKey3", "value3"); + } + + private static final String[] PROJECTIONS = new String[] { "name", "value" }; + + private ContentResolver mContentResolver; + + @Override + public void setUp() { + mContentResolver = mContext.getContentResolver(); + } + + @MediumTest + public void testBulkInsertSuccess() { + Log.d(TAG, "Starting bulk insert test"); + + ContentValues[] contentValues = new ContentValues[mMap.size()]; + int count = 0; + for (Map.Entry<String, String> kVPair : mMap.entrySet()) { + ContentValues contentValue = new ContentValues(); + contentValue.put(PROJECTIONS[0], kVPair.getKey()); + contentValue.put(PROJECTIONS[1], kVPair.getValue()); + contentValues[count++] = contentValue; + } + + testBulkInsertForUri(CMSettings.System.CONTENT_URI, contentValues); + testBulkInsertForUri(CMSettings.Secure.CONTENT_URI, contentValues); + testBulkInsertForUri(CMSettings.Global.CONTENT_URI, contentValues); + + Log.d(TAG, "Finished bulk insert test"); + } + + private void testBulkInsertForUri(Uri uri, ContentValues[] contentValues) { + int rowsInserted = mContentResolver.bulkInsert(uri, contentValues); + assertEquals(mMap.size(), rowsInserted); + + Cursor queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); + try { + while (queryCursor.moveToNext()) { + assertEquals(PROJECTIONS.length, queryCursor.getColumnCount()); + + String actualKey = queryCursor.getString(0); + assertTrue(mMap.containsKey(actualKey)); + + assertEquals(mMap.get(actualKey), queryCursor.getString(1)); + } + + Log.d(TAG, "Test successful"); + } + finally { + queryCursor.close(); + } + + // TODO: Find a better way to cleanup database/use ProviderTestCase2 without process crash + for (String key : mMap.keySet()) { + mContentResolver.delete(uri, PROJECTIONS[0] + " = ?", new String[]{ key }); + } + } + + @MediumTest + public void testInsertUpdateDeleteSuccess() { + Log.d(TAG, "Starting insert/update/delete test"); + + testInsertUpdateDeleteForUri(CMSettings.System.CONTENT_URI); + testInsertUpdateDeleteForUri(CMSettings.Secure.CONTENT_URI); + testInsertUpdateDeleteForUri(CMSettings.Global.CONTENT_URI); + + Log.d(TAG, "Finished insert/update/delete test"); + } + + private void testInsertUpdateDeleteForUri(Uri uri) { + String key1 = "testKey1"; + String value1 = "value1"; + String value2 = "value2"; + + // test insert + ContentValues contentValue = new ContentValues(); + contentValue.put(PROJECTIONS[0], key1); + contentValue.put(PROJECTIONS[1], value1); + + mContentResolver.insert(uri, contentValue); + + // check insert + Cursor queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); + assertEquals(1, queryCursor.getCount()); + + queryCursor.moveToNext(); + assertEquals(PROJECTIONS.length, queryCursor.getColumnCount()); + + String actualKey = queryCursor.getString(0); + assertEquals(key1, actualKey); + assertEquals(value1, queryCursor.getString(1)); + + // test update + contentValue.clear(); + contentValue.put(PROJECTIONS[1], value2); + + int rowsAffected = mContentResolver.update(uri, contentValue, PROJECTIONS[0] + " = ?", + new String[]{key1}); + assertEquals(1, rowsAffected); + + // check update + queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); + assertEquals(1, queryCursor.getCount()); + + queryCursor.moveToNext(); + assertEquals(PROJECTIONS.length, queryCursor.getColumnCount()); + + actualKey = queryCursor.getString(0); + assertEquals(key1, actualKey); + assertEquals(value2, queryCursor.getString(1)); + + // test delete + rowsAffected = mContentResolver.delete(uri, PROJECTIONS[0] + " = ?", new String[]{key1}); + assertEquals(1, rowsAffected); + + // check delete + queryCursor = mContentResolver.query(uri, PROJECTIONS, null, null, null); + assertEquals(0, queryCursor.getCount()); + } + } |