summaryrefslogtreecommitdiffstats
path: root/packages/SettingsProvider/src
diff options
context:
space:
mode:
authorSvetoslav <svetoslavganov@google.com>2015-01-15 14:22:26 -0800
committerSvetoslav <svetoslavganov@google.com>2015-02-11 17:58:22 -0800
commit683914bfb13908bf380a25258cd45bcf43f13dc9 (patch)
tree8dbece2e42872875b0b3c4972bdcbf0ef24fbdc7 /packages/SettingsProvider/src
parent6e08723f65ee3393115c3288bc475e7362cb9117 (diff)
downloadframeworks_base-683914bfb13908bf380a25258cd45bcf43f13dc9.zip
frameworks_base-683914bfb13908bf380a25258cd45bcf43f13dc9.tar.gz
frameworks_base-683914bfb13908bf380a25258cd45bcf43f13dc9.tar.bz2
Rewrite of the settings provider.
This change modifies how global, secure, and system settings are managed. In particular, we are moving away from the database to an in-memory model where the settings are persisted asynchronously to XML. This simplifies evolution and improves performance, for example, changing a setting is down from around 400 ms to 10 ms as we do not hit the disk. The trade off is that we may lose data if the system dies before persisting the change. In practice this is not a problem because 1) this is very rare; 2) apps changing a setting use the setting itself to know if it changed, so next time the app runs (after a reboot that lost data) the app will be oblivious that data was lost. When persisting the settings we delay the write a bit to batch multiple changes. If a change occurs we reschedule the write but when a maximal delay occurs after the first non-persisted change we write to disk no matter what. This prevents a malicious app poking the settings all the time to prevent them being persisted. The settings are persisted in separate XML files for each type of setting per user. Specifically, they are in the user's system directory and the files are named: settings_type_of_settings.xml. Data migration is performed after the data base is upgraded to its last version after which the global, system, and secure tables are dropped. The global, secure, and system settings now have the same version and are upgraded as a whole per user to allow migration of settings between these them. The upgrade steps should be added to the SettingsProvider.UpgradeController and not in the DatabaseHelper. Setting states are mapped to an integer key derived from the user id and the setting type. Therefore, all setting states are in a lookup table which makes all opertions very fast. The code is a complete rewrite aiming for improved clarity and increased maintainability as opposed to using minor optimizations. Now setting and getting the changed setting takes around 10 ms. We can optimize later if needed. Now the code path through the call API and the one through the content provider APIs end up being the same which fixes bugs where some enterprise cases were not implemented in the content provider code path. Note that we are keeping the call code path as it is a bit faster than the provider APIs with about 2 ms for setting and getting a setting. The front-end settings APIs use the call method. Further, we are restricting apps writing to the system settings. If the app is targeting API higher than Lollipop MR1 we do not let them have their settings in the system ones. Otherwise, we warn that this will become an error. System apps like GMS core can change anything like the system or shell or root. Since old apps can add their settings, this can increase the system memory footprint with no limit. Therefore, we limit the amount of settings data an app can write to the system settings before starting to reject new data. Another problem with the system settings was that an app with a permission to write there can put invalid values for the settings. We now have validators for these settings that ensure only valid values are accepted. Since apps can put their settings in the system table, when the app is uninstalled this data is stale in the sytem table without ever being used. Now we keep the package that last changed the setting and when the package is removed all settings it touched that are not in the ones defined in the APIs are dropped. Keeping in memory settings means that we cannot handle arbitrary SQL operations, rather the supported operations are on a single setting by name and all settings (querying). This should not be a problem in practice but we have to verify it. For that reason, we log unsupported SQL operations to the event log to do some crunching and see what if any cases we should additionally support. There are also tests for the settings provider in this change. Change-Id: I941dc6e567588d9812905b147dbe1a3191c8dd68
Diffstat (limited to 'packages/SettingsProvider/src')
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java115
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags5
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java19
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java2578
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java574
5 files changed, 2215 insertions, 1076 deletions
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
index 06e26bd..729efcb 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
@@ -58,6 +58,7 @@ import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Database helper class for {@link SettingsProvider}.
@@ -78,6 +79,9 @@ public class DatabaseHelper extends SQLiteOpenHelper {
private static final HashSet<String> mValidTables = new HashSet<String>();
+ private static final String DATABASE_JOURNAL_SUFFIX = "-journal";
+ private static final String DATABASE_BACKUP_SUFFIX = "-backup";
+
private static final String TABLE_SYSTEM = "system";
private static final String TABLE_SECURE = "secure";
private static final String TABLE_GLOBAL = "global";
@@ -86,13 +90,13 @@ public class DatabaseHelper extends SQLiteOpenHelper {
mValidTables.add(TABLE_SYSTEM);
mValidTables.add(TABLE_SECURE);
mValidTables.add(TABLE_GLOBAL);
- mValidTables.add("bluetooth_devices");
- mValidTables.add("bookmarks");
// These are old.
+ mValidTables.add("bluetooth_devices");
+ mValidTables.add("bookmarks");
mValidTables.add("favorites");
- mValidTables.add("gservices");
mValidTables.add("old_favorites");
+ mValidTables.add("android_metadata");
}
static String dbNameForUser(final int userHandle) {
@@ -118,6 +122,33 @@ public class DatabaseHelper extends SQLiteOpenHelper {
return mValidTables.contains(name);
}
+ public void dropDatabase() {
+ close();
+ File databaseFile = mContext.getDatabasePath(getDatabaseName());
+ if (databaseFile.exists()) {
+ databaseFile.delete();
+ }
+ File databaseJournalFile = mContext.getDatabasePath(getDatabaseName()
+ + DATABASE_JOURNAL_SUFFIX);
+ if (databaseJournalFile.exists()) {
+ databaseJournalFile.delete();
+ }
+ }
+
+ public void backupDatabase() {
+ close();
+ File databaseFile = mContext.getDatabasePath(getDatabaseName());
+ if (!databaseFile.exists()) {
+ return;
+ }
+ File backupFile = mContext.getDatabasePath(getDatabaseName()
+ + DATABASE_BACKUP_SUFFIX);
+ if (backupFile.exists()) {
+ return;
+ }
+ databaseFile.renameTo(backupFile);
+ }
+
private void createSecureTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE secure (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -1221,9 +1252,11 @@ public class DatabaseHelper extends SQLiteOpenHelper {
// Migrate now-global settings. Note that this happens before
// new users can be created.
createGlobalTable(db);
- String[] settingsToMove = hashsetToStringArray(SettingsProvider.sSystemGlobalKeys);
+ String[] settingsToMove = setToStringArray(
+ SettingsProvider.sSystemMovedToGlobalSettings);
moveSettingsToNewTable(db, TABLE_SYSTEM, TABLE_GLOBAL, settingsToMove, false);
- settingsToMove = hashsetToStringArray(SettingsProvider.sSecureGlobalKeys);
+ settingsToMove = setToStringArray(
+ SettingsProvider.sSecureMovedToGlobalSettings);
moveSettingsToNewTable(db, TABLE_SECURE, TABLE_GLOBAL, settingsToMove, false);
db.setTransactionSuccessful();
@@ -1489,9 +1522,11 @@ public class DatabaseHelper extends SQLiteOpenHelper {
db.beginTransaction();
try {
// Migrate now-global settings
- String[] settingsToMove = hashsetToStringArray(SettingsProvider.sSystemGlobalKeys);
+ String[] settingsToMove = setToStringArray(
+ SettingsProvider.sSystemMovedToGlobalSettings);
moveSettingsToNewTable(db, TABLE_SYSTEM, TABLE_GLOBAL, settingsToMove, true);
- settingsToMove = hashsetToStringArray(SettingsProvider.sSecureGlobalKeys);
+ settingsToMove = setToStringArray(
+ SettingsProvider.sSecureMovedToGlobalSettings);
moveSettingsToNewTable(db, TABLE_SECURE, TABLE_GLOBAL, settingsToMove, true);
db.setTransactionSuccessful();
@@ -1855,7 +1890,8 @@ public class DatabaseHelper extends SQLiteOpenHelper {
try {
stmt = db.compileStatement("INSERT OR IGNORE INTO global(name,value)"
+ " VALUES(?,?);");
- loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED, ImsConfig.FeatureValueConstants.ON);
+ loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED,
+ ImsConfig.FeatureValueConstants.ON);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ -1895,34 +1931,50 @@ public class DatabaseHelper extends SQLiteOpenHelper {
}
upgradeVersion = 118;
}
+
+ /**
+ * IMPORTANT: Do not add any more upgrade steps here as the global,
+ * secure, and system settings are no longer stored in a database
+ * but are kept in memory and persisted to XML. The correct places
+ * for adding upgrade steps are:
+ *
+ * Global: SettingsProvider.UpgradeController#onUpgradeGlobalSettings
+ * Secure: SettingsProvider.UpgradeController#onUpgradeSecureSettings
+ * System: SettingsProvider.UpgradeController#onUpgradeSystemSettings
+ */
+
// *** Remember to update DATABASE_VERSION above!
if (upgradeVersion != currentVersion) {
- Log.w(TAG, "Got stuck trying to upgrade from version " + upgradeVersion
- + ", must wipe the settings provider");
- db.execSQL("DROP TABLE IF EXISTS global");
- db.execSQL("DROP TABLE IF EXISTS globalIndex1");
- db.execSQL("DROP TABLE IF EXISTS system");
- db.execSQL("DROP INDEX IF EXISTS systemIndex1");
- db.execSQL("DROP TABLE IF EXISTS secure");
- db.execSQL("DROP INDEX IF EXISTS secureIndex1");
- db.execSQL("DROP TABLE IF EXISTS gservices");
- db.execSQL("DROP INDEX IF EXISTS gservicesIndex1");
- db.execSQL("DROP TABLE IF EXISTS bluetooth_devices");
- db.execSQL("DROP TABLE IF EXISTS bookmarks");
- db.execSQL("DROP INDEX IF EXISTS bookmarksIndex1");
- db.execSQL("DROP INDEX IF EXISTS bookmarksIndex2");
- db.execSQL("DROP TABLE IF EXISTS favorites");
- onCreate(db);
-
- // Added for diagnosing settings.db wipes after the fact
- String wipeReason = oldVersion + "/" + upgradeVersion + "/" + currentVersion;
- db.execSQL("INSERT INTO secure(name,value) values('" +
- "wiped_db_reason" + "','" + wipeReason + "');");
+ recreateDatabase(db, oldVersion, upgradeVersion, currentVersion);
}
}
- private String[] hashsetToStringArray(HashSet<String> set) {
+ public void recreateDatabase(SQLiteDatabase db, int oldVersion,
+ int upgradeVersion, int currentVersion) {
+ db.execSQL("DROP TABLE IF EXISTS global");
+ db.execSQL("DROP TABLE IF EXISTS globalIndex1");
+ db.execSQL("DROP TABLE IF EXISTS system");
+ db.execSQL("DROP INDEX IF EXISTS systemIndex1");
+ db.execSQL("DROP TABLE IF EXISTS secure");
+ db.execSQL("DROP INDEX IF EXISTS secureIndex1");
+ db.execSQL("DROP TABLE IF EXISTS gservices");
+ db.execSQL("DROP INDEX IF EXISTS gservicesIndex1");
+ db.execSQL("DROP TABLE IF EXISTS bluetooth_devices");
+ db.execSQL("DROP TABLE IF EXISTS bookmarks");
+ db.execSQL("DROP INDEX IF EXISTS bookmarksIndex1");
+ db.execSQL("DROP INDEX IF EXISTS bookmarksIndex2");
+ db.execSQL("DROP TABLE IF EXISTS favorites");
+
+ onCreate(db);
+
+ // Added for diagnosing settings.db wipes after the fact
+ String wipeReason = oldVersion + "/" + upgradeVersion + "/" + currentVersion;
+ db.execSQL("INSERT INTO secure(name,value) values('" +
+ "wiped_db_reason" + "','" + wipeReason + "');");
+ }
+
+ private String[] setToStringArray(Set<String> set) {
String[] array = new String[set.size()];
return set.toArray(array);
}
@@ -2639,7 +2691,8 @@ public class DatabaseHelper extends SQLiteOpenHelper {
loadBooleanSetting(stmt, Settings.Global.GUEST_USER_ENABLED,
R.bool.def_guest_user_enabled);
- loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED, ImsConfig.FeatureValueConstants.ON);
+ loadSetting(stmt, Settings.Global.ENHANCED_4G_MODE_ENABLED,
+ ImsConfig.FeatureValueConstants.ON);
// --- New global settings start here
} finally {
if (stmt != null) stmt.close();
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags b/packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags
new file mode 100644
index 0000000..298d776
--- /dev/null
+++ b/packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags
@@ -0,0 +1,5 @@
+# See system/core/logcat/e for a description of the format of this file.
+
+option java_package com.android.providers.settings;
+
+52100 unsupported_settings_query (uri|3),(selection|3),(whereArgs|3)
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 264dcae..8371117 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -110,11 +110,7 @@ public class SettingsBackupAgent extends BackupAgentHelper {
private static final String TAG = "SettingsBackupAgent";
- private static final int COLUMN_NAME = 1;
- private static final int COLUMN_VALUE = 2;
-
private static final String[] PROJECTION = {
- Settings.NameValueTable._ID,
Settings.NameValueTable.NAME,
Settings.NameValueTable.VALUE
};
@@ -473,8 +469,8 @@ public class SettingsBackupAgent extends BackupAgentHelper {
ParcelFileDescriptor newState) throws IOException {
HashSet<String> movedToGlobal = new HashSet<String>();
- Settings.System.getMovedKeys(movedToGlobal);
- Settings.Secure.getMovedKeys(movedToGlobal);
+ Settings.System.getMovedToGlobalSettings(movedToGlobal);
+ Settings.Secure.getMovedToGlobalSettings(movedToGlobal);
while (data.readNextHeader()) {
final String key = data.getKey();
@@ -577,8 +573,8 @@ public class SettingsBackupAgent extends BackupAgentHelper {
if (version <= FULL_BACKUP_VERSION) {
// Generate the moved-to-global lookup table
HashSet<String> movedToGlobal = new HashSet<String>();
- Settings.System.getMovedKeys(movedToGlobal);
- Settings.Secure.getMovedKeys(movedToGlobal);
+ Settings.System.getMovedToGlobalSettings(movedToGlobal);
+ Settings.Secure.getMovedToGlobalSettings(movedToGlobal);
// system settings data first
int nBytes = in.readInt();
@@ -824,11 +820,14 @@ public class SettingsBackupAgent extends BackupAgentHelper {
String key = settings[i];
String value = cachedEntries.remove(key);
+ final int nameColumnIndex = cursor.getColumnIndex(Settings.NameValueTable.NAME);
+ final int valueColumnIndex = cursor.getColumnIndex(Settings.NameValueTable.VALUE);
+
// If the value not cached, let us look it up.
if (value == null) {
while (!cursor.isAfterLast()) {
- String cursorKey = cursor.getString(COLUMN_NAME);
- String cursorValue = cursor.getString(COLUMN_VALUE);
+ String cursorKey = cursor.getString(nameColumnIndex);
+ String cursorValue = cursor.getString(valueColumnIndex);
cursor.moveToNext();
if (key.equals(cursorKey)) {
value = cursorValue;
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 6828301..ff2c004 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -16,1315 +16,1823 @@
package com.android.providers.settings;
-import java.io.FileNotFoundException;
-import java.security.SecureRandom;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
+import android.Manifest;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.backup.BackupManager;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
-import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
-import android.database.AbstractCursor;
import android.database.Cursor;
+import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
+import android.hardware.camera2.utils.ArrayUtils;
import android.net.Uri;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
import android.os.DropBoxManager;
-import android.os.FileObserver;
+import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.provider.Settings.Secure;
import android.text.TextUtils;
-import android.util.Log;
-import android.util.LruCache;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
-
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.os.BackgroundThread;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import com.android.providers.settings.SettingsState.Setting;
+
+/**
+ * <p>
+ * This class is a content provider that publishes the system settings.
+ * It can be accessed via the content provider APIs or via custom call
+ * commands. The latter is a bit faster and is the preferred way to access
+ * the platform settings.
+ * </p>
+ * <p>
+ * There are three settings types, global (with signature level protection
+ * and shared across users), secure (with signature permission level
+ * protection and per user), and system (with dangerous permission level
+ * protection and per user). Global settings are stored under the device owner.
+ * Each of these settings is represented by a {@link
+ * com.android.providers.settings.SettingsState} object mapped to an integer
+ * key derived from the setting type in the most significant bits and user
+ * id in the least significant bits. Settings are synchronously loaded on
+ * instantiation of a SettingsState and asynchronously persisted on mutation.
+ * Settings are stored in the user specific system directory.
+ * </p>
+ * <p>
+ * Apps targeting APIs Lollipop MR1 and lower can add custom settings entries
+ * and get a warning. Targeting higher API version prohibits this as the
+ * system settings are not a place for apps to save their state. When a package
+ * is removed the settings it added are deleted. Apps cannot delete system
+ * settings added by the platform. System settings values are validated to
+ * ensure the clients do not put bad values. Global and secure settings are
+ * changed only by trusted parties, therefore no validation is performed. Also
+ * there is a limit on the amount of app specific settings that can be added
+ * to prevent unlimited growth of the system process memory footprint.
+ * </p>
+ */
+@SuppressWarnings("deprecation")
public class SettingsProvider extends ContentProvider {
- private static final String TAG = "SettingsProvider";
- private static final boolean LOCAL_LOGV = false;
+ private static final boolean DEBUG = false;
+
+ private static final boolean DROP_DATABASE_ON_MIGRATION = !Build.IS_DEBUGGABLE;
- private static final boolean USER_CHECK_THROWS = true;
+ private static final String LOG_TAG = "SettingsProvider";
private static final String TABLE_SYSTEM = "system";
private static final String TABLE_SECURE = "secure";
private static final String TABLE_GLOBAL = "global";
+
+ // Old tables no longer exist.
private static final String TABLE_FAVORITES = "favorites";
private static final String TABLE_OLD_FAVORITES = "old_favorites";
+ private static final String TABLE_BLUETOOTH_DEVICES = "bluetooth_devices";
+ private static final String TABLE_BOOKMARKS = "bookmarks";
+ private static final String TABLE_ANDROID_METADATA = "android_metadata";
- private static final String[] COLUMN_VALUE = new String[] { "value" };
+ // The set of removed legacy tables.
+ private static final Set<String> REMOVED_LEGACY_TABLES = new ArraySet<>();
+ static {
+ REMOVED_LEGACY_TABLES.add(TABLE_FAVORITES);
+ REMOVED_LEGACY_TABLES.add(TABLE_OLD_FAVORITES);
+ REMOVED_LEGACY_TABLES.add(TABLE_BLUETOOTH_DEVICES);
+ REMOVED_LEGACY_TABLES.add(TABLE_BOOKMARKS);
+ REMOVED_LEGACY_TABLES.add(TABLE_ANDROID_METADATA);
+ }
- // Caches for each user's settings, access-ordered for acting as LRU.
- // Guarded by themselves.
- private static final int MAX_CACHE_ENTRIES = 200;
- private static final SparseArray<SettingsCache> sSystemCaches
- = new SparseArray<SettingsCache>();
- private static final SparseArray<SettingsCache> sSecureCaches
- = new SparseArray<SettingsCache>();
- private static final SettingsCache sGlobalCache = new SettingsCache(TABLE_GLOBAL);
+ private static final int MUTATION_OPERATION_INSERT = 1;
+ private static final int MUTATION_OPERATION_DELETE = 2;
+ private static final int MUTATION_OPERATION_UPDATE = 3;
- // The count of how many known (handled by SettingsProvider)
- // database mutations are currently being handled for this user.
- // Used by file observers to not reload the database when it's ourselves
- // modifying it.
- private static final SparseArray<AtomicInteger> sKnownMutationsInFlight
- = new SparseArray<AtomicInteger>();
+ private static final String[] ALL_COLUMNS = new String[] {
+ Settings.NameValueTable._ID,
+ Settings.NameValueTable.NAME,
+ Settings.NameValueTable.VALUE
+ };
- // Each defined user has their own settings
- protected final SparseArray<DatabaseHelper> mOpenHelpers = new SparseArray<DatabaseHelper>();
+ private static final Bundle NULL_SETTING = Bundle.forPair(Settings.NameValueTable.VALUE, null);
- // Keep the list of managed profiles synced here
- private List<UserInfo> mManagedProfiles = null;
+ // Per user settings that cannot be modified if associated user restrictions are enabled.
+ private static final Map<String, String> sSettingToUserRestrictionMap = new ArrayMap<>();
+ static {
+ sSettingToUserRestrictionMap.put(Settings.Secure.LOCATION_MODE,
+ UserManager.DISALLOW_SHARE_LOCATION);
+ sSettingToUserRestrictionMap.put(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+ UserManager.DISALLOW_SHARE_LOCATION);
+ sSettingToUserRestrictionMap.put(Settings.Secure.INSTALL_NON_MARKET_APPS,
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
+ sSettingToUserRestrictionMap.put(Settings.Global.ADB_ENABLED,
+ UserManager.DISALLOW_DEBUGGING_FEATURES);
+ sSettingToUserRestrictionMap.put(Settings.Global.PACKAGE_VERIFIER_ENABLE,
+ UserManager.ENSURE_VERIFY_APPS);
+ sSettingToUserRestrictionMap.put(Settings.Global.PREFERRED_NETWORK_MODE,
+ UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
+ }
- // Over this size we don't reject loading or saving settings but
- // we do consider them broken/malicious and don't keep them in
- // memory at least:
- private static final int MAX_CACHE_ENTRY_SIZE = 500;
+ // Per user secure settings that moved to the for all users global settings.
+ static final Set<String> sSecureMovedToGlobalSettings = new ArraySet<>();
+ static {
+ Settings.Secure.getMovedToGlobalSettings(sSecureMovedToGlobalSettings);
+ }
- private static final Bundle NULL_SETTING = Bundle.forPair("value", null);
+ // Per user system settings that moved to the for all users global settings.
+ static final Set<String> sSystemMovedToGlobalSettings = new ArraySet<>();
+ static {
+ Settings.System.getMovedToGlobalSettings(sSystemMovedToGlobalSettings);
+ }
- // Used as a sentinel value in an instance equality test when we
- // want to cache the existence of a key, but not store its value.
- private static final Bundle TOO_LARGE_TO_CACHE_MARKER = Bundle.forPair("_dummy", null);
+ // Per user system settings that moved to the per user secure settings.
+ static final Set<String> sSystemMovedToSecureSettings = new ArraySet<>();
+ static {
+ Settings.System.getMovedToSecureSettings(sSystemMovedToSecureSettings);
+ }
- private UserManager mUserManager;
- private BackupManager mBackupManager;
+ // Per all users global settings that moved to the per user secure settings.
+ static final Set<String> sGlobalMovedToSecureSettings = new ArraySet<>();
+ static {
+ Settings.Global.getMovedToSecureSettings(sGlobalMovedToSecureSettings);
+ }
- /**
- * Settings which need to be treated as global/shared in multi-user environments.
- */
- static final HashSet<String> sSecureGlobalKeys;
- static final HashSet<String> sSystemGlobalKeys;
+ // Per user secure settings that are cloned for the managed profiles of the user.
+ private static final Set<String> sSecureCloneToManagedSettings = new ArraySet<>();
+ static {
+ Settings.Secure.getCloneToManagedProfileSettings(sSecureCloneToManagedSettings);
+ }
- // Settings that cannot be modified if associated user restrictions are enabled.
- static final Map<String, String> sRestrictedKeys;
+ // Per user system settings that are cloned for the managed profiles of the user.
+ private static final Set<String> sSystemCloneToManagedSettings = new ArraySet<>();
+ static {
+ Settings.System.getCloneToManagedProfileSettings(sSystemCloneToManagedSettings);
+ }
- private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid";
+ private final Object mLock = new Object();
- static final HashSet<String> sSecureCloneToManagedKeys;
- static final HashSet<String> sSystemCloneToManagedKeys;
+ @GuardedBy("mLock")
+ private SettingsRegistry mSettingsRegistry;
- static {
- // Keys (name column) from the 'secure' table that are now in the owner user's 'global'
- // table, shared across all users
- // These must match Settings.Secure.MOVED_TO_GLOBAL
- sSecureGlobalKeys = new HashSet<String>();
- Settings.Secure.getMovedKeys(sSecureGlobalKeys);
-
- // Keys from the 'system' table now moved to 'global'
- // These must match Settings.System.MOVED_TO_GLOBAL
- sSystemGlobalKeys = new HashSet<String>();
- Settings.System.getNonLegacyMovedKeys(sSystemGlobalKeys);
-
- sRestrictedKeys = new HashMap<String, String>();
- sRestrictedKeys.put(Settings.Secure.LOCATION_MODE, UserManager.DISALLOW_SHARE_LOCATION);
- sRestrictedKeys.put(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
- UserManager.DISALLOW_SHARE_LOCATION);
- sRestrictedKeys.put(Settings.Secure.INSTALL_NON_MARKET_APPS,
- UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
- sRestrictedKeys.put(Settings.Global.ADB_ENABLED, UserManager.DISALLOW_DEBUGGING_FEATURES);
- sRestrictedKeys.put(Settings.Global.PACKAGE_VERIFIER_ENABLE,
- UserManager.ENSURE_VERIFY_APPS);
- sRestrictedKeys.put(Settings.Global.PREFERRED_NETWORK_MODE,
- UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
+ @GuardedBy("mLock")
+ private UserManager mUserManager;
+
+ @GuardedBy("mLock")
+ private AppOpsManager mAppOpsManager;
- sSecureCloneToManagedKeys = new HashSet<String>();
- for (int i = 0; i < Settings.Secure.CLONE_TO_MANAGED_PROFILE.length; i++) {
- sSecureCloneToManagedKeys.add(Settings.Secure.CLONE_TO_MANAGED_PROFILE[i]);
+ @GuardedBy("mLock")
+ private PackageManager mPackageManager;
+
+ @Override
+ public boolean onCreate() {
+ synchronized (mLock) {
+ mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
+ mAppOpsManager = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
+ mPackageManager = getContext().getPackageManager();
+ mSettingsRegistry = new SettingsRegistry();
}
- sSystemCloneToManagedKeys = new HashSet<String>();
- for (int i = 0; i < Settings.System.CLONE_TO_MANAGED_PROFILE.length; i++) {
- sSystemCloneToManagedKeys.add(Settings.System.CLONE_TO_MANAGED_PROFILE[i]);
+ registerBroadcastReceivers();
+ return true;
+ }
+
+ @Override
+ public Bundle call(String method, String name, Bundle args) {
+ synchronized (mLock) {
+ final int requestingUserId = getRequestingUserId(args);
+ switch (method) {
+ case Settings.CALL_METHOD_GET_GLOBAL: {
+ Setting setting = getGlobalSettingLocked(name);
+ return packageValueForCallResult(setting);
+ }
+
+ case Settings.CALL_METHOD_GET_SECURE: {
+ Setting setting = getSecureSettingLocked(name, requestingUserId);
+ return packageValueForCallResult(setting);
+ }
+
+ case Settings.CALL_METHOD_GET_SYSTEM: {
+ Setting setting = getSystemSettingLocked(name, requestingUserId);
+ return packageValueForCallResult(setting);
+ }
+
+ case Settings.CALL_METHOD_PUT_GLOBAL: {
+ String value = getSettingValue(args);
+ insertGlobalSettingLocked(name, value, requestingUserId);
+ } break;
+
+ case Settings.CALL_METHOD_PUT_SECURE: {
+ String value = getSettingValue(args);
+ insertSecureSettingLocked(name, value, requestingUserId);
+ } break;
+
+ case Settings.CALL_METHOD_PUT_SYSTEM: {
+ String value = getSettingValue(args);
+ insertSystemSettingLocked(name, value, requestingUserId);
+ } break;
+
+ default: {
+ Slog.w(LOG_TAG, "call() with invalid method: " + method);
+ } break;
+ }
}
+ return null;
}
- private boolean settingMovedToGlobal(final String name) {
- return sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name);
+ @Override
+ public String getType(Uri uri) {
+ Arguments args = new Arguments(uri, null, null, true);
+ if (TextUtils.isEmpty(args.name)) {
+ return "vnd.android.cursor.dir/" + args.table;
+ } else {
+ return "vnd.android.cursor.item/" + args.table;
+ }
}
- /**
- * Decode a content URL into the table, projection, and arguments
- * used to access the corresponding database rows.
- */
- private static class SqlArguments {
- public String table;
- public final String where;
- public final String[] args;
-
- /** Operate on existing rows. */
- SqlArguments(Uri url, String where, String[] args) {
- if (url.getPathSegments().size() == 1) {
- // of the form content://settings/secure, arbitrary where clause
- this.table = url.getPathSegments().get(0);
- if (!DatabaseHelper.isValidTable(this.table)) {
- throw new IllegalArgumentException("Bad root path: " + this.table);
+ @Override
+ public Cursor query(Uri uri, String[] projection, String where, String[] whereArgs,
+ String order) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "query() for user: " + UserHandle.getCallingUserId());
+ }
+
+ Arguments args = new Arguments(uri, where, whereArgs, true);
+ String[] normalizedProjection = normalizeProjection(projection);
+
+ // If a legacy table that is gone, done.
+ if (REMOVED_LEGACY_TABLES.contains(args.table)) {
+ return new MatrixCursor(normalizedProjection, 0);
+ }
+
+ synchronized (mLock) {
+ switch (args.table) {
+ case TABLE_GLOBAL: {
+ if (args.name != null) {
+ Setting setting = getGlobalSettingLocked(args.name);
+ return packageSettingForQuery(setting, normalizedProjection);
+ } else {
+ return getAllGlobalSettingsLocked(projection);
+ }
}
- this.where = where;
- this.args = args;
- } else if (url.getPathSegments().size() != 2) {
- throw new IllegalArgumentException("Invalid URI: " + url);
- } else if (!TextUtils.isEmpty(where)) {
- throw new UnsupportedOperationException("WHERE clause not supported: " + url);
- } else {
- // of the form content://settings/secure/element_name, no where clause
- this.table = url.getPathSegments().get(0);
- if (!DatabaseHelper.isValidTable(this.table)) {
- throw new IllegalArgumentException("Bad root path: " + this.table);
+
+ case TABLE_SECURE: {
+ final int userId = UserHandle.getCallingUserId();
+ if (args.name != null) {
+ Setting setting = getSecureSettingLocked(args.name, userId);
+ return packageSettingForQuery(setting, normalizedProjection);
+ } else {
+ return getAllSecureSettingsLocked(userId, projection);
+ }
}
- if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table) ||
- TABLE_GLOBAL.equals(this.table)) {
- this.where = Settings.NameValueTable.NAME + "=?";
- final String name = url.getPathSegments().get(1);
- this.args = new String[] { name };
- // Rewrite the table for known-migrated names
- if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table)) {
- if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) {
- this.table = TABLE_GLOBAL;
- }
+
+ case TABLE_SYSTEM: {
+ final int userId = UserHandle.getCallingUserId();
+ if (args.name != null) {
+ Setting setting = getSystemSettingLocked(args.name, userId);
+ return packageSettingForQuery(setting, normalizedProjection);
+ } else {
+ return getAllSystemSettingsLocked(userId, projection);
}
- } else {
- // of the form content://bookmarks/19
- this.where = "_id=" + ContentUris.parseId(url);
- this.args = null;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Invalid Uri path:" + uri);
}
}
}
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "insert() for user: " + UserHandle.getCallingUserId());
+ }
+
+ String table = getValidTableOrThrow(uri);
+
+ // If a legacy table that is gone, done.
+ if (REMOVED_LEGACY_TABLES.contains(table)) {
+ return null;
+ }
+
+ String name = values.getAsString(Settings.Secure.NAME);
+ if (TextUtils.isEmpty(name)) {
+ return null;
+ }
- /** Insert new rows (no where clause allowed). */
- SqlArguments(Uri url) {
- if (url.getPathSegments().size() == 1) {
- this.table = url.getPathSegments().get(0);
- if (!DatabaseHelper.isValidTable(this.table)) {
- throw new IllegalArgumentException("Bad root path: " + this.table);
+ String value = values.getAsString(Settings.Secure.VALUE);
+
+ synchronized (mLock) {
+ switch (table) {
+ case TABLE_GLOBAL: {
+ if (insertGlobalSettingLocked(name, value, UserHandle.getCallingUserId())) {
+ return Uri.withAppendedPath(Settings.Global.CONTENT_URI, name);
+ }
+ } break;
+
+ case TABLE_SECURE: {
+ if (insertSecureSettingLocked(name, value, UserHandle.getCallingUserId())) {
+ return Uri.withAppendedPath(Settings.Secure.CONTENT_URI, name);
+ }
+ } break;
+
+ case TABLE_SYSTEM: {
+ if (insertSystemSettingLocked(name, value, UserHandle.getCallingUserId())) {
+ return Uri.withAppendedPath(Settings.System.CONTENT_URI, name);
+ }
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Bad Uri path:" + uri);
}
- this.where = null;
- this.args = null;
- } else {
- throw new IllegalArgumentException("Invalid URI: " + url);
}
}
+
+ return null;
}
- /**
- * Get the content URI of a row added to a table.
- * @param tableUri of the entire table
- * @param values found in the row
- * @param rowId of the row
- * @return the content URI for this particular row
- */
- private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) {
- if (tableUri.getPathSegments().size() != 1) {
- throw new IllegalArgumentException("Invalid URI: " + tableUri);
- }
- String table = tableUri.getPathSegments().get(0);
- if (TABLE_SYSTEM.equals(table) ||
- TABLE_SECURE.equals(table) ||
- TABLE_GLOBAL.equals(table)) {
- String name = values.getAsString(Settings.NameValueTable.NAME);
- return Uri.withAppendedPath(tableUri, name);
- } else {
- return ContentUris.withAppendedId(tableUri, rowId);
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] allValues) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "bulkInsert() for user: " + UserHandle.getCallingUserId());
}
- }
- /**
- * Send a notification when a particular content URI changes.
- * Modify the system property used to communicate the version of
- * this table, for tables which have such a property. (The Settings
- * contract class uses these to provide client-side caches.)
- * @param uri to send notifications for
- */
- private void sendNotify(Uri uri, int userHandle) {
- // Update the system property *first*, so if someone is listening for
- // a notification and then using the contract class to get their data,
- // the system property will be updated and they'll get the new data.
-
- boolean backedUpDataChanged = false;
- String property = null, table = uri.getPathSegments().get(0);
- final boolean isGlobal = table.equals(TABLE_GLOBAL);
- if (table.equals(TABLE_SYSTEM)) {
- property = Settings.System.SYS_PROP_SETTING_VERSION;
- backedUpDataChanged = true;
- } else if (table.equals(TABLE_SECURE)) {
- property = Settings.Secure.SYS_PROP_SETTING_VERSION;
- backedUpDataChanged = true;
- } else if (isGlobal) {
- property = Settings.Global.SYS_PROP_SETTING_VERSION; // this one is global
- backedUpDataChanged = true;
- }
-
- 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));
- }
-
- // Inform the backup manager about a data change
- if (backedUpDataChanged) {
- mBackupManager.dataChanged();
- }
- // Now send the notification through the content framework.
-
- String notify = uri.getQueryParameter("notify");
- if (notify == null || "true".equals(notify)) {
- final int notifyTarget = isGlobal ? UserHandle.USER_ALL : userHandle;
- final long oldId = Binder.clearCallingIdentity();
- try {
- getContext().getContentResolver().notifyChange(uri, null, true, notifyTarget);
- } finally {
- Binder.restoreCallingIdentity(oldId);
+ int insertionCount = 0;
+ final int valuesCount = allValues.length;
+ for (int i = 0; i < valuesCount; i++) {
+ ContentValues values = allValues[i];
+ if (insert(uri, values) != null) {
+ insertionCount++;
}
- if (LOCAL_LOGV) Log.v(TAG, "notifying for " + notifyTarget + ": " + uri);
- } else {
- if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri);
}
+
+ return insertionCount;
}
- /**
- * Make sure the caller has permission to write this data.
- * @param args supplied by the caller
- * @throws SecurityException if the caller is forbidden to write.
- */
- private void checkWritePermissions(SqlArguments args) {
- if ((TABLE_SECURE.equals(args.table) || TABLE_GLOBAL.equals(args.table)) &&
- getContext().checkCallingOrSelfPermission(
- android.Manifest.permission.WRITE_SECURE_SETTINGS) !=
- PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException(
- String.format("Permission denial: writing to secure settings requires %1$s",
- android.Manifest.permission.WRITE_SECURE_SETTINGS));
+ @Override
+ public int delete(Uri uri, String where, String[] whereArgs) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "delete() for user: " + UserHandle.getCallingUserId());
}
- }
- private void checkUserRestrictions(String setting, int userId) {
- String userRestriction = sRestrictedKeys.get(setting);
- if (!TextUtils.isEmpty(userRestriction)
- && mUserManager.hasUserRestriction(userRestriction, new UserHandle(userId))) {
- throw new SecurityException(
- "Permission denial: user is restricted from changing this setting.");
+ Arguments args = new Arguments(uri, where, whereArgs, false);
+
+ // If a legacy table that is gone, done.
+ if (REMOVED_LEGACY_TABLES.contains(args.table)) {
+ return 0;
}
- }
- // FileObserver for external modifications to the database file.
- // Note that this is for platform developers only with
- // userdebug/eng builds who should be able to tinker with the
- // sqlite database out from under the SettingsProvider, which is
- // normally the exclusive owner of the database. But we keep this
- // enabled all the time to minimize development-vs-user
- // differences in testing.
- private static SparseArray<SettingsFileObserver> sObserverInstances
- = new SparseArray<SettingsFileObserver>();
- private class SettingsFileObserver extends FileObserver {
- private final AtomicBoolean mIsDirty = new AtomicBoolean(false);
- private final int mUserHandle;
- private final String mPath;
-
- public SettingsFileObserver(int userHandle, String path) {
- super(path, FileObserver.CLOSE_WRITE |
- FileObserver.CREATE | FileObserver.DELETE |
- FileObserver.MOVED_TO | FileObserver.MODIFY);
- mUserHandle = userHandle;
- mPath = path;
- }
-
- public void onEvent(int event, String path) {
- final AtomicInteger mutationCount;
- synchronized (SettingsProvider.this) {
- mutationCount = sKnownMutationsInFlight.get(mUserHandle);
- }
- if (mutationCount != null && mutationCount.get() > 0) {
- // our own modification.
- return;
- }
- Log.d(TAG, "User " + mUserHandle + " external modification to " + mPath
- + "; event=" + event);
- if (!mIsDirty.compareAndSet(false, true)) {
- // already handled. (we get a few update events
- // during an sqlite write)
- return;
+ if (TextUtils.isEmpty(args.name)) {
+ return 0;
+ }
+
+ synchronized (mLock) {
+ switch (args.table) {
+ case TABLE_GLOBAL: {
+ final int userId = UserHandle.getCallingUserId();
+ return deleteGlobalSettingLocked(args.name, userId) ? 1 : 0;
+ }
+
+ case TABLE_SECURE: {
+ final int userId = UserHandle.getCallingUserId();
+ return deleteSecureSettingLocked(args.name, userId) ? 1 : 0;
+ }
+
+ case TABLE_SYSTEM: {
+ final int userId = UserHandle.getCallingUserId();
+ return deleteSystemSettingLocked(args.name, userId) ? 1 : 0;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Bad Uri path:" + uri);
+ }
}
- Log.d(TAG, "User " + mUserHandle + " updating our caches for " + mPath);
- fullyPopulateCaches(mUserHandle);
- mIsDirty.set(false);
}
}
@Override
- public boolean onCreate() {
- mBackupManager = new BackupManager(getContext());
- mUserManager = UserManager.get(getContext());
+ public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "update() for user: " + UserHandle.getCallingUserId());
+ }
+
+ Arguments args = new Arguments(uri, where, whereArgs, false);
+
+ // If a legacy table that is gone, done.
+ if (REMOVED_LEGACY_TABLES.contains(args.table)) {
+ return 0;
+ }
+
+ String value = values.getAsString(Settings.Secure.VALUE);
+ if (TextUtils.isEmpty(value)) {
+ return 0;
+ }
+
+ synchronized (mLock) {
+ switch (args.table) {
+ case TABLE_GLOBAL: {
+ final int userId = UserHandle.getCallingUserId();
+ return updateGlobalSettingLocked(args.name, value, userId) ? 1 : 0;
+ }
+
+ case TABLE_SECURE: {
+ final int userId = UserHandle.getCallingUserId();
+ return updateSecureSettingLocked(args.name, value, userId) ? 1 : 0;
+ }
+
+ case TABLE_SYSTEM: {
+ final int userId = UserHandle.getCallingUserId();
+ return updateSystemSettingLocked(args.name, value, userId) ? 1 : 0;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Invalid Uri path:" + uri);
+ }
+ }
+ }
+ }
- setAppOps(AppOpsManager.OP_NONE, AppOpsManager.OP_WRITE_SETTINGS);
- establishDbTracking(UserHandle.USER_OWNER);
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ throw new FileNotFoundException("Direct file access no longer supported; "
+ + "ringtone playback is available through android.media.Ringtone");
+ }
+ private void registerBroadcastReceivers() {
IntentFilter userFilter = new IntentFilter();
userFilter.addAction(Intent.ACTION_USER_REMOVED);
- userFilter.addAction(Intent.ACTION_USER_ADDED);
+ userFilter.addAction(Intent.ACTION_USER_STOPPED);
+
getContext().registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
UserHandle.USER_OWNER);
- if (intent.getAction().equals(Intent.ACTION_USER_REMOVED)) {
- onUserRemoved(userHandle);
- } else if (intent.getAction().equals(Intent.ACTION_USER_ADDED)) {
- onProfilesChanged();
+
+ switch (intent.getAction()) {
+ case Intent.ACTION_USER_REMOVED: {
+ mSettingsRegistry.removeUserStateLocked(userId, true);
+ } break;
+
+ case Intent.ACTION_USER_STOPPED: {
+ mSettingsRegistry.removeUserStateLocked(userId, false);
+ } break;
}
}
}, userFilter);
- onProfilesChanged();
+ PackageMonitor monitor = new PackageMonitor() {
+ @Override
+ public void onPackageRemoved(String packageName, int uid) {
+ synchronized (mLock) {
+ mSettingsRegistry.onPackageRemovedLocked(packageName,
+ UserHandle.getUserId(uid));
+ }
+ }
+ };
- return true;
+ // package changes
+ monitor.register(getContext(), BackgroundThread.getHandler().getLooper(),
+ UserHandle.ALL, true);
}
- void onUserRemoved(int userHandle) {
- synchronized (this) {
- // the db file itself will be deleted automatically, but we need to tear down
- // our caches and other internal bookkeeping.
- FileObserver observer = sObserverInstances.get(userHandle);
- if (observer != null) {
- observer.stopWatching();
- sObserverInstances.delete(userHandle);
- }
+ private Cursor getAllGlobalSettingsLocked(String[] projection) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "getAllGlobalSettingsLocked()");
+ }
+
+ // Get the settings.
+ SettingsState settingsState = mSettingsRegistry.getSettingsLocked(
+ SettingsRegistry.SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+
+ List<String> names = settingsState.getSettingNamesLocked();
- mOpenHelpers.delete(userHandle);
- sSystemCaches.delete(userHandle);
- sSecureCaches.delete(userHandle);
- sKnownMutationsInFlight.delete(userHandle);
- onProfilesChanged();
+ final int nameCount = names.size();
+
+ String[] normalizedProjection = normalizeProjection(projection);
+ MatrixCursor result = new MatrixCursor(normalizedProjection, nameCount);
+
+ // Anyone can get the global settings, so no security checks.
+ for (int i = 0; i < nameCount; i++) {
+ String name = names.get(i);
+ Setting setting = settingsState.getSettingLocked(name);
+ appendSettingToCursor(result, setting);
}
+
+ return result;
}
- /**
- * Updates the list of managed profiles. It assumes that only the primary user
- * can have managed profiles. Modify this code if that changes in the future.
- */
- void onProfilesChanged() {
- synchronized (this) {
- mManagedProfiles = mUserManager.getProfiles(UserHandle.USER_OWNER);
- if (mManagedProfiles != null) {
- // Remove the primary user from the list
- for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
- if (mManagedProfiles.get(i).id == UserHandle.USER_OWNER) {
- mManagedProfiles.remove(i);
- }
- }
- // If there are no managed profiles, reset the variable
- if (mManagedProfiles.size() == 0) {
- mManagedProfiles = null;
- }
+ private Setting getGlobalSettingLocked(String name) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "getGlobalSetting(" + name + ")");
+ }
+
+ // Get the value.
+ return mSettingsRegistry.getSettingLocked(SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+ UserHandle.USER_OWNER, name);
+ }
+
+ private boolean updateGlobalSettingLocked(String name, String value, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "updateGlobalSettingLocked(" + name + ", " + value + ")");
+ }
+ return mutateGlobalSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_UPDATE);
+ }
+
+ private boolean insertGlobalSettingLocked(String name, String value, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "insertGlobalSettingLocked(" + name + ", " + value + ")");
+ }
+ return mutateGlobalSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT);
+ }
+
+ private boolean deleteGlobalSettingLocked(String name, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "deleteGlobalSettingLocked(" + name + ")");
+ }
+ return mutateGlobalSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE);
+ }
+
+ private boolean mutateGlobalSettingLocked(String name, String value, int requestingUserId,
+ int operation) {
+ // Make sure the caller can change the settings - treated as secure.
+ enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
+
+ // Verify whether this operation is allowed for the calling package.
+ if (!isAppOpWriteSettingsAllowedForCallingPackage()) {
+ return false;
+ }
+
+ // Resolve the userId on whose behalf the call is made.
+ final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+
+ // If this is a setting that is currently restricted for this user, done.
+ if (isGlobalOrSecureSettingRestrictedForUser(name, callingUserId)) {
+ return false;
+ }
+
+ // Perform the mutation.
+ switch (operation) {
+ case MUTATION_OPERATION_INSERT: {
+ return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+ UserHandle.USER_OWNER, name, value, getCallingPackage());
}
- if (LOCAL_LOGV) {
- Slog.d(TAG, "Managed Profiles = " + mManagedProfiles);
+
+ case MUTATION_OPERATION_DELETE: {
+ return mSettingsRegistry.deleteSettingLocked(
+ SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+ UserHandle.USER_OWNER, name);
+ }
+
+ case MUTATION_OPERATION_UPDATE: {
+ return mSettingsRegistry.updateSettingLocked(SettingsRegistry.SETTINGS_TYPE_GLOBAL,
+ UserHandle.USER_OWNER, name, value, getCallingPackage());
}
}
+
+ return false;
}
- private void establishDbTracking(int userHandle) {
- if (LOCAL_LOGV) {
- Slog.i(TAG, "Installing settings db helper and caches for user " + userHandle);
+ private Cursor getAllSecureSettingsLocked(int userId, String[] projection) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "getAllSecureSettings(" + userId + ")");
}
- DatabaseHelper dbhelper;
+ // Resolve the userId on whose behalf the call is made.
+ final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(userId);
+
+ List<String> names = mSettingsRegistry.getSettingsNamesLocked(
+ SettingsRegistry.SETTINGS_TYPE_SECURE, callingUserId);
+
+ final int nameCount = names.size();
- synchronized (this) {
- dbhelper = mOpenHelpers.get(userHandle);
- if (dbhelper == null) {
- dbhelper = new DatabaseHelper(getContext(), userHandle);
- mOpenHelpers.append(userHandle, dbhelper);
+ String[] normalizedProjection = normalizeProjection(projection);
+ MatrixCursor result = new MatrixCursor(normalizedProjection, nameCount);
- sSystemCaches.append(userHandle, new SettingsCache(TABLE_SYSTEM));
- sSecureCaches.append(userHandle, new SettingsCache(TABLE_SECURE));
- sKnownMutationsInFlight.append(userHandle, new AtomicInteger(0));
+ for (int i = 0; i < nameCount; i++) {
+ String name = names.get(i);
+
+ // Determine the owning user as some profile settings are cloned from the parent.
+ final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+
+ // Special case for location (sigh).
+ if (isLocationProvidersAllowedRestricted(name, callingUserId, owningUserId)) {
+ return null;
}
+
+ Setting setting = mSettingsRegistry.getSettingLocked(
+ SettingsRegistry.SETTINGS_TYPE_SECURE, owningUserId, name);
+ appendSettingToCursor(result, setting);
}
- // 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 = dbhelper.getWritableDatabase();
-
- // Watch for external modifications to the database files,
- // keeping our caches in sync. We synchronize the observer set
- // separately, and of course it has to run after the db file
- // itself was set up by the DatabaseHelper.
- synchronized (sObserverInstances) {
- if (sObserverInstances.get(userHandle) == null) {
- SettingsFileObserver observer = new SettingsFileObserver(userHandle, db.getPath());
- sObserverInstances.append(userHandle, observer);
- observer.startWatching();
- }
+ return result;
+ }
+
+ private Setting getSecureSettingLocked(String name, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "getSecureSetting(" + name + ", " + requestingUserId + ")");
}
- ensureAndroidIdIsSet(userHandle);
+ // Resolve the userId on whose behalf the call is made.
+ final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
- startAsyncCachePopulation(userHandle);
- }
+ // Determine the owning user as some profile settings are cloned from the parent.
+ final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+
+ // Special case for location (sigh).
+ if (isLocationProvidersAllowedRestricted(name, callingUserId, owningUserId)) {
+ return null;
+ }
- class CachePrefetchThread extends Thread {
- private int mUserHandle;
+ // Get the value.
+ return mSettingsRegistry.getSettingLocked(SettingsRegistry.SETTINGS_TYPE_SECURE,
+ owningUserId, name);
+ }
- CachePrefetchThread(int userHandle) {
- super("populate-settings-caches");
- mUserHandle = userHandle;
+ private boolean insertSecureSettingLocked(String name, String value, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "insertSecureSettingLocked(" + name + ", " + value + ", "
+ + requestingUserId + ")");
}
- @Override
- public void run() {
- fullyPopulateCaches(mUserHandle);
+ return mutateSecureSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT);
+ }
+
+ private boolean deleteSecureSettingLocked(String name, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "deleteSecureSettingLocked(" + name + ", " + requestingUserId + ")");
}
+
+ return mutateSecureSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE);
}
- private void startAsyncCachePopulation(int userHandle) {
- new CachePrefetchThread(userHandle).start();
+ private boolean updateSecureSettingLocked(String name, String value, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "updateSecureSettingLocked(" + name + ", " + value + ", "
+ + requestingUserId + ")");
+ }
+
+ return mutateSecureSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_UPDATE);
}
- private void fullyPopulateCaches(final int userHandle) {
- DatabaseHelper dbHelper;
- synchronized (this) {
- dbHelper = mOpenHelpers.get(userHandle);
+ private boolean mutateSecureSettingLocked(String name, String value, int requestingUserId,
+ int operation) {
+ // Make sure the caller can change the settings.
+ enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
+
+ // Verify whether this operation is allowed for the calling package.
+ if (!isAppOpWriteSettingsAllowedForCallingPackage()) {
+ return false;
}
- if (dbHelper == null) {
- // User is gone.
- return;
+
+ // Resolve the userId on whose behalf the call is made.
+ final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+
+ // If this is a setting that is currently restricted for this user, done.
+ if (isGlobalOrSecureSettingRestrictedForUser(name, callingUserId)) {
+ return false;
}
- // Only populate the globals cache once, for the owning user
- if (userHandle == UserHandle.USER_OWNER) {
- fullyPopulateCache(dbHelper, TABLE_GLOBAL, sGlobalCache);
+
+ // Determine the owning user as some profile settings are cloned from the parent.
+ final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+
+ // Only the owning user can change the setting.
+ if (owningUserId != callingUserId) {
+ return false;
}
- fullyPopulateCache(dbHelper, TABLE_SECURE, sSecureCaches.get(userHandle));
- fullyPopulateCache(dbHelper, TABLE_SYSTEM, sSystemCaches.get(userHandle));
- }
- // Slurp all values (if sane in number & size) into cache.
- private void fullyPopulateCache(DatabaseHelper dbHelper, String table, SettingsCache cache) {
- SQLiteDatabase db = dbHelper.getReadableDatabase();
- Cursor c = db.query(
- table,
- new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE },
- null, null, null, null, null,
- "" + (MAX_CACHE_ENTRIES + 1) /* limit */);
- try {
- synchronized (cache) {
- cache.evictAll();
- cache.setFullyMatchesDisk(true); // optimistic
- int rows = 0;
- while (c.moveToNext()) {
- rows++;
- String name = c.getString(0);
- String value = c.getString(1);
- cache.populate(name, value);
- }
- if (rows > MAX_CACHE_ENTRIES) {
- // Somewhat redundant, as removeEldestEntry() will
- // have already done this, but to be explicit:
- cache.setFullyMatchesDisk(false);
- Log.d(TAG, "row count exceeds max cache entries for table " + table);
- }
- if (LOCAL_LOGV) Log.d(TAG, "cache for settings table '" + table
- + "' rows=" + rows + "; fullycached=" + cache.fullyMatchesDisk());
+ // Special cases for location providers (sigh).
+ if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
+ return updateLocationProvidersAllowed(value, owningUserId);
+ }
+
+ // Mutate the value.
+ switch(operation) {
+ case MUTATION_OPERATION_INSERT: {
+ return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_SECURE,
+ owningUserId, name, value, getCallingPackage());
+ }
+
+ case MUTATION_OPERATION_DELETE: {
+ return mSettingsRegistry.deleteSettingLocked(
+ SettingsRegistry.SETTINGS_TYPE_SECURE,
+ owningUserId, name);
+ }
+
+ case MUTATION_OPERATION_UPDATE: {
+ return mSettingsRegistry.updateSettingLocked(SettingsRegistry.SETTINGS_TYPE_SECURE,
+ owningUserId, name, value, getCallingPackage());
}
- } finally {
- c.close();
}
+
+ return false;
}
- private boolean ensureAndroidIdIsSet(int userHandle) {
- final Cursor c = queryForUser(Settings.Secure.CONTENT_URI,
- new String[] { Settings.NameValueTable.VALUE },
- Settings.NameValueTable.NAME + "=?",
- new String[] { Settings.Secure.ANDROID_ID }, null,
- userHandle);
- try {
- final String value = c.moveToNext() ? c.getString(0) : null;
- if (value == null) {
- // sanity-check the user before touching the db
- final UserInfo user = mUserManager.getUserInfo(userHandle);
- if (user == null) {
- // can happen due to races when deleting users; treat as benign
- return false;
- }
+ private Cursor getAllSystemSettingsLocked(int userId, String[] projection) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "getAllSecureSystemLocked(" + userId + ")");
+ }
- final SecureRandom random = new SecureRandom();
- final String newAndroidIdValue = Long.toHexString(random.nextLong());
- final ContentValues values = new ContentValues();
- values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID);
- values.put(Settings.NameValueTable.VALUE, newAndroidIdValue);
- final Uri uri = insertForUser(Settings.Secure.CONTENT_URI, values, userHandle);
- if (uri == null) {
- Slog.e(TAG, "Unable to generate new ANDROID_ID for user " + userHandle);
- return false;
- }
- Slog.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue
- + "] for user " + userHandle);
- // Write a dropbox entry if it's a restricted profile
- if (user.isRestricted()) {
- DropBoxManager dbm = (DropBoxManager)
- getContext().getSystemService(Context.DROPBOX_SERVICE);
- if (dbm != null && dbm.isTagEnabled(DROPBOX_TAG_USERLOG)) {
- dbm.addText(DROPBOX_TAG_USERLOG, System.currentTimeMillis()
- + ",restricted_profile_ssaid,"
- + newAndroidIdValue + "\n");
- }
- }
- }
- return true;
- } finally {
- c.close();
+ // Resolve the userId on whose behalf the call is made.
+ final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(userId);
+
+ List<String> names = mSettingsRegistry.getSettingsNamesLocked(
+ SettingsRegistry.SETTINGS_TYPE_SYSTEM, callingUserId);
+
+ final int nameCount = names.size();
+
+ String[] normalizedProjection = normalizeProjection(projection);
+ MatrixCursor result = new MatrixCursor(normalizedProjection, nameCount);
+
+ for (int i = 0; i < nameCount; i++) {
+ String name = names.get(i);
+
+ // Determine the owning user as some profile settings are cloned from the parent.
+ final int owningUserId = resolveOwningUserIdForSystemSettingLocked(callingUserId, name);
+
+ Setting setting = mSettingsRegistry.getSettingLocked(
+ SettingsRegistry.SETTINGS_TYPE_SYSTEM, owningUserId, name);
+ appendSettingToCursor(result, setting);
}
+
+ return result;
}
- // Lazy-initialize the settings caches for non-primary users
- private SettingsCache getOrConstructCache(int callingUser, SparseArray<SettingsCache> which) {
- getOrEstablishDatabase(callingUser); // ignore return value; we don't need it
- return which.get(callingUser);
+ private Setting getSystemSettingLocked(String name, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "getSystemSetting(" + name + ", " + requestingUserId + ")");
+ }
+
+ // Resolve the userId on whose behalf the call is made.
+ final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+
+ // Determine the owning user as some profile settings are cloned from the parent.
+ final int owningUserId = resolveOwningUserIdForSystemSettingLocked(callingUserId, name);
+
+ // Get the value.
+ return mSettingsRegistry.getSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+ owningUserId, name);
}
- // Lazy initialize the database helper and caches for this user, if necessary
- private DatabaseHelper getOrEstablishDatabase(int callingUser) {
- if (callingUser >= Process.SYSTEM_UID) {
- if (USER_CHECK_THROWS) {
- throw new IllegalArgumentException("Uid rather than user handle: " + callingUser);
- } else {
- Slog.wtf(TAG, "establish db for uid rather than user: " + callingUser);
- }
+ private boolean insertSystemSettingLocked(String name, String value, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "insertSystemSettingLocked(" + name + ", " + value + ", "
+ + requestingUserId + ")");
}
- long oldId = Binder.clearCallingIdentity();
- try {
- DatabaseHelper dbHelper;
- synchronized (this) {
- dbHelper = mOpenHelpers.get(callingUser);
+ return mutateSystemSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT);
+ }
+
+ private boolean deleteSystemSettingLocked(String name, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "deleteSystemSettingLocked(" + name + ", " + requestingUserId + ")");
+ }
+
+ return mutateSystemSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE);
+ }
+
+ private boolean updateSystemSettingLocked(String name, String value, int requestingUserId) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "updateSystemSettingLocked(" + name + ", " + value + ", "
+ + requestingUserId + ")");
+ }
+
+ return mutateSystemSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_UPDATE);
+ }
+
+ private boolean mutateSystemSettingLocked(String name, String value, int runAsUserId,
+ int operation) {
+ // Make sure the caller can change the settings.
+ enforceWritePermission(Manifest.permission.WRITE_SETTINGS);
+
+ // Verify whether this operation is allowed for the calling package.
+ if (!isAppOpWriteSettingsAllowedForCallingPackage()) {
+ return false;
+ }
+
+ // Enforce what the calling package can mutate in the system settings.
+ enforceRestrictedSystemSettingsMutationForCallingPackageLocked(operation, name);
+
+ // Resolve the userId on whose behalf the call is made.
+ final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(runAsUserId);
+
+ // Determine the owning user as some profile settings are cloned from the parent.
+ final int owningUserId = resolveOwningUserIdForSystemSettingLocked(callingUserId, name);
+
+ // Only the owning user id can change the setting.
+ if (owningUserId != callingUserId) {
+ return false;
+ }
+
+ // Mutate the value.
+ switch (operation) {
+ case MUTATION_OPERATION_INSERT: {
+ validateSystemSettingValue(name, value);
+ return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+ owningUserId, name, value, getCallingPackage());
}
- if (null == dbHelper) {
- establishDbTracking(callingUser);
- synchronized (this) {
- dbHelper = mOpenHelpers.get(callingUser);
- }
+
+ case MUTATION_OPERATION_DELETE: {
+ return mSettingsRegistry.deleteSettingLocked(
+ SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+ owningUserId, name);
+ }
+
+ case MUTATION_OPERATION_UPDATE: {
+ validateSystemSettingValue(name, value);
+ return mSettingsRegistry.updateSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM,
+ owningUserId, name, value, getCallingPackage());
}
- return dbHelper;
- } finally {
- Binder.restoreCallingIdentity(oldId);
}
+
+ return false;
}
- public SettingsCache cacheForTable(final int callingUser, String tableName) {
- if (TABLE_SYSTEM.equals(tableName)) {
- return getOrConstructCache(callingUser, sSystemCaches);
+ private void validateSystemSettingValue(String name, String value) {
+ Settings.System.Validator validator = Settings.System.VALIDATORS.get(name);
+ if (validator != null && !validator.validate(value)) {
+ throw new IllegalArgumentException("Invalid value: " + value
+ + " for setting: " + name);
}
- if (TABLE_SECURE.equals(tableName)) {
- return getOrConstructCache(callingUser, sSecureCaches);
+ }
+
+ private boolean isLocationProvidersAllowedRestricted(String name, int callingUserId,
+ int owningUserId) {
+ // Optimization - location providers are restricted only for managed profiles.
+ if (callingUserId == owningUserId) {
+ return false;
}
- if (TABLE_GLOBAL.equals(tableName)) {
- return sGlobalCache;
+ if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)
+ && mUserManager.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION,
+ new UserHandle(callingUserId))) {
+ return true;
}
- return null;
+ return false;
}
- /**
- * Used for wiping a whole cache on deletes when we're not
- * sure what exactly was deleted or changed.
- */
- public void invalidateCache(final int callingUser, String tableName) {
- SettingsCache cache = cacheForTable(callingUser, tableName);
- if (cache == null) {
- return;
+ private boolean isGlobalOrSecureSettingRestrictedForUser(String setting, int userId) {
+ String restriction = sSettingToUserRestrictionMap.get(setting);
+ if (restriction == null) {
+ return false;
}
- synchronized (cache) {
- cache.evictAll();
- cache.mCacheFullyMatchesDisk = false;
+ return mUserManager.hasUserRestriction(restriction, new UserHandle(userId));
+ }
+
+ private int resolveOwningUserIdForSecureSettingLocked(int userId, String setting) {
+ return resolveOwningUserIdLocked(userId, sSecureCloneToManagedSettings, setting);
+ }
+
+ private int resolveOwningUserIdForSystemSettingLocked(int userId, String setting) {
+ return resolveOwningUserIdLocked(userId, sSystemCloneToManagedSettings, setting);
+ }
+
+ private int resolveOwningUserIdLocked(int userId, Set<String> keys, String name) {
+ final int parentId = getGroupParentLocked(userId);
+ if (parentId != userId && keys.contains(name)) {
+ return parentId;
}
+ return userId;
}
- /**
- * Checks if the calling user is a managed profile of the primary user.
- * Currently only the primary user (USER_OWNER) can have managed profiles.
- * @param callingUser the user trying to read/write settings
- * @return true if it is a managed profile of the primary user
- */
- private boolean isManagedProfile(int callingUser) {
- synchronized (this) {
- if (mManagedProfiles == null) return false;
- for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
- if (mManagedProfiles.get(i).id == callingUser) {
- return true;
+ private void enforceRestrictedSystemSettingsMutationForCallingPackageLocked(int operation,
+ String name) {
+ // System/root/shell can mutate whatever secure settings they want.
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid == android.os.Process.SYSTEM_UID
+ || callingUid == Process.SHELL_UID
+ || callingUid == Process.ROOT_UID) {
+ return;
+ }
+
+ switch (operation) {
+ case MUTATION_OPERATION_INSERT:
+ // Insert updates.
+ case MUTATION_OPERATION_UPDATE: {
+ if (Settings.System.PUBLIC_SETTINGS.contains(name)) {
+ return;
}
- }
- return false;
+
+ // The calling package is already verified.
+ PackageInfo packageInfo = getCallingPackageInfoOrThrow();
+
+ // Privileged apps can do whatever they want.
+ if ((packageInfo.applicationInfo.privateFlags
+ & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
+ return;
+ }
+
+ warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
+ packageInfo.applicationInfo.targetSdkVersion, name);
+ } break;
+
+ case MUTATION_OPERATION_DELETE: {
+ if (Settings.System.PUBLIC_SETTINGS.contains(name)
+ || Settings.System.PRIVATE_SETTINGS.contains(name)) {
+ throw new IllegalArgumentException("You cannot delete system defined"
+ + " secure settings.");
+ }
+
+ // The calling package is already verified.
+ PackageInfo packageInfo = getCallingPackageInfoOrThrow();
+
+ // Privileged apps can do whatever they want.
+ if ((packageInfo.applicationInfo.privateFlags &
+ ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
+ return;
+ }
+
+ warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
+ packageInfo.applicationInfo.targetSdkVersion, name);
+ } break;
}
}
- /**
- * Fast path that avoids the use of chatty remoted Cursors.
- */
- @Override
- public Bundle call(String method, String request, Bundle args) {
- int callingUser = UserHandle.getCallingUserId();
- if (args != null) {
- int reqUser = args.getInt(Settings.CALL_METHOD_USER_KEY, callingUser);
- if (reqUser != callingUser) {
- callingUser = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
- Binder.getCallingUid(), reqUser, false, true,
- "get/set setting for user", null);
- if (LOCAL_LOGV) Slog.v(TAG, " access setting for user " + callingUser);
- }
+ private PackageInfo getCallingPackageInfoOrThrow() {
+ try {
+ return mPackageManager.getPackageInfo(getCallingPackage(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new IllegalStateException("Calling package doesn't exist");
}
+ }
- // Note: we assume that get/put operations for moved-to-global names have already
- // been directed to the new location on the caller side (otherwise we'd fix them
- // up here).
- DatabaseHelper dbHelper;
- SettingsCache cache;
+ private int getGroupParentLocked(int userId) {
+ // Most frequent use case.
+ if (userId == UserHandle.USER_OWNER) {
+ return userId;
+ }
+ // We are in the same process with the user manager and the returned
+ // user info is a cached instance, so just look up instead of cache.
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ UserInfo userInfo = mUserManager.getProfileParent(userId);
+ return (userInfo != null) ? userInfo.id : userId;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
- // Get methods
- if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) {
- if (LOCAL_LOGV) Slog.v(TAG, "call(system:" + request + ") for " + callingUser);
- // Check if this request should be (re)directed to the primary user's db
- if (callingUser != UserHandle.USER_OWNER
- && shouldShadowParentProfile(callingUser, sSystemCloneToManagedKeys, request)) {
- callingUser = UserHandle.USER_OWNER;
- }
- dbHelper = getOrEstablishDatabase(callingUser);
- cache = sSystemCaches.get(callingUser);
- return lookupValue(dbHelper, TABLE_SYSTEM, cache, request);
- }
- if (Settings.CALL_METHOD_GET_SECURE.equals(method)) {
- if (LOCAL_LOGV) Slog.v(TAG, "call(secure:" + request + ") for " + callingUser);
- // Check if this is a setting to be copied from the primary user
- if (shouldShadowParentProfile(callingUser, sSecureCloneToManagedKeys, request)) {
- // If the request if for location providers and there's a restriction, return none
- if (Secure.LOCATION_PROVIDERS_ALLOWED.equals(request)
- && mUserManager.hasUserRestriction(
- UserManager.DISALLOW_SHARE_LOCATION, new UserHandle(callingUser))) {
- return sSecureCaches.get(callingUser).putIfAbsent(request, "");
- }
- callingUser = UserHandle.USER_OWNER;
- }
- dbHelper = getOrEstablishDatabase(callingUser);
- cache = sSecureCaches.get(callingUser);
- return lookupValue(dbHelper, TABLE_SECURE, cache, request);
- }
- if (Settings.CALL_METHOD_GET_GLOBAL.equals(method)) {
- if (LOCAL_LOGV) Slog.v(TAG, "call(global:" + request + ") for " + callingUser);
- // fast path: owner db & cache are immutable after onCreate() so we need not
- // guard on the attempt to look them up
- return lookupValue(getOrEstablishDatabase(UserHandle.USER_OWNER), TABLE_GLOBAL,
- sGlobalCache, 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(android.Manifest.permission.WRITE_SETTINGS)
+ private boolean isAppOpWriteSettingsAllowedForCallingPackage() {
+ final int callingUid = Binder.getCallingUid();
+
+ mAppOpsManager.checkPackage(Binder.getCallingUid(), getCallingPackage());
+
+ return mAppOpsManager.noteOp(AppOpsManager.OP_WRITE_SETTINGS, callingUid,
+ getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
+ }
+
+ private void enforceWritePermission(String permission) {
+ if (getContext().checkCallingOrSelfPermission(permission)
!= PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException(
- String.format("Permission denial: writing to settings requires %1$s",
- android.Manifest.permission.WRITE_SETTINGS));
+ throw new SecurityException("Permission denial: writing to settings requires:"
+ + permission);
+ }
+ }
+
+ /*
+ * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
+ * This setting contains a list of the currently enabled location providers.
+ * But helper functions in android.providers.Settings can enable or disable
+ * a single provider by using a "+" or "-" prefix before the provider name.
+ *
+ * @returns whether the enabled location providers changed.
+ */
+ private boolean updateLocationProvidersAllowed(String value, int owningUserId) {
+ if (TextUtils.isEmpty(value)) {
+ return false;
}
- // Also need to take care of app op.
- if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SETTINGS, Binder.getCallingUid(),
- getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
- return null;
+ final char prefix = value.charAt(0);
+ if (prefix != '+' && prefix != '-') {
+ return false;
}
- final ContentValues values = new ContentValues();
- values.put(Settings.NameValueTable.NAME, request);
- values.put(Settings.NameValueTable.VALUE, newValue);
- if (Settings.CALL_METHOD_PUT_SYSTEM.equals(method)) {
- if (LOCAL_LOGV) {
- Slog.v(TAG, "call_put(system:" + request + "=" + newValue + ") for "
- + callingUser);
- }
- // Extra check for USER_OWNER to optimize for the 99%
- if (callingUser != UserHandle.USER_OWNER && shouldShadowParentProfile(callingUser,
- sSystemCloneToManagedKeys, request)) {
- // Don't write these settings, as they are cloned from the parent profile
- return null;
- }
- insertForUser(Settings.System.CONTENT_URI, values, callingUser);
- // Clone the settings to the managed profiles so that notifications can be sent out
- if (callingUser == UserHandle.USER_OWNER && mManagedProfiles != null
- && sSystemCloneToManagedKeys.contains(request)) {
- final long token = Binder.clearCallingIdentity();
- try {
- for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
- if (LOCAL_LOGV) {
- Slog.v(TAG, "putting to additional user "
- + mManagedProfiles.get(i).id);
- }
- insertForUser(Settings.System.CONTENT_URI, values,
- mManagedProfiles.get(i).id);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
- } else if (Settings.CALL_METHOD_PUT_SECURE.equals(method)) {
- if (LOCAL_LOGV) {
- Slog.v(TAG, "call_put(secure:" + request + "=" + newValue + ") for "
- + callingUser);
+ // skip prefix
+ value = value.substring(1);
+
+ Setting settingValue = getSecureSettingLocked(
+ Settings.Secure.LOCATION_PROVIDERS_ALLOWED, owningUserId);
+
+ String oldProviders = (settingValue != null) ? settingValue.getValue() : "";
+
+ int index = oldProviders.indexOf(value);
+ int end = index + value.length();
+
+ // check for commas to avoid matching on partial string
+ if (index > 0 && oldProviders.charAt(index - 1) != ',') {
+ index = -1;
+ }
+
+ // check for commas to avoid matching on partial string
+ if (end < oldProviders.length() && oldProviders.charAt(end) != ',') {
+ index = -1;
+ }
+
+ String newProviders;
+
+ if (prefix == '+' && index < 0) {
+ // append the provider to the list if not present
+ if (oldProviders.length() == 0) {
+ newProviders = value;
+ } else {
+ newProviders = oldProviders + ',' + value;
}
- // Extra check for USER_OWNER to optimize for the 99%
- if (callingUser != UserHandle.USER_OWNER && shouldShadowParentProfile(callingUser,
- sSecureCloneToManagedKeys, request)) {
- // Don't write these settings, as they are cloned from the parent profile
- return null;
+ } else if (prefix == '-' && index >= 0) {
+ // remove the provider from the list if present
+ // remove leading or trailing comma
+ if (index > 0) {
+ index--;
+ } else if (end < oldProviders.length()) {
+ end++;
}
- insertForUser(Settings.Secure.CONTENT_URI, values, callingUser);
- // Clone the settings to the managed profiles so that notifications can be sent out
- if (callingUser == UserHandle.USER_OWNER && mManagedProfiles != null
- && sSecureCloneToManagedKeys.contains(request)) {
- final long token = Binder.clearCallingIdentity();
- try {
- for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
- if (LOCAL_LOGV) {
- Slog.v(TAG, "putting to additional user "
- + mManagedProfiles.get(i).id);
- }
- try {
- insertForUser(Settings.Secure.CONTENT_URI, values,
- mManagedProfiles.get(i).id);
- } catch (SecurityException e) {
- // Temporary fix, see b/17450158
- Slog.w(TAG, "Cannot clone request '" + request + "' with value '"
- + newValue + "' to managed profile (id "
- + mManagedProfiles.get(i).id + ")", e);
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
- } else if (Settings.CALL_METHOD_PUT_GLOBAL.equals(method)) {
- if (LOCAL_LOGV) {
- Slog.v(TAG, "call_put(global:" + request + "=" + newValue + ") for "
- + callingUser);
+
+ newProviders = oldProviders.substring(0, index);
+ if (end < oldProviders.length()) {
+ newProviders += oldProviders.substring(end);
}
- insertForUser(Settings.Global.CONTENT_URI, values, callingUser);
} else {
- Slog.w(TAG, "call() with invalid method: " + method);
+ // nothing changed, so no need to update the database
+ return false;
}
- return null;
- }
+ updateSecureSettingLocked(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+ newProviders, owningUserId);
- /**
- * Check if the user is a managed profile and name is one of the settings to be cloned
- * from the parent profile.
- */
- private boolean shouldShadowParentProfile(int userId, HashSet<String> keys, String name) {
- return isManagedProfile(userId) && keys.contains(name);
+ return true;
}
- // Looks up value 'key' in 'table' and returns either a single-pair Bundle,
- // possibly with a null value, or null on failure.
- private Bundle lookupValue(DatabaseHelper dbHelper, String table,
- final SettingsCache cache, String key) {
- if (cache == null) {
- Slog.e(TAG, "cache is null for user " + UserHandle.getCallingUserId() + " : key=" + key);
- return null;
- }
- synchronized (cache) {
- Bundle value = cache.get(key);
- if (value != null) {
- if (value != TOO_LARGE_TO_CACHE_MARKER) {
- return value;
- }
- // else we fall through and read the value from disk
- } else if (cache.fullyMatchesDisk()) {
- // Fast path (very common). Don't even try touch disk
- // if we know we've slurped it all in. Trying to
- // touch the disk would mean waiting for yaffs2 to
- // give us access, which could takes hundreds of
- // milliseconds. And we're very likely being called
- // from somebody's UI thread...
- return NULL_SETTING;
+ private void sendNotify(Uri uri, int userId) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ getContext().getContentResolver().notifyChange(uri, null, true, userId);
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "Notifying for " + userId + ": " + uri);
}
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
+ }
- SQLiteDatabase db = dbHelper.getReadableDatabase();
- Cursor cursor = null;
- try {
- cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key},
- null, null, null, null);
- if (cursor != null && cursor.getCount() == 1) {
- cursor.moveToFirst();
- return cache.putIfAbsent(key, cursor.getString(0));
+ private static void warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
+ int targetSdkVersion, String name) {
+ // If the app targets Lollipop MR1 or older SDK we warn, otherwise crash.
+ if (targetSdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ if (Settings.System.PRIVATE_SETTINGS.contains(name)) {
+ Slog.w(LOG_TAG, "You shouldn't not change private system settings."
+ + " This will soon become an error.");
+ } else {
+ Slog.w(LOG_TAG, "You shouldn't keep your settings in the secure settings."
+ + " This will soon become an error.");
+ }
+ } else {
+ if (Settings.System.PRIVATE_SETTINGS.contains(name)) {
+ throw new IllegalArgumentException("You cannot change private secure settings.");
+ } else {
+ throw new IllegalArgumentException("You cannot keep your settings in"
+ + " the secure settings.");
}
- } catch (SQLiteException e) {
- Log.w(TAG, "settings lookup error", e);
- return null;
- } finally {
- if (cursor != null) cursor.close();
}
- cache.putIfAbsent(key, null);
- return NULL_SETTING;
}
- @Override
- public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
- return queryForUser(url, select, where, whereArgs, sort, UserHandle.getCallingUserId());
+ private static int resolveCallingUserIdEnforcingPermissionsLocked(int requestingUserId) {
+ if (requestingUserId == UserHandle.getCallingUserId()) {
+ return requestingUserId;
+ }
+ return ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), requestingUserId, false, true,
+ "get/set setting for user", null);
}
- private Cursor queryForUser(Uri url, String[] select, String where, String[] whereArgs,
- String sort, int forUser) {
- if (LOCAL_LOGV) Slog.v(TAG, "query(" + url + ") for user " + forUser);
- SqlArguments args = new SqlArguments(url, where, whereArgs);
- DatabaseHelper dbH;
- dbH = getOrEstablishDatabase(
- TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : forUser);
- SQLiteDatabase db = dbH.getReadableDatabase();
-
- // The favorites table was moved from this provider to a provider inside Home
- // Home still need to query this table to upgrade from pre-cupcake builds
- // However, a cupcake+ build with no data does not contain this table which will
- // cause an exception in the SQL stack. The following line is a special case to
- // let the caller of the query have a chance to recover and avoid the exception
- if (TABLE_FAVORITES.equals(args.table)) {
- return null;
- } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
- args.table = TABLE_FAVORITES;
- Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null);
- if (cursor != null) {
- boolean exists = cursor.getCount() > 0;
- cursor.close();
- if (!exists) return null;
- } else {
- return null;
- }
+ private static Bundle packageValueForCallResult(Setting setting) {
+ if (setting == null) {
+ return NULL_SETTING;
}
+ return Bundle.forPair(Settings.NameValueTable.VALUE, setting.getValue());
+ }
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables(args.table);
+ private static int getRequestingUserId(Bundle args) {
+ final int callingUserId = UserHandle.getCallingUserId();
+ return (args != null) ? args.getInt(Settings.CALL_METHOD_USER_KEY, callingUserId)
+ : callingUserId;
+ }
- Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
- // the default Cursor interface does not support per-user observation
- try {
- AbstractCursor c = (AbstractCursor) ret;
- c.setNotificationUri(getContext().getContentResolver(), url, forUser);
- } 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 ret;
+ private static String getSettingValue(Bundle args) {
+ return (args != null) ? args.getString(Settings.NameValueTable.VALUE) : null;
}
- @Override
- public String getType(Uri url) {
- // If SqlArguments supplies a where clause, then it must be an item
- // (because we aren't supplying our own where clause).
- SqlArguments args = new SqlArguments(url, null, null);
- if (TextUtils.isEmpty(args.where)) {
- return "vnd.android.cursor.dir/" + args.table;
- } else {
- return "vnd.android.cursor.item/" + args.table;
+ private static String getValidTableOrThrow(Uri uri) {
+ if (uri.getPathSegments().size() > 0) {
+ String table = uri.getPathSegments().get(0);
+ if (DatabaseHelper.isValidTable(table)) {
+ return table;
+ }
+ throw new IllegalArgumentException("Bad root path: " + table);
}
+ throw new IllegalArgumentException("Invalid URI:" + uri);
}
- @Override
- public int bulkInsert(Uri uri, ContentValues[] values) {
- final int callingUser = UserHandle.getCallingUserId();
- if (LOCAL_LOGV) Slog.v(TAG, "bulkInsert() for user " + callingUser);
- SqlArguments args = new SqlArguments(uri);
- if (TABLE_FAVORITES.equals(args.table)) {
- return 0;
+ private static MatrixCursor packageSettingForQuery(Setting setting, String[] projection) {
+ if (setting == null) {
+ return new MatrixCursor(projection, 0);
}
- checkWritePermissions(args);
- SettingsCache cache = cacheForTable(callingUser, args.table);
+ MatrixCursor cursor = new MatrixCursor(projection, 1);
+ appendSettingToCursor(cursor, setting);
+ return cursor;
+ }
- final AtomicInteger mutationCount;
- synchronized (this) {
- mutationCount = sKnownMutationsInFlight.get(callingUser);
- }
- if (mutationCount != null) {
- mutationCount.incrementAndGet();
+ private static String[] normalizeProjection(String[] projection) {
+ if (projection == null) {
+ return ALL_COLUMNS;
}
- DatabaseHelper dbH = getOrEstablishDatabase(
- TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : callingUser);
- SQLiteDatabase db = dbH.getWritableDatabase();
- db.beginTransaction();
- try {
- int numValues = values.length;
- for (int i = 0; i < numValues; i++) {
- checkUserRestrictions(values[i].getAsString(Settings.Secure.NAME), callingUser);
- if (db.insert(args.table, null, values[i]) < 0) return 0;
- SettingsCache.populate(cache, values[i]);
- if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
+
+ final int columnCount = projection.length;
+ for (int i = 0; i < columnCount; i++) {
+ String column = projection[i];
+ if (!ArrayUtils.contains(ALL_COLUMNS, column)) {
+ throw new IllegalArgumentException("Invalid column: " + column);
}
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- if (mutationCount != null) {
- mutationCount.decrementAndGet();
+ }
+
+ return projection;
+ }
+
+ private static void appendSettingToCursor(MatrixCursor cursor, Setting setting) {
+ final int columnCount = cursor.getColumnCount();
+
+ String[] values = new String[columnCount];
+
+ for (int i = 0; i < columnCount; i++) {
+ String column = cursor.getColumnName(i);
+
+ switch (column) {
+ case Settings.NameValueTable._ID: {
+ values[i] = setting.getId();
+ } break;
+
+ case Settings.NameValueTable.NAME: {
+ values[i] = setting.getName();
+ } break;
+
+ case Settings.NameValueTable.VALUE: {
+ values[i] = setting.getValue();
+ } break;
}
}
- sendNotify(uri, callingUser);
- return values.length;
+ cursor.addRow(values);
}
- /*
- * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
- * This setting contains a list of the currently enabled location providers.
- * But helper functions in android.providers.Settings can enable or disable
- * a single provider by using a "+" or "-" prefix before the provider name.
- *
- * @returns whether the database needs to be updated or not, also modifying
- * 'initialValues' if needed.
- */
- private boolean parseProviderList(Uri url, ContentValues initialValues, int desiredUser) {
- String value = initialValues.getAsString(Settings.Secure.VALUE);
- String newProviders = null;
- if (value != null && value.length() > 1) {
- char prefix = value.charAt(0);
- if (prefix == '+' || prefix == '-') {
- // skip prefix
- value = value.substring(1);
-
- // read list of enabled providers into "providers"
- String providers = "";
- String[] columns = {Settings.Secure.VALUE};
- String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'";
- Cursor cursor = queryForUser(url, columns, where, null, null, desiredUser);
- if (cursor != null && cursor.getCount() == 1) {
- try {
- cursor.moveToFirst();
- providers = cursor.getString(0);
- } finally {
- cursor.close();
+ private static final class Arguments {
+ private static final Pattern WHERE_PATTERN_WITH_PARAM_NO_BRACKETS =
+ Pattern.compile("[\\s]*name[\\s]*=[\\s]*\\?[\\s]*");
+
+ private static final Pattern WHERE_PATTERN_WITH_PARAM_IN_BRACKETS =
+ Pattern.compile("[\\s]*\\([\\s]*name[\\s]*=[\\s]*\\?[\\s]*\\)[\\s]*");
+
+ private static final Pattern WHERE_PATTERN_NO_PARAM_IN_BRACKETS =
+ Pattern.compile("[\\s]*\\([\\s]*name[\\s]*=[\\s]*['\"].*['\"][\\s]*\\)[\\s]*");
+
+ private static final Pattern WHERE_PATTERN_NO_PARAM_NO_BRACKETS =
+ Pattern.compile("[\\s]*name[\\s]*=[\\s]*['\"].*['\"][\\s]*");
+
+ public final String table;
+ public final String name;
+
+ public Arguments(Uri uri, String where, String[] whereArgs, boolean supportAll) {
+ final int segmentSize = uri.getPathSegments().size();
+ switch (segmentSize) {
+ case 1: {
+ if (where != null
+ && (WHERE_PATTERN_WITH_PARAM_NO_BRACKETS.matcher(where).matches()
+ || WHERE_PATTERN_WITH_PARAM_IN_BRACKETS.matcher(where).matches())
+ && whereArgs.length == 1) {
+ name = whereArgs[0];
+ table = computeTableForSetting(uri, name);
+ } else if (where != null
+ && (WHERE_PATTERN_NO_PARAM_NO_BRACKETS.matcher(where).matches()
+ || WHERE_PATTERN_NO_PARAM_IN_BRACKETS.matcher(where).matches())) {
+ final int startIndex = Math.max(where.indexOf("'"),
+ where.indexOf("\"")) + 1;
+ final int endIndex = Math.max(where.lastIndexOf("'"),
+ where.lastIndexOf("\""));
+ name = where.substring(startIndex, endIndex);
+ table = computeTableForSetting(uri, name);
+ } else if (supportAll && where == null && whereArgs == null) {
+ name = null;
+ table = computeTableForSetting(uri, null);
+ } else if (uri.getPathSegments().size() == 2
+ && where == null && whereArgs == null) {
+ name = uri.getPathSegments().get(1);
+ table = computeTableForSetting(uri, name);
+ } else {
+ EventLogTags.writeUnsupportedSettingsQuery(
+ uri.toSafeString(), where, Arrays.toString(whereArgs));
+ throw new IllegalArgumentException("Only null where and args"
+ + " or name=? where and a single arg or name='SOME_SETTING' "
+ + "are supported uri: " + uri + " where: " + where + " args: "
+ + Arrays.toString(whereArgs));
}
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
}
+ }
+ }
- int index = providers.indexOf(value);
- int end = index + value.length();
- // check for commas to avoid matching on partial string
- if (index > 0 && providers.charAt(index - 1) != ',') index = -1;
- if (end < providers.length() && providers.charAt(end) != ',') index = -1;
+ public static String computeTableForSetting(Uri uri, String name) {
+ String table = getValidTableOrThrow(uri);
- if (prefix == '+' && index < 0) {
- // append the provider to the list if not present
- if (providers.length() == 0) {
- newProviders = value;
- } else {
- newProviders = providers + ',' + value;
- }
- } else if (prefix == '-' && index >= 0) {
- // remove the provider from the list if present
- // remove leading or trailing comma
- if (index > 0) {
- index--;
- } else if (end < providers.length()) {
- end++;
- }
+ if (name != null) {
+ if (sSystemMovedToSecureSettings.contains(name)) {
+ table = TABLE_SECURE;
+ }
- newProviders = providers.substring(0, index);
- if (end < providers.length()) {
- newProviders += providers.substring(end);
- }
- } else {
- // nothing changed, so no need to update the database
- return false;
+ if (sSystemMovedToGlobalSettings.contains(name)) {
+ table = TABLE_GLOBAL;
+ }
+
+ if (sSecureMovedToGlobalSettings.contains(name)) {
+ table = TABLE_GLOBAL;
}
- if (newProviders != null) {
- initialValues.put(Settings.Secure.VALUE, newProviders);
+ if (sGlobalMovedToSecureSettings.contains(name)) {
+ table = TABLE_SECURE;
}
}
- }
- return true;
+ return table;
+ }
}
- @Override
- public Uri insert(Uri url, ContentValues initialValues) {
- return insertForUser(url, initialValues, UserHandle.getCallingUserId());
- }
+ final class SettingsRegistry {
+ private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid";
+
+ private static final int SETTINGS_TYPE_GLOBAL = 0;
+ private static final int SETTINGS_TYPE_SYSTEM = 1;
+ private static final int SETTINGS_TYPE_SECURE = 2;
- // Settings.put*ForUser() always winds up here, so this is where we apply
- // policy around permission to write settings for other users.
- private Uri insertForUser(Uri url, ContentValues initialValues, int desiredUserHandle) {
- final int callingUser = UserHandle.getCallingUserId();
- if (callingUser != desiredUserHandle) {
- getContext().enforceCallingOrSelfPermission(
- android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
- "Not permitted to access settings for other users");
+ private static final int SETTINGS_TYPE_MASK = 0xF0000000;
+ private static final int SETTINGS_TYPE_SHIFT = 28;
+
+ private static final String SETTINGS_FILE_GLOBAL = "settings_global.xml";
+ private static final String SETTINGS_FILE_SYSTEM = "settings_system.xml";
+ private static final String SETTINGS_FILE_SECURE = "settings_secure.xml";
+
+ private final SparseArray<SettingsState> mSettingsStates = new SparseArray<>();
+
+ private final BackupManager mBackupManager;
+
+ public SettingsRegistry() {
+ mBackupManager = new BackupManager(getContext());
+ migrateAllLegacySettingsIfNeeded();
}
- if (LOCAL_LOGV) Slog.v(TAG, "insert(" + url + ") for user " + desiredUserHandle
- + " by " + callingUser);
+ public List<String> getSettingsNamesLocked(int type, int userId) {
+ final int key = makeKey(type, userId);
+ SettingsState settingsState = peekSettingsStateLocked(key);
+ return settingsState.getSettingNamesLocked();
+ }
- SqlArguments args = new SqlArguments(url);
- if (TABLE_FAVORITES.equals(args.table)) {
- return null;
+ public SettingsState getSettingsLocked(int type, int userId) {
+ final int key = makeKey(type, userId);
+ return peekSettingsStateLocked(key);
}
- // Special case LOCATION_PROVIDERS_ALLOWED.
- // Support enabling/disabling a single provider (using "+" or "-" prefix)
- String name = initialValues.getAsString(Settings.Secure.NAME);
- if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
- if (!parseProviderList(url, initialValues, desiredUserHandle)) return null;
+ public void ensureSettingsForUserLocked(int userId) {
+ // Migrate the setting for this user if needed.
+ migrateLegacySettingsForUserIfNeededLocked(userId);
+
+ // Ensure global settings loaded if owner.
+ if (userId == UserHandle.USER_OWNER) {
+ final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+ ensureSettingsStateLocked(globalKey);
+ }
+
+ // Ensure secure settings loaded.
+ final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+ ensureSettingsStateLocked(secureKey);
+
+ // Make sure the secure settings have an Android id set.
+ SettingsState secureSettings = getSettingsLocked(SETTINGS_TYPE_SECURE, userId);
+ ensureSecureSettingAndroidIdSetLocked(secureSettings);
+
+ // Ensure system settings loaded.
+ final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+ ensureSettingsStateLocked(systemKey);
+
+ // Upgrade the settings to the latest version.
+ UpgradeController upgrader = new UpgradeController(userId);
+ upgrader.upgradeIfNeededLocked();
}
- // If this is an insert() of a key that has been migrated to the global store,
- // redirect the operation to that store
- if (name != null) {
- if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) {
- if (!TABLE_GLOBAL.equals(args.table)) {
- if (LOCAL_LOGV) Slog.i(TAG, "Rewrite of insert() of now-global key " + name);
- }
- args.table = TABLE_GLOBAL; // next condition will rewrite the user handle
+ private void ensureSettingsStateLocked(int key) {
+ if (mSettingsStates.get(key) == null) {
+ final int maxBytesPerPackage = getMaxBytesPerPackageForType(getTypeFromKey(key));
+ SettingsState settingsState = new SettingsState(mLock, getSettingsFile(key), key,
+ maxBytesPerPackage);
+ mSettingsStates.put(key, settingsState);
}
}
- // Check write permissions only after determining which table the insert will touch
- checkWritePermissions(args);
+ public void removeUserStateLocked(int userId, boolean permanently) {
+ // We always keep the global settings in memory.
- checkUserRestrictions(name, desiredUserHandle);
+ // Nuke system settings.
+ final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+ final SettingsState systemSettingsState = mSettingsStates.get(systemKey);
+ if (systemSettingsState != null) {
+ if (permanently) {
+ mSettingsStates.remove(systemKey);
+ systemSettingsState.destroyLocked(null);
+ } else {
+ systemSettingsState.destroyLocked(new Runnable() {
+ @Override
+ public void run() {
+ mSettingsStates.remove(systemKey);
+ }
+ });
+ }
+ }
- // The global table is stored under the owner, always
- if (TABLE_GLOBAL.equals(args.table)) {
- desiredUserHandle = UserHandle.USER_OWNER;
+ // Nuke secure settings.
+ final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+ final SettingsState secureSettingsState = mSettingsStates.get(secureKey);
+ if (secureSettingsState != null) {
+ if (permanently) {
+ mSettingsStates.remove(secureKey);
+ secureSettingsState.destroyLocked(null);
+ } else {
+ secureSettingsState.destroyLocked(new Runnable() {
+ @Override
+ public void run() {
+ mSettingsStates.remove(secureKey);
+ }
+ });
+ }
+ }
}
- SettingsCache cache = cacheForTable(desiredUserHandle, args.table);
- String value = initialValues.getAsString(Settings.NameValueTable.VALUE);
- if (SettingsCache.isRedundantSetValue(cache, name, value)) {
- return Uri.withAppendedPath(url, name);
+ public boolean insertSettingLocked(int type, int userId, String name, String value,
+ String packageName) {
+ final int key = makeKey(type, userId);
+
+ SettingsState settingsState = peekSettingsStateLocked(key);
+ final boolean success = settingsState.insertSettingLocked(name, value, packageName);
+
+ if (success) {
+ notifyForSettingsChange(key, name);
+ }
+ return success;
}
- final AtomicInteger mutationCount;
- synchronized (this) {
- mutationCount = sKnownMutationsInFlight.get(callingUser);
+ public boolean deleteSettingLocked(int type, int userId, String name) {
+ final int key = makeKey(type, userId);
+
+ SettingsState settingsState = peekSettingsStateLocked(key);
+ final boolean success = settingsState.deleteSettingLocked(name);
+
+ if (success) {
+ notifyForSettingsChange(key, name);
+ }
+ return success;
}
- if (mutationCount != null) {
- mutationCount.incrementAndGet();
+
+ public Setting getSettingLocked(int type, int userId, String name) {
+ final int key = makeKey(type, userId);
+
+ SettingsState settingsState = peekSettingsStateLocked(key);
+ return settingsState.getSettingLocked(name);
}
- DatabaseHelper dbH = getOrEstablishDatabase(desiredUserHandle);
- SQLiteDatabase db = dbH.getWritableDatabase();
- final long rowId = db.insert(args.table, null, initialValues);
- if (mutationCount != null) {
- mutationCount.decrementAndGet();
+
+ public boolean updateSettingLocked(int type, int userId, String name, String value,
+ String packageName) {
+ final int key = makeKey(type, userId);
+
+ SettingsState settingsState = peekSettingsStateLocked(key);
+ final boolean success = settingsState.updateSettingLocked(name, value, packageName);
+
+ if (success) {
+ notifyForSettingsChange(key, name);
+ }
+
+ return success;
}
- if (rowId <= 0) return null;
- SettingsCache.populate(cache, initialValues); // before we notify
+ public void onPackageRemovedLocked(String packageName, int userId) {
+ final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+ SettingsState globalSettings = mSettingsStates.get(globalKey);
+ globalSettings.onPackageRemovedLocked(packageName);
- if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues
- + " for user " + desiredUserHandle);
- // Note that we use the original url here, not the potentially-rewritten table name
- url = getUriFor(url, initialValues, rowId);
- sendNotify(url, desiredUserHandle);
- return url;
- }
+ final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+ SettingsState secureSettings = mSettingsStates.get(secureKey);
+ secureSettings.onPackageRemovedLocked(packageName);
- @Override
- public int delete(Uri url, String where, String[] whereArgs) {
- int callingUser = UserHandle.getCallingUserId();
- if (LOCAL_LOGV) Slog.v(TAG, "delete() for user " + callingUser);
- SqlArguments args = new SqlArguments(url, where, whereArgs);
- if (TABLE_FAVORITES.equals(args.table)) {
- return 0;
- } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
- args.table = TABLE_FAVORITES;
- } else if (TABLE_GLOBAL.equals(args.table)) {
- callingUser = UserHandle.USER_OWNER;
+ final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+ SettingsState systemSettings = mSettingsStates.get(systemKey);
+ systemSettings.onPackageRemovedLocked(packageName);
}
- checkWritePermissions(args);
- final AtomicInteger mutationCount;
- synchronized (this) {
- mutationCount = sKnownMutationsInFlight.get(callingUser);
+ private SettingsState peekSettingsStateLocked(int key) {
+ SettingsState settingsState = mSettingsStates.get(key);
+ if (settingsState != null) {
+ return settingsState;
+ }
+
+ ensureSettingsForUserLocked(getUserIdFromKey(key));
+ return mSettingsStates.get(key);
}
- if (mutationCount != null) {
- mutationCount.incrementAndGet();
+
+ private void migrateAllLegacySettingsIfNeeded() {
+ synchronized (mLock) {
+ final int key = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
+ File globalFile = getSettingsFile(key);
+ if (globalFile.exists()) {
+ return;
+ }
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ List<UserInfo> users = mUserManager.getUsers(true);
+
+ final int userCount = users.size();
+ for (int i = 0; i < userCount; i++) {
+ final int userId = users.get(i).id;
+
+ DatabaseHelper dbHelper = new DatabaseHelper(getContext(), userId);
+ SQLiteDatabase database = dbHelper.getWritableDatabase();
+ migrateLegacySettingsForUserLocked(dbHelper, database, userId);
+
+ // Upgrade to the latest version.
+ UpgradeController upgrader = new UpgradeController(userId);
+ upgrader.upgradeIfNeededLocked();
+
+ // Drop from memory if not a running user.
+ if (!mUserManager.isUserRunning(new UserHandle(userId))) {
+ removeUserStateLocked(userId, false);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
}
- DatabaseHelper dbH = getOrEstablishDatabase(callingUser);
- SQLiteDatabase db = dbH.getWritableDatabase();
- int count = db.delete(args.table, args.where, args.args);
- if (mutationCount != null) {
- mutationCount.decrementAndGet();
+
+ private void migrateLegacySettingsForUserIfNeededLocked(int userId) {
+ // Every user has secure settings and if no file we need to migrate.
+ final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+ File secureFile = getSettingsFile(secureKey);
+ if (secureFile.exists()) {
+ return;
+ }
+
+ DatabaseHelper dbHelper = new DatabaseHelper(getContext(), userId);
+ SQLiteDatabase database = dbHelper.getWritableDatabase();
+
+ migrateLegacySettingsForUserLocked(dbHelper, database, userId);
}
- if (count > 0) {
- invalidateCache(callingUser, args.table); // before we notify
- sendNotify(url, callingUser);
+
+ private void migrateLegacySettingsForUserLocked(DatabaseHelper dbHelper,
+ SQLiteDatabase database, int userId) {
+ // Move over the global settings if owner.
+ if (userId == UserHandle.USER_OWNER) {
+ final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, userId);
+ ensureSettingsStateLocked(globalKey);
+ SettingsState globalSettings = mSettingsStates.get(globalKey);
+ migrateLegacySettingsLocked(globalSettings, database, TABLE_GLOBAL);
+ globalSettings.persistSyncLocked();
+ }
+
+ // Move over the secure settings.
+ final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
+ ensureSettingsStateLocked(secureKey);
+ SettingsState secureSettings = mSettingsStates.get(secureKey);
+ migrateLegacySettingsLocked(secureSettings, database, TABLE_SECURE);
+ ensureSecureSettingAndroidIdSetLocked(secureSettings);
+ secureSettings.persistSyncLocked();
+
+ // Move over the system settings.
+ final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId);
+ ensureSettingsStateLocked(systemKey);
+ SettingsState systemSettings = mSettingsStates.get(systemKey);
+ migrateLegacySettingsLocked(systemSettings, database, TABLE_SYSTEM);
+ systemSettings.persistSyncLocked();
+
+ // Drop the database as now all is moved and persisted.
+ if (DROP_DATABASE_ON_MIGRATION) {
+ dbHelper.dropDatabase();
+ } else {
+ dbHelper.backupDatabase();
+ }
}
- startAsyncCachePopulation(callingUser);
- if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
- return count;
- }
- @Override
- public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
- // NOTE: update() is never called by the front-end Settings 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.
- int callingUser = UserHandle.getCallingUserId();
- if (LOCAL_LOGV) Slog.v(TAG, "update() for user " + callingUser);
- SqlArguments args = new SqlArguments(url, where, whereArgs);
- if (TABLE_FAVORITES.equals(args.table)) {
- return 0;
- } else if (TABLE_GLOBAL.equals(args.table)) {
- callingUser = UserHandle.USER_OWNER;
+ private void migrateLegacySettingsLocked(SettingsState settingsState,
+ SQLiteDatabase database, String table) {
+ SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+ queryBuilder.setTables(table);
+
+ Cursor cursor = queryBuilder.query(database, ALL_COLUMNS,
+ null, null, null, null, null);
+
+ if (cursor == null) {
+ return;
+ }
+
+ try {
+ if (!cursor.moveToFirst()) {
+ return;
+ }
+
+ final int nameColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.NAME);
+ final int valueColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.VALUE);
+
+ settingsState.setVersionLocked(database.getVersion());
+
+ while (!cursor.isAfterLast()) {
+ String name = cursor.getString(nameColumnIdx);
+ String value = cursor.getString(valueColumnIdx);
+ settingsState.insertSettingLocked(name, value,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ cursor.moveToNext();
+ }
+ } finally {
+ cursor.close();
+ }
}
- checkWritePermissions(args);
- checkUserRestrictions(initialValues.getAsString(Settings.Secure.NAME), callingUser);
- final AtomicInteger mutationCount;
- synchronized (this) {
- mutationCount = sKnownMutationsInFlight.get(callingUser);
+ private void ensureSecureSettingAndroidIdSetLocked(SettingsState secureSettings) {
+ Setting value = secureSettings.getSettingLocked(Settings.Secure.ANDROID_ID);
+
+ if (value != null) {
+ return;
+ }
+
+ final int userId = getUserIdFromKey(secureSettings.mKey);
+
+ final UserInfo user;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ user = mUserManager.getUserInfo(userId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ if (user == null) {
+ // Can happen due to races when deleting users - treat as benign.
+ return;
+ }
+
+ String androidId = Long.toHexString(new SecureRandom().nextLong());
+ secureSettings.insertSettingLocked(Settings.Secure.ANDROID_ID, androidId,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+
+ Slog.d(LOG_TAG, "Generated and saved new ANDROID_ID [" + androidId
+ + "] for user " + userId);
+
+ // Write a drop box entry if it's a restricted profile
+ if (user.isRestricted()) {
+ DropBoxManager dbm = (DropBoxManager) getContext().getSystemService(
+ Context.DROPBOX_SERVICE);
+ if (dbm != null && dbm.isTagEnabled(DROPBOX_TAG_USERLOG)) {
+ dbm.addText(DROPBOX_TAG_USERLOG, System.currentTimeMillis()
+ + "," + DROPBOX_TAG_USERLOG + "," + androidId + "\n");
+ }
+ }
}
- if (mutationCount != null) {
- mutationCount.incrementAndGet();
+
+ private void notifyForSettingsChange(int key, String name) {
+ // Update the system property *first*, so if someone is listening for
+ // a notification and then using the contract class to get their data,
+ // the system property will be updated and they'll get the new data.
+
+ boolean backedUpDataChanged = false;
+ String property = null;
+ if (isGlobalSettingsKey(key)) {
+ property = Settings.Global.SYS_PROP_SETTING_VERSION;
+ backedUpDataChanged = true;
+ } else if (isSecureSettingsKey(key)) {
+ property = Settings.Secure.SYS_PROP_SETTING_VERSION;
+ backedUpDataChanged = true;
+ } else if (isSystemSettingsKey(key)) {
+ property = Settings.System.SYS_PROP_SETTING_VERSION;
+ backedUpDataChanged = true;
+ }
+
+ if (property != null) {
+ final long version = SystemProperties.getLong(property, 0) + 1;
+ SystemProperties.set(property, Long.toString(version));
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "System property " + property + "=" + version);
+ }
+ }
+
+ // Inform the backup manager about a data change
+ if (backedUpDataChanged) {
+ mBackupManager.dataChanged();
+ }
+
+ // Now send the notification through the content framework.
+
+ final int userId = getUserIdFromKey(key);
+ Uri uri = getNotificationUriFor(key, name);
+
+ sendNotify(uri, userId);
}
- DatabaseHelper dbH = getOrEstablishDatabase(callingUser);
- SQLiteDatabase db = dbH.getWritableDatabase();
- int count = db.update(args.table, initialValues, args.where, args.args);
- if (mutationCount != null) {
- mutationCount.decrementAndGet();
+
+ private int makeKey(int type, int userId) {
+ return (type << SETTINGS_TYPE_SHIFT) | userId;
}
- if (count > 0) {
- invalidateCache(callingUser, args.table); // before we notify
- sendNotify(url, callingUser);
+
+ private int getTypeFromKey(int key) {
+ return key >> SETTINGS_TYPE_SHIFT;
}
- startAsyncCachePopulation(callingUser);
- if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
- return count;
- }
- @Override
- public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
- throw new FileNotFoundException("Direct file access no longer supported; "
- + "ringtone playback is available through android.media.Ringtone");
- }
+ private int getUserIdFromKey(int key) {
+ return key & ~SETTINGS_TYPE_MASK;
+ }
- /**
- * In-memory LRU Cache of system and secure settings, along with
- * associated helper functions to keep cache coherent with the
- * database.
- */
- private static final class SettingsCache extends LruCache<String, Bundle> {
+ private boolean isGlobalSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_GLOBAL;
+ }
- private final String mCacheName;
- private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped.
+ private boolean isSystemSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_SYSTEM;
+ }
- public SettingsCache(String name) {
- super(MAX_CACHE_ENTRIES);
- mCacheName = name;
+ private boolean isSecureSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_SECURE;
}
- /**
- * Is the whole database table slurped into this cache?
- */
- public boolean fullyMatchesDisk() {
- synchronized (this) {
- return mCacheFullyMatchesDisk;
+ private File getSettingsFile(int key) {
+ if (isGlobalSettingsKey(key)) {
+ final int userId = getUserIdFromKey(key);
+ return new File(Environment.getUserSystemDirectory(userId),
+ SETTINGS_FILE_GLOBAL);
+ } else if (isSystemSettingsKey(key)) {
+ final int userId = getUserIdFromKey(key);
+ return new File(Environment.getUserSystemDirectory(userId),
+ SETTINGS_FILE_SYSTEM);
+ } else if (isSecureSettingsKey(key)) {
+ final int userId = getUserIdFromKey(key);
+ return new File(Environment.getUserSystemDirectory(userId),
+ SETTINGS_FILE_SECURE);
+ } else {
+ throw new IllegalArgumentException("Invalid settings key:" + key);
}
}
- public void setFullyMatchesDisk(boolean value) {
- synchronized (this) {
- mCacheFullyMatchesDisk = value;
+ private Uri getNotificationUriFor(int key, String name) {
+ if (isGlobalSettingsKey(key)) {
+ return (name != null) ? Uri.withAppendedPath(Settings.Global.CONTENT_URI, name)
+ : Settings.Global.CONTENT_URI;
+ } else if (isSecureSettingsKey(key)) {
+ return (name != null) ? Uri.withAppendedPath(Settings.Secure.CONTENT_URI, name)
+ : Settings.Secure.CONTENT_URI;
+ } else if (isSystemSettingsKey(key)) {
+ return (name != null) ? Uri.withAppendedPath(Settings.System.CONTENT_URI, name)
+ : Settings.System.CONTENT_URI;
+ } else {
+ throw new IllegalArgumentException("Invalid settings key:" + key);
}
}
- @Override
- protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) {
- if (evicted) {
- mCacheFullyMatchesDisk = false;
+ private int getMaxBytesPerPackageForType(int type) {
+ switch (type) {
+ case SETTINGS_TYPE_GLOBAL:
+ case SETTINGS_TYPE_SECURE: {
+ return SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED;
+ }
+
+ default: {
+ return SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED;
+ }
}
}
- /**
- * Atomic cache population, conditional on size of value and if
- * we lost a race.
- *
- * @returns a Bundle to send back to the client from call(), even
- * if we lost the race.
- */
- public Bundle putIfAbsent(String key, String value) {
- Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value);
- if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
- synchronized (this) {
- if (get(key) == null) {
- put(key, bundle);
- }
+ private final class UpgradeController {
+ private static final int SETTINGS_VERSION = 118;
+
+ private final int mUserId;
+
+ public UpgradeController(int userId) {
+ mUserId = userId;
+ }
+
+ public void upgradeIfNeededLocked() {
+ // The version of all settings for a user is the same (all users have secure).
+ SettingsState secureSettings = getSettingsLocked(
+ SettingsRegistry.SETTINGS_TYPE_SECURE, mUserId);
+
+ // Try an update from the current state.
+ final int oldVersion = secureSettings.getVersionLocked();
+ final int newVersion = SETTINGS_VERSION;
+
+ // If up do data - done.
+ if (oldVersion == newVersion) {
+ return;
}
+
+ // Try to upgrade.
+ final int curVersion = onUpgradeLocked(mUserId, oldVersion, newVersion);
+
+ // If upgrade failed start from scratch and upgrade.
+ if (curVersion != newVersion) {
+ // Drop state we have for this user.
+ removeUserStateLocked(mUserId, true);
+
+ // Recreate the database.
+ DatabaseHelper dbHelper = new DatabaseHelper(getContext(), mUserId);
+ SQLiteDatabase database = dbHelper.getWritableDatabase();
+ dbHelper.recreateDatabase(database, newVersion, curVersion, oldVersion);
+
+ // Migrate the settings for this user.
+ migrateLegacySettingsForUserLocked(dbHelper, database, mUserId);
+
+ // Now upgrade should work fine.
+ onUpgradeLocked(mUserId, oldVersion, newVersion);
+ }
+
+ // Set the global settings version if owner.
+ if (mUserId == UserHandle.USER_OWNER) {
+ SettingsState globalSettings = getSettingsLocked(
+ SettingsRegistry.SETTINGS_TYPE_GLOBAL, mUserId);
+ globalSettings.setVersionLocked(newVersion);
+ }
+
+ // Set the secure settings version.
+ secureSettings.setVersionLocked(newVersion);
+
+ // Set the system settings version.
+ SettingsState systemSettings = getSettingsLocked(
+ SettingsRegistry.SETTINGS_TYPE_SYSTEM, mUserId);
+ systemSettings.setVersionLocked(newVersion);
}
- return bundle;
- }
- /**
- * Populates a key in a given (possibly-null) cache.
- */
- public static void populate(SettingsCache cache, ContentValues contentValues) {
- if (cache == null) {
- return;
+ private SettingsState getGlobalSettingsLocked() {
+ return getSettingsLocked(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER);
}
- String name = contentValues.getAsString(Settings.NameValueTable.NAME);
- if (name == null) {
- Log.w(TAG, "null name populating settings cache.");
- return;
+
+ private SettingsState getSecureSettingsLocked(int userId) {
+ return getSettingsLocked(SETTINGS_TYPE_SECURE, userId);
}
- String value = contentValues.getAsString(Settings.NameValueTable.VALUE);
- cache.populate(name, value);
- }
- public void populate(String name, String value) {
- synchronized (this) {
- if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
- put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value));
- } else {
- put(name, TOO_LARGE_TO_CACHE_MARKER);
- }
+ private SettingsState getSystemSettingsLocked(int userId) {
+ return getSettingsLocked(SETTINGS_TYPE_SYSTEM, userId);
}
- }
- /**
- * For suppressing duplicate/redundant settings inserts early,
- * checking our cache first (but without faulting it in),
- * before going to sqlite with the mutation.
- */
- public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) {
- if (cache == null) return false;
- synchronized (cache) {
- Bundle bundle = cache.get(name);
- if (bundle == null) return false;
- String oldValue = bundle.getPairValue();
- if (oldValue == null && value == null) return true;
- if ((oldValue == null) != (value == null)) return false;
- return oldValue.equals(value);
+ private int onUpgradeLocked(int userId, int oldVersion, int newVersion) {
+ if (DEBUG) {
+ Slog.w(LOG_TAG, "Upgrading settings for user: " + userId + " from version: "
+ + oldVersion + " to version: " + newVersion);
+ }
+
+ // You must perform all necessary mutations to bring the settings
+ // for this user from the old to the new version. When you add a new
+ // upgrade step you *must* update SETTINGS_VERSION.
+
+ /**
+ * This is an example of moving a setting from secure to global.
+ *
+ * int currentVersion = oldVersion;
+ * if (currentVersion == 118) {
+ * // Remove from the secure settings.
+ * SettingsState secureSettings = getSecureSettingsLocked(userId);
+ * String name = "example_setting_to_move";
+ * String value = secureSettings.getSetting(name);
+ * secureSettings.deleteSetting(name);
+ *
+ * // Add to the global settings.
+ * SettingsState globalSettings = getGlobalSettingsLocked();
+ * globalSettings.insertSetting(name, value, SettingsState.SYSTEM_PACKAGE_NAME);
+ *
+ * // Update the current version.
+ * currentVersion = 119;
+ * }
+ *
+ * // Return the current version.
+ * return currentVersion;
+ */
+
+ return SettingsState.VERSION_UNDEFINED;
}
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
new file mode 100644
index 0000000..e63d220
--- /dev/null
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2015 The Android Open Source 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 com.android.providers.settings;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.Xml;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BackgroundThread;
+import libcore.io.IoUtils;
+import libcore.util.Objects;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class contains the state for one type of settings. It is responsible
+ * for saving the state asynchronously to an XML file after a mutation and
+ * loading the from an XML file on construction.
+ * <p>
+ * This class uses the same lock as the settings provider to ensure that
+ * multiple changes made by the settings provider, e,g, upgrade, bulk insert,
+ * etc, are atomically persisted since the asynchronous persistence is using
+ * the same lock to grab the current state to write to disk.
+ * </p>
+ */
+final class SettingsState {
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_PERSISTENCE = false;
+
+ private static final String LOG_TAG = "SettingsState";
+
+ private static final long WRITE_SETTINGS_DELAY_MILLIS = 200;
+ private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000;
+
+ public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1;
+ public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000;
+
+ public static final String SYSTEM_PACKAGE_NAME = "android";
+
+ public static final int VERSION_UNDEFINED = -1;
+
+ private static final String TAG_SETTINGS = "settings";
+ private static final String TAG_SETTING = "setting";
+ private static final String ATTR_PACKAGE = "package";
+
+ private static final String ATTR_VERSION = "version";
+ private static final String ATTR_ID = "id";
+ private static final String ATTR_NAME = "name";
+ private static final String ATTR_VALUE = "value";
+
+ private static final String NULL_VALUE = "null";
+
+ private final Object mLock;
+
+ private final Handler mHandler = new MyHandler();
+
+ @GuardedBy("mLock")
+ private final ArrayMap<String, Setting> mSettings = new ArrayMap<>();
+
+ @GuardedBy("mLock")
+ private final ArrayMap<String, Integer> mPackageToMemoryUsage;
+
+ @GuardedBy("mLock")
+ private final int mMaxBytesPerAppPackage;
+
+ @GuardedBy("mLock")
+ private final File mStatePersistFile;
+
+ public final int mKey;
+
+ @GuardedBy("mLock")
+ private int mVersion = VERSION_UNDEFINED;
+
+ @GuardedBy("mLock")
+ private long mLastNotWrittenMutationTimeMillis;
+
+ @GuardedBy("mLock")
+ private boolean mDirty;
+
+ @GuardedBy("mLock")
+ private boolean mWriteScheduled;
+
+ public SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage) {
+ // It is important that we use the same lock as the settings provider
+ // to ensure multiple mutations on this state are atomicaly persisted
+ // as the async persistence should be blocked while we make changes.
+ mLock = lock;
+ mStatePersistFile = file;
+ mKey = key;
+ if (maxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_LIMITED) {
+ mMaxBytesPerAppPackage = maxBytesPerAppPackage;
+ mPackageToMemoryUsage = new ArrayMap<>();
+ } else {
+ mMaxBytesPerAppPackage = maxBytesPerAppPackage;
+ mPackageToMemoryUsage = null;
+ }
+ synchronized (mLock) {
+ readStateSyncLocked();
+ }
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public int getVersionLocked() {
+ return mVersion;
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public void setVersionLocked(int version) {
+ if (version == mVersion) {
+ return;
+ }
+ mVersion = version;
+
+ scheduleWriteIfNeededLocked();
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public void onPackageRemovedLocked(String packageName) {
+ boolean removedSomething = false;
+
+ final int settingCount = mSettings.size();
+ for (int i = settingCount - 1; i >= 0; i--) {
+ String name = mSettings.keyAt(i);
+ // Settings defined by use are never dropped.
+ if (Settings.System.PUBLIC_SETTINGS.contains(name)
+ || Settings.System.PRIVATE_SETTINGS.contains(name)) {
+ continue;
+ }
+ Setting setting = mSettings.valueAt(i);
+ if (packageName.equals(setting.packageName)) {
+ mSettings.removeAt(i);
+ removedSomething = true;
+ }
+ }
+
+ if (removedSomething) {
+ scheduleWriteIfNeededLocked();
+ }
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public List<String> getSettingNamesLocked() {
+ ArrayList<String> names = new ArrayList<>();
+ final int settingsCount = mSettings.size();
+ for (int i = 0; i < settingsCount; i++) {
+ String name = mSettings.keyAt(i);
+ names.add(name);
+ }
+ return names;
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public Setting getSettingLocked(String name) {
+ if (TextUtils.isEmpty(name)) {
+ return null;
+ }
+ return mSettings.get(name);
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public boolean updateSettingLocked(String name, String value, String packageName) {
+ if (!hasSettingLocked(name)) {
+ return false;
+ }
+
+ return insertSettingLocked(name, value, packageName);
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public boolean insertSettingLocked(String name, String value, String packageName) {
+ if (TextUtils.isEmpty(name)) {
+ return false;
+ }
+
+ Setting oldState = mSettings.get(name);
+ String oldValue = (oldState != null) ? oldState.value : null;
+
+ if (oldState != null) {
+ if (!oldState.update(value, packageName)) {
+ return false;
+ }
+ } else {
+ Setting state = new Setting(name, value, packageName);
+ mSettings.put(name, state);
+ }
+
+ updateMemoryUsagePerPackageLocked(packageName, oldValue, value);
+
+ scheduleWriteIfNeededLocked();
+
+ return true;
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public void persistSyncLocked() {
+ mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
+ doWriteState();
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public boolean deleteSettingLocked(String name) {
+ if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) {
+ return false;
+ }
+
+ Setting oldState = mSettings.remove(name);
+
+ updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, null);
+
+ scheduleWriteIfNeededLocked();
+
+ return true;
+ }
+
+ // The settings provider must hold its lock when calling here.
+ public void destroyLocked(Runnable callback) {
+ mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
+ if (callback != null) {
+ if (mDirty) {
+ // Do it without a delay.
+ mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS,
+ callback).sendToTarget();
+ return;
+ }
+ callback.run();
+ }
+ }
+
+ private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue,
+ String newValue) {
+ if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) {
+ return;
+ }
+
+ if (SYSTEM_PACKAGE_NAME.equals(packageName)) {
+ return;
+ }
+
+ final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
+ final int newValueSize = (newValue != null) ? newValue.length() : 0;
+ final int deltaSize = newValueSize - oldValueSize;
+
+ Integer currentSize = mPackageToMemoryUsage.get(packageName);
+ final int newSize = Math.max((currentSize != null)
+ ? currentSize + deltaSize : deltaSize, 0);
+
+ if (newSize > mMaxBytesPerAppPackage) {
+ throw new IllegalStateException("You are adding too many system settings. "
+ + "You should stop using system settings for app specific data.");
+ }
+
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Settings for package: " + packageName
+ + " size: " + newSize + " bytes.");
+ }
+
+ mPackageToMemoryUsage.put(packageName, newSize);
+ }
+
+ private boolean hasSettingLocked(String name) {
+ return mSettings.indexOfKey(name) >= 0;
+ }
+
+ private void scheduleWriteIfNeededLocked() {
+ // If dirty then we have a write already scheduled.
+ if (!mDirty) {
+ mDirty = true;
+ writeStateAsyncLocked();
+ }
+ }
+
+ private void writeStateAsyncLocked() {
+ final long currentTimeMillis = SystemClock.uptimeMillis();
+
+ if (mWriteScheduled) {
+ mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
+
+ // If enough time passed, write without holding off anymore.
+ final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis
+ - mLastNotWrittenMutationTimeMillis;
+ if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) {
+ mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget();
+ return;
+ }
+
+ // Hold off a bit more as settings are frequently changing.
+ final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis
+ + MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0);
+ final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis);
+
+ Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
+ mHandler.sendMessageDelayed(message, writeDelayMillis);
+ } else {
+ mLastNotWrittenMutationTimeMillis = currentTimeMillis;
+ Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
+ mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS);
+ mWriteScheduled = true;
+ }
+ }
+
+ private void doWriteState() {
+ if (DEBUG_PERSISTENCE) {
+ Slog.i(LOG_TAG, "[PERSIST START]");
+ }
+
+ AtomicFile destination = new AtomicFile(mStatePersistFile);
+
+ final int version;
+ final ArrayMap<String, Setting> settings;
+
+ synchronized (mLock) {
+ version = mVersion;
+ settings = new ArrayMap<>(mSettings);
+ mDirty = false;
+ mWriteScheduled = false;
+ }
+
+ FileOutputStream out = null;
+ try {
+ out = destination.startWrite();
+
+ XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(out, "utf-8");
+ serializer.startDocument(null, true);
+ serializer.startTag(null, TAG_SETTINGS);
+ serializer.attribute(null, ATTR_VERSION, String.valueOf(version));
+
+ final int settingCount = settings.size();
+ for (int i = 0; i < settingCount; i++) {
+ Setting setting = settings.valueAt(i);
+
+ serializer.startTag(null, TAG_SETTING);
+ serializer.attribute(null, ATTR_ID, setting.getId());
+ serializer.attribute(null, ATTR_NAME, setting.getName());
+ serializer.attribute(null, ATTR_VALUE, packValue(setting.getValue()));
+ serializer.attribute(null, ATTR_PACKAGE, packValue(setting.getPackageName()));
+ serializer.endTag(null, TAG_SETTING);
+
+ if (DEBUG_PERSISTENCE) {
+ Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue());
+ }
+ }
+
+ serializer.endTag(null, TAG_SETTINGS);
+ serializer.endDocument();
+ destination.finishWrite(out);
+
+ if (DEBUG_PERSISTENCE) {
+ Slog.i(LOG_TAG, "[PERSIST END]");
+ }
+
+ } catch (IOException e) {
+ Slog.w(LOG_TAG, "Failed to write settings, restoring backup", e);
+ destination.failWrite(out);
+ } finally {
+ IoUtils.closeQuietly(out);
+ }
+ }
+
+ private void readStateSyncLocked() {
+ FileInputStream in;
+ if (!mStatePersistFile.exists()) {
+ return;
+ }
+ try {
+ in = new FileInputStream(mStatePersistFile);
+ } catch (FileNotFoundException fnfe) {
+ Slog.i(LOG_TAG, "No settings state");
+ return;
+ }
+ try {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(in, null);
+ parseStateLocked(parser);
+ } catch (XmlPullParserException | IOException ise) {
+ throw new IllegalStateException("Failed parsing settings file: "
+ + mStatePersistFile , ise);
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ }
+
+ private void parseStateLocked(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ parser.next();
+ skipEmptyTextTags(parser);
+ expect(parser, XmlPullParser.START_TAG, TAG_SETTINGS);
+
+ mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION));
+
+ parser.next();
+
+ while (parseSettingLocked(parser)) {
+ parser.next();
+ }
+
+ skipEmptyTextTags(parser);
+ expect(parser, XmlPullParser.END_TAG, TAG_SETTINGS);
+ }
+
+ private boolean parseSettingLocked(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ skipEmptyTextTags(parser);
+ if (!accept(parser, XmlPullParser.START_TAG, TAG_SETTING)) {
+ return false;
+ }
+
+ String id = parser.getAttributeValue(null, ATTR_ID);
+ String name = parser.getAttributeValue(null, ATTR_NAME);
+ String value = parser.getAttributeValue(null, ATTR_VALUE);
+ String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
+ mSettings.put(name, new Setting(name, unpackValue(value),
+ unpackValue(packageName), id));
+
+ if (DEBUG_PERSISTENCE) {
+ Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
+ }
+
+ parser.next();
+
+ skipEmptyTextTags(parser);
+ expect(parser, XmlPullParser.END_TAG, TAG_SETTING);
+
+ return true;
+ }
+
+ private void expect(XmlPullParser parser, int type, String tag)
+ throws IOException, XmlPullParserException {
+ if (!accept(parser, type, tag)) {
+ throw new XmlPullParserException("Expected event: " + type
+ + " and tag: " + tag + " but got event: " + parser.getEventType()
+ + " and tag:" + parser.getName());
+ }
+ }
+
+ private void skipEmptyTextTags(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ while (accept(parser, XmlPullParser.TEXT, null)
+ && "\n".equals(parser.getText())) {
+ parser.next();
+ }
+ }
+
+ private boolean accept(XmlPullParser parser, int type, String tag)
+ throws IOException, XmlPullParserException {
+ if (parser.getEventType() != type) {
+ return false;
+ }
+ if (tag != null) {
+ if (!tag.equals(parser.getName())) {
+ return false;
+ }
+ } else if (parser.getName() != null) {
+ return false;
+ }
+ return true;
+ }
+
+ private final class MyHandler extends Handler {
+ public static final int MSG_PERSIST_SETTINGS = 1;
+
+ public MyHandler() {
+ super(BackgroundThread.getHandler().getLooper());
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_PERSIST_SETTINGS: {
+ Runnable callback = (Runnable) message.obj;
+ doWriteState();
+ if (callback != null) {
+ callback.run();
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ private static String packValue(String value) {
+ if (value == null) {
+ return NULL_VALUE;
+ }
+ return value;
+ }
+
+ private static String unpackValue(String value) {
+ if (NULL_VALUE.equals(value)) {
+ return null;
+ }
+ return value;
+ }
+
+ public static final class Setting {
+ private static long sNextId;
+
+ private String name;
+ private String value;
+ private String packageName;
+ private String id;
+
+ public Setting(String name, String value, String packageName) {
+ init(name, value, packageName, String.valueOf(sNextId++));
+ }
+
+ public Setting(String name, String value, String packageName, String id) {
+ sNextId = Math.max(sNextId, Long.valueOf(id));
+ init(name, value, packageName, String.valueOf(sNextId));
+ }
+
+ private void init(String name, String value, String packageName, String id) {
+ this.name = name;
+ this.value = value;
+ this.packageName = packageName;
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public boolean update(String value, String packageName) {
+ if (Objects.equal(value, this.value)) {
+ return false;
+ }
+ this.value = value;
+ this.packageName = packageName;
+ this.id = String.valueOf(sNextId++);
+ return true;
+ }
+ }
+}