diff options
Diffstat (limited to 'packages')
18 files changed, 3086 insertions, 1100 deletions
diff --git a/packages/PrintSpooler/res/values-si-rLK/strings.xml b/packages/PrintSpooler/res/values-si-rLK/strings.xml index a3518e1..dae8a02 100644 --- a/packages/PrintSpooler/res/values-si-rLK/strings.xml +++ b/packages/PrintSpooler/res/values-si-rLK/strings.xml @@ -24,7 +24,7 @@ <string name="label_paper_size" msgid="908654383827777759">"කඩදාසියේ ප්රමාණය"</string> <string name="label_paper_size_summary" msgid="5668204981332138168">"කඩදාසියේ ප්රමාණය:"</string> <string name="label_color" msgid="1108690305218188969">"වර්ණය"</string> - <string name="label_duplex" msgid="1263181386446435253">"දෙපත්"</string> + <string name="label_duplex" msgid="1263181386446435253">"ඩුප්ලෙක්ස්"</string> <string name="label_orientation" msgid="2853142581990496477">"දිශානතිය"</string> <string name="label_pages" msgid="7768589729282182230">"පිටු"</string> <string name="template_all_pages" msgid="3322235982020148762">"සියලුම <xliff:g id="PAGE_COUNT">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java index c3e23d2..2eb7abf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java @@ -127,6 +127,7 @@ public class WifiTracker { public void pauseScanning() { if (mScanner != null) { mScanner.pause(); + mScanner = null; } } @@ -134,10 +135,10 @@ public class WifiTracker { * Resume scanning for wifi networks after it has been paused. */ public void resumeScanning() { + if (mScanner == null) { + mScanner = new Scanner(); + } if (mWifiManager.isWifiEnabled()) { - if (mScanner == null) { - mScanner = new Scanner(); - } mScanner.resume(); } updateAccessPoints(); @@ -335,11 +336,17 @@ public class WifiTracker { private void updateWifiState(int state) { if (state == WifiManager.WIFI_STATE_ENABLED) { - mScanner.resume(); + if (mScanner != null) { + // We only need to resume if mScanner isn't null because + // that means we want to be scanning. + mScanner.resume(); + } } else { mLastInfo = null; mLastNetworkInfo = null; - mScanner.pause(); + if (mScanner != null) { + mScanner.pause(); + } } if (mListener != null) { mListener.onWifiStateChanged(state); @@ -382,26 +389,34 @@ public class WifiTracker { @VisibleForTesting class Scanner extends Handler { + private static final int MSG_SCAN = 0; + private int mRetry = 0; void resume() { - if (!hasMessages(0)) { - sendEmptyMessage(0); + if (!hasMessages(MSG_SCAN)) { + sendEmptyMessage(MSG_SCAN); } } void forceScan() { - removeMessages(0); - sendEmptyMessage(0); + removeMessages(MSG_SCAN); + sendEmptyMessage(MSG_SCAN); } void pause() { mRetry = 0; - removeMessages(0); + removeMessages(MSG_SCAN); + } + + @VisibleForTesting + boolean isScanning() { + return hasMessages(MSG_SCAN); } @Override public void handleMessage(Message message) { + if (message.what != MSG_SCAN) return; if (mWifiManager.startScan()) { mRetry = 0; } else if (++mRetry >= 3) { diff --git a/packages/SettingsLib/tests/src/com/android/settingslib/wifi/WifiTrackerTest.java b/packages/SettingsLib/tests/src/com/android/settingslib/wifi/WifiTrackerTest.java index 73d4938..8eb1ca4 100644 --- a/packages/SettingsLib/tests/src/com/android/settingslib/wifi/WifiTrackerTest.java +++ b/packages/SettingsLib/tests/src/com/android/settingslib/wifi/WifiTrackerTest.java @@ -201,6 +201,29 @@ public class WifiTrackerTest extends BaseTest { assertTrue("Connected to wifi", accessPoints.get(0).isActive()); } + public void testEnableResumeScanning() { + mWifiTracker.mScanner = null; + + Intent i = new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION); + // Make sure disable/enable cycle works with no scanner (no crashing). + i.putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_DISABLED); + mWifiTracker.mReceiver.onReceive(mContext, i); + i.putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_ENABLED); + mWifiTracker.mReceiver.onReceive(mContext, i); + + Mockito.when(mWifiManager.isWifiEnabled()).thenReturn(false); + i.putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_DISABLED); + mWifiTracker.mReceiver.onReceive(mContext, i); + // Now enable scanning while wifi is off, it shouldn't start. + mWifiTracker.resumeScanning(); + assertFalse(mWifiTracker.mScanner.isScanning()); + + // Turn on wifi and make sure scanning starts. + i.putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_ENABLED); + mWifiTracker.mReceiver.onReceive(mContext, i); + assertTrue(mWifiTracker.mScanner.isScanning()); + } + private String[] generateTestNetworks(List<WifiConfiguration> wifiConfigs, List<ScanResult> scanResults, boolean connectedIsEphemeral) { String[] expectedSsids = new String[NUM_NETWORKS]; diff --git a/packages/SettingsProvider/Android.mk b/packages/SettingsProvider/Android.mk index c16f7b6..2b833b2 100644 --- a/packages/SettingsProvider/Android.mk +++ b/packages/SettingsProvider/Android.mk @@ -3,7 +3,8 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional -LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_SRC_FILES := $(call all-subdir-java-files) \ + src/com/android/providers/settings/EventLogTags.logtags LOCAL_JAVA_LIBRARIES := telephony-common ims-common 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..5aac06d 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -16,981 +16,997 @@ 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; + } - setAppOps(AppOpsManager.OP_NONE, AppOpsManager.OP_WRITE_SETTINGS); - establishDbTracking(UserHandle.USER_OWNER); + 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); + } + } + } + } + + @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(); + + final int nameCount = names.size(); - mOpenHelpers.delete(userHandle); - sSystemCaches.delete(userHandle); - sSecureCaches.delete(userHandle); - sKnownMutationsInFlight.delete(userHandle); - onProfilesChanged(); + 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; - } - } - if (LOCAL_LOGV) { - Slog.d(TAG, "Managed Profiles = " + mManagedProfiles); - } + 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 void establishDbTracking(int userHandle) { - if (LOCAL_LOGV) { - Slog.i(TAG, "Installing settings db helper and caches for user " + userHandle); + 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); + } - DatabaseHelper dbhelper; + 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); + } - synchronized (this) { - dbhelper = mOpenHelpers.get(userHandle); - if (dbhelper == null) { - dbhelper = new DatabaseHelper(getContext(), userHandle); - mOpenHelpers.append(userHandle, dbhelper); + private boolean deleteGlobalSettingLocked(String name, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "deleteGlobalSettingLocked(" + name + ")"); + } + return mutateGlobalSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE); + } - sSystemCaches.append(userHandle, new SettingsCache(TABLE_SYSTEM)); - sSecureCaches.append(userHandle, new SettingsCache(TABLE_SECURE)); - sKnownMutationsInFlight.append(userHandle, new AtomicInteger(0)); - } + 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; } - // 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(); - } + // 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; } - ensureAndroidIdIsSet(userHandle); + // Perform the mutation. + switch (operation) { + case MUTATION_OPERATION_INSERT: { + return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_GLOBAL, + UserHandle.USER_OWNER, name, value, getCallingPackage()); + } - startAsyncCachePopulation(userHandle); - } + 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()); + } + } - class CachePrefetchThread extends Thread { - private int mUserHandle; + return false; + } - CachePrefetchThread(int userHandle) { - super("populate-settings-caches"); - mUserHandle = userHandle; + private Cursor getAllSecureSettingsLocked(int userId, String[] projection) { + if (DEBUG) { + Slog.v(LOG_TAG, "getAllSecureSettings(" + userId + ")"); } - @Override - public void run() { - fullyPopulateCaches(mUserHandle); + // 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(); + + 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 = 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); } - } - private void startAsyncCachePopulation(int userHandle) { - new CachePrefetchThread(userHandle).start(); + return result; } - private void fullyPopulateCaches(final int userHandle) { - DatabaseHelper dbHelper; - synchronized (this) { - dbHelper = mOpenHelpers.get(userHandle); - } - if (dbHelper == null) { - // User is gone. - return; + private Setting getSecureSettingLocked(String name, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "getSecureSetting(" + name + ", " + requestingUserId + ")"); } - // Only populate the globals cache once, for the owning user - if (userHandle == UserHandle.USER_OWNER) { - fullyPopulateCache(dbHelper, TABLE_GLOBAL, sGlobalCache); + + // 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 = resolveOwningUserIdForSecureSettingLocked(callingUserId, name); + + // Special case for location (sigh). + if (isLocationProvidersAllowedRestricted(name, callingUserId, owningUserId)) { + return null; } - fullyPopulateCache(dbHelper, TABLE_SECURE, sSecureCaches.get(userHandle)); - fullyPopulateCache(dbHelper, TABLE_SYSTEM, sSystemCaches.get(userHandle)); + + // Get the value. + return mSettingsRegistry.getSettingLocked(SettingsRegistry.SETTINGS_TYPE_SECURE, + owningUserId, name); } - // 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()); - } - } finally { - c.close(); + private boolean insertSecureSettingLocked(String name, String value, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "insertSecureSettingLocked(" + name + ", " + value + ", " + + requestingUserId + ")"); } - } - 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; - } + return mutateSecureSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT); + } - 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(); + private boolean deleteSecureSettingLocked(String name, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "deleteSecureSettingLocked(" + name + ", " + requestingUserId + ")"); } + + return mutateSecureSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE); } - // 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 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); } - // 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 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; } - long oldId = Binder.clearCallingIdentity(); - try { - DatabaseHelper dbHelper; - synchronized (this) { - dbHelper = mOpenHelpers.get(callingUser); + // 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; + } + + // 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; + } + + // 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()); } - if (null == dbHelper) { - establishDbTracking(callingUser); - synchronized (this) { - dbHelper = mOpenHelpers.get(callingUser); - } + + 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()); } - 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 Cursor getAllSystemSettingsLocked(int userId, String[] projection) { + if (DEBUG) { + Slog.v(LOG_TAG, "getAllSecureSystemLocked(" + userId + ")"); } - if (TABLE_SECURE.equals(tableName)) { - return getOrConstructCache(callingUser, sSecureCaches); - } - if (TABLE_GLOBAL.equals(tableName)) { - return sGlobalCache; + + // 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 null; + + return result; } - /** - * 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 Setting getSystemSettingLocked(String name, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "getSystemSetting(" + name + ", " + requestingUserId + ")"); } - synchronized (cache) { - cache.evictAll(); - cache.mCacheFullyMatchesDisk = false; + + // 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); + } + + private boolean insertSystemSettingLocked(String name, String value, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "insertSystemSettingLocked(" + name + ", " + value + ", " + + requestingUserId + ")"); } + + return mutateSystemSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_INSERT); } - /** - * 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; - } - } - return false; + private boolean deleteSystemSettingLocked(String name, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "deleteSystemSettingLocked(" + name + ", " + requestingUserId + ")"); } + + return mutateSystemSettingLocked(name, null, requestingUserId, MUTATION_OPERATION_DELETE); } - /** - * 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 boolean updateSystemSettingLocked(String name, String value, int requestingUserId) { + if (DEBUG) { + Slog.v(LOG_TAG, "updateSystemSettingLocked(" + name + ", " + value + ", " + + requestingUserId + ")"); } - // 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; + return mutateSystemSettingLocked(name, value, requestingUserId, MUTATION_OPERATION_UPDATE); + } - // 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) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException( - String.format("Permission denial: writing to settings requires %1$s", - android.Manifest.permission.WRITE_SETTINGS)); + 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; } - // Also need to take care of app op. - if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SETTINGS, Binder.getCallingUid(), - getCallingPackage()) != AppOpsManager.MODE_ALLOWED) { - return null; + // 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; } - 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; + // Mutate the value. + switch (operation) { + case MUTATION_OPERATION_INSERT: { + validateSystemSettingValue(name, value); + return mSettingsRegistry.insertSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM, + owningUserId, name, value, getCallingPackage()); } - 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); - } - // 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; - } - 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); - } + + case MUTATION_OPERATION_DELETE: { + return mSettingsRegistry.deleteSettingLocked( + SettingsRegistry.SETTINGS_TYPE_SYSTEM, + owningUserId, name); } - } else if (Settings.CALL_METHOD_PUT_GLOBAL.equals(method)) { - if (LOCAL_LOGV) { - Slog.v(TAG, "call_put(global:" + request + "=" + newValue + ") for " - + callingUser); + + case MUTATION_OPERATION_UPDATE: { + validateSystemSettingValue(name, value); + return mSettingsRegistry.updateSettingLocked(SettingsRegistry.SETTINGS_TYPE_SYSTEM, + owningUserId, name, value, getCallingPackage()); } - insertForUser(Settings.Global.CONTENT_URI, values, callingUser); - } else { - Slog.w(TAG, "call() with invalid method: " + method); } - return null; + return false; } - /** - * 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); + 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); + } } - // 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; + private boolean isLocationProvidersAllowedRestricted(String name, int callingUserId, + int owningUserId) { + // Optimization - location providers are restricted only for managed profiles. + if (callingUserId == owningUserId) { + return false; } - 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; - } + if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name) + && mUserManager.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, + new UserHandle(callingUserId))) { + return true; } + return false; + } - 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)); - } - } catch (SQLiteException e) { - Log.w(TAG, "settings lookup error", e); - return null; - } finally { - if (cursor != null) cursor.close(); + private boolean isGlobalOrSecureSettingRestrictedForUser(String setting, int userId) { + String restriction = sSettingToUserRestrictionMap.get(setting); + if (restriction == null) { + return false; } - cache.putIfAbsent(key, null); - return NULL_SETTING; + return mUserManager.hasUserRestriction(restriction, new UserHandle(userId)); } - @Override - public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) { - return queryForUser(url, select, where, whereArgs, sort, UserHandle.getCallingUserId()); + private int resolveOwningUserIdForSecureSettingLocked(int userId, String setting) { + return resolveOwningUserIdLocked(userId, sSecureCloneToManagedSettings, setting); } - 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 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; + } - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - qb.setTables(args.table); + 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; + } - 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; - } + switch (operation) { + case MUTATION_OPERATION_INSERT: + // Insert updates. + case MUTATION_OPERATION_UPDATE: { + if (Settings.System.PUBLIC_SETTINGS.contains(name)) { + return; + } - @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; + // 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; } } - @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 PackageInfo getCallingPackageInfoOrThrow() { + try { + return mPackageManager.getPackageInfo(getCallingPackage(), 0); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException("Calling package doesn't exist"); } - checkWritePermissions(args); - SettingsCache cache = cacheForTable(callingUser, args.table); + } - final AtomicInteger mutationCount; - synchronized (this) { - mutationCount = sKnownMutationsInFlight.get(callingUser); - } - if (mutationCount != null) { - mutationCount.incrementAndGet(); + private int getGroupParentLocked(int userId) { + // Most frequent use case. + if (userId == UserHandle.USER_OWNER) { + return userId; } - DatabaseHelper dbH = getOrEstablishDatabase( - TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : callingUser); - SQLiteDatabase db = dbH.getWritableDatabase(); - db.beginTransaction(); + // 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 { - 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]); - } - db.setTransactionSuccessful(); + UserInfo userInfo = mUserManager.getProfileParent(userId); + return (userInfo != null) ? userInfo.id : userId; } finally { - db.endTransaction(); - if (mutationCount != null) { - mutationCount.decrementAndGet(); - } + Binder.restoreCallingIdentity(identity); } + } + + 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; + } - sendNotify(uri, callingUser); - return values.length; + private void enforceWritePermission(String permission) { + if (getContext().checkCallingOrSelfPermission(permission) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Permission denial: writing to settings requires:" + + permission); + } } /* @@ -999,332 +1015,830 @@ public class SettingsProvider extends ContentProvider { * 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. + * @returns whether the enabled location providers changed. */ - 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 boolean updateLocationProvidersAllowed(String value, int owningUserId) { + if (TextUtils.isEmpty(value)) { + return false; + } - 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; + final char prefix = value.charAt(0); + if (prefix != '+' && prefix != '-') { + return false; + } - 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++; - } + // skip prefix + value = value.substring(1); - 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; - } + Setting settingValue = getSecureSettingLocked( + Settings.Secure.LOCATION_PROVIDERS_ALLOWED, owningUserId); - if (newProviders != null) { - initialValues.put(Settings.Secure.VALUE, newProviders); - } + 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; + } + } 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++; } + + newProviders = oldProviders.substring(0, index); + if (end < oldProviders.length()) { + newProviders += oldProviders.substring(end); + } + } else { + // nothing changed, so no need to update the database + return false; } + updateSecureSettingLocked(Settings.Secure.LOCATION_PROVIDERS_ALLOWED, + newProviders, owningUserId); + return true; } - @Override - public Uri insert(Uri url, ContentValues initialValues) { - return insertForUser(url, initialValues, UserHandle.getCallingUserId()); + 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); + } } - // 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 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."); + } } + } - if (LOCAL_LOGV) Slog.v(TAG, "insert(" + url + ") for user " + desiredUserHandle - + " by " + callingUser); - - SqlArguments args = new SqlArguments(url); - if (TABLE_FAVORITES.equals(args.table)) { - return null; + 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); + } - // 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; + private static Bundle packageValueForCallResult(Setting setting) { + if (setting == null) { + return NULL_SETTING; } + return Bundle.forPair(Settings.NameValueTable.VALUE, setting.getValue()); + } - // 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 static int getRequestingUserId(Bundle args) { + final int callingUserId = UserHandle.getCallingUserId(); + return (args != null) ? args.getInt(Settings.CALL_METHOD_USER_KEY, callingUserId) + : callingUserId; + } - // Check write permissions only after determining which table the insert will touch - checkWritePermissions(args); + private static String getSettingValue(Bundle args) { + return (args != null) ? args.getString(Settings.NameValueTable.VALUE) : null; + } - checkUserRestrictions(name, desiredUserHandle); + 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); + } - // The global table is stored under the owner, always - if (TABLE_GLOBAL.equals(args.table)) { - desiredUserHandle = UserHandle.USER_OWNER; + private static MatrixCursor packageSettingForQuery(Setting setting, String[] projection) { + if (setting == null) { + return new MatrixCursor(projection, 0); } + MatrixCursor cursor = new MatrixCursor(projection, 1); + appendSettingToCursor(cursor, setting); + return cursor; + } - SettingsCache cache = cacheForTable(desiredUserHandle, args.table); - String value = initialValues.getAsString(Settings.NameValueTable.VALUE); - if (SettingsCache.isRedundantSetValue(cache, name, value)) { - return Uri.withAppendedPath(url, name); + private static String[] normalizeProjection(String[] projection) { + if (projection == null) { + return ALL_COLUMNS; } - final AtomicInteger mutationCount; - synchronized (this) { - mutationCount = sKnownMutationsInFlight.get(callingUser); + 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); + } } - if (mutationCount != null) { - mutationCount.incrementAndGet(); + + 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; + } } - DatabaseHelper dbH = getOrEstablishDatabase(desiredUserHandle); - SQLiteDatabase db = dbH.getWritableDatabase(); - final long rowId = db.insert(args.table, null, initialValues); - if (mutationCount != null) { - mutationCount.decrementAndGet(); + + cursor.addRow(values); + } + + 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); + return; + } 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); + return; + } else if (supportAll && where == null && whereArgs == null) { + name = null; + table = computeTableForSetting(uri, null); + return; + } + } break; + + case 2: { + if (where == null && whereArgs == null) { + name = uri.getPathSegments().get(1); + table = computeTableForSetting(uri, name); + return; + } + } break; + } + + EventLogTags.writeUnsupportedSettingsQuery( + uri.toSafeString(), where, Arrays.toString(whereArgs)); + String message = String.format( "Supported SQL:\n" + + " uri content://some_table/some_property with null where and where args\n" + + " uri content://some_table with query name=? and single name as arg\n" + + " uri content://some_table with query name=some_name and null args\n" + + " but got - uri:%1s, where:%2s whereArgs:%3s", uri, where, + Arrays.toString(whereArgs)); + throw new IllegalArgumentException(message); } - if (rowId <= 0) return null; - SettingsCache.populate(cache, initialValues); // before we notify + private static String computeTableForSetting(Uri uri, String name) { + String table = getValidTableOrThrow(uri); + + if (name != null) { + if (sSystemMovedToSecureSettings.contains(name)) { + table = TABLE_SECURE; + } + + if (sSystemMovedToGlobalSettings.contains(name)) { + table = TABLE_GLOBAL; + } + + if (sSecureMovedToGlobalSettings.contains(name)) { + table = TABLE_GLOBAL; + } + + if (sGlobalMovedToSecureSettings.contains(name)) { + table = TABLE_SECURE; + } + } - 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; + return table; + } } - @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 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; + + 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(); } - checkWritePermissions(args); - final AtomicInteger mutationCount; - synchronized (this) { - mutationCount = sKnownMutationsInFlight.get(callingUser); + public List<String> getSettingsNamesLocked(int type, int userId) { + final int key = makeKey(type, userId); + SettingsState settingsState = peekSettingsStateLocked(key); + return settingsState.getSettingNamesLocked(); } - if (mutationCount != null) { - mutationCount.incrementAndGet(); + + public SettingsState getSettingsLocked(int type, int userId) { + final int key = makeKey(type, userId); + return peekSettingsStateLocked(key); } - DatabaseHelper dbH = getOrEstablishDatabase(callingUser); - SQLiteDatabase db = dbH.getWritableDatabase(); - int count = db.delete(args.table, args.where, args.args); - if (mutationCount != null) { - mutationCount.decrementAndGet(); + + 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 (count > 0) { - invalidateCache(callingUser, args.table); // before we notify - sendNotify(url, callingUser); + + 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); + } } - 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; + public void removeUserStateLocked(int userId, boolean permanently) { + // We always keep the global settings in memory. + + // 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); + } + }); + } + } + + // 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); + } + }); + } + } } - checkWritePermissions(args); - checkUserRestrictions(initialValues.getAsString(Settings.Secure.NAME), callingUser); - final AtomicInteger mutationCount; - synchronized (this) { - mutationCount = sKnownMutationsInFlight.get(callingUser); + 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; } - if (mutationCount != null) { - mutationCount.incrementAndGet(); + + 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; } - DatabaseHelper dbH = getOrEstablishDatabase(callingUser); - SQLiteDatabase db = dbH.getWritableDatabase(); - int count = db.update(args.table, initialValues, args.where, args.args); - if (mutationCount != null) { - mutationCount.decrementAndGet(); + + public Setting getSettingLocked(int type, int userId, String name) { + final int key = makeKey(type, userId); + + SettingsState settingsState = peekSettingsStateLocked(key); + return settingsState.getSettingLocked(name); } - if (count > 0) { - invalidateCache(callingUser, args.table); // before we notify - sendNotify(url, callingUser); + + 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; } - 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"); - } + 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); - /** - * 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> { + final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId); + SettingsState secureSettings = mSettingsStates.get(secureKey); + secureSettings.onPackageRemovedLocked(packageName); - private final String mCacheName; - private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped. + final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId); + SettingsState systemSettings = mSettingsStates.get(systemKey); + systemSettings.onPackageRemovedLocked(packageName); + } + + private SettingsState peekSettingsStateLocked(int key) { + SettingsState settingsState = mSettingsStates.get(key); + if (settingsState != null) { + return settingsState; + } - public SettingsCache(String name) { - super(MAX_CACHE_ENTRIES); - mCacheName = name; + ensureSettingsForUserLocked(getUserIdFromKey(key)); + return mSettingsStates.get(key); } - /** - * Is the whole database table slurped into this cache? - */ - public boolean fullyMatchesDisk() { - synchronized (this) { - return mCacheFullyMatchesDisk; + 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); + } } } - public void setFullyMatchesDisk(boolean value) { - synchronized (this) { - mCacheFullyMatchesDisk = value; + 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); } - @Override - protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) { - if (evicted) { - mCacheFullyMatchesDisk = false; + 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(); } } - /** - * 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 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(); } - return bundle; } - /** - * Populates a key in a given (possibly-null) cache. - */ - public static void populate(SettingsCache cache, ContentValues contentValues) { - if (cache == null) { + private void ensureSecureSettingAndroidIdSetLocked(SettingsState secureSettings) { + Setting value = secureSettings.getSettingLocked(Settings.Secure.ANDROID_ID); + + if (value != null) { return; } - String name = contentValues.getAsString(Settings.NameValueTable.NAME); - if (name == null) { - Log.w(TAG, "null name populating settings cache."); + + 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 value = contentValues.getAsString(Settings.NameValueTable.VALUE); - cache.populate(name, value); + + 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"); + } + } } - 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 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); + } + + private int makeKey(int type, int userId) { + return (type << SETTINGS_TYPE_SHIFT) | userId; + } + + private int getTypeFromKey(int key) { + return key >> SETTINGS_TYPE_SHIFT; + } + + private int getUserIdFromKey(int key) { + return key & ~SETTINGS_TYPE_MASK; + } + + private boolean isGlobalSettingsKey(int key) { + return getTypeFromKey(key) == SETTINGS_TYPE_GLOBAL; + } + + private boolean isSystemSettingsKey(int key) { + return getTypeFromKey(key) == SETTINGS_TYPE_SYSTEM; + } + + private boolean isSecureSettingsKey(int key) { + return getTypeFromKey(key) == SETTINGS_TYPE_SECURE; + } + + 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); + } } - /** - * 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 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); + } + } + + 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; + } + } + } + + 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); + } + + private SettingsState getGlobalSettingsLocked() { + return getSettingsLocked(SETTINGS_TYPE_GLOBAL, UserHandle.USER_OWNER); + } + + private SettingsState getSecureSettingsLocked(int userId) { + return getSettingsLocked(SETTINGS_TYPE_SECURE, userId); + } + + private SettingsState getSystemSettingsLocked(int userId) { + return getSettingsLocked(SETTINGS_TYPE_SYSTEM, userId); + } + + 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..833638c --- /dev/null +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -0,0 +1,575 @@ +/* + * 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" + + " package: " + packageName); + } + + 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; + } + } +} diff --git a/packages/SettingsProvider/test/Android.mk b/packages/SettingsProvider/test/Android.mk new file mode 100644 index 0000000..01c6ccf --- /dev/null +++ b/packages/SettingsProvider/test/Android.mk @@ -0,0 +1,13 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := SettingsProviderTest + +LOCAL_MODULE_TAGS := tests + +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE)
\ No newline at end of file diff --git a/packages/SettingsProvider/test/AndroidManifest.xml b/packages/SettingsProvider/test/AndroidManifest.xml new file mode 100644 index 0000000..7a86b5f --- /dev/null +++ b/packages/SettingsProvider/test/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.providers.setting.test"> + + <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" /> + + <uses-permission android:name="android.permission.WRITE_SETTINGS"/> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/> + <uses-permission android:name="android.permission.MANAGE_USERS"/> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="android.test.InstrumentationTestRunner" + android:targetPackage="com.android.providers.setting.test" + android:label="Settings Provider Tests" /> +</manifest> diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java new file mode 100644 index 0000000..8473db4 --- /dev/null +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java @@ -0,0 +1,196 @@ +/* + * 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.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.UserInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.test.AndroidTestCase; + +import java.util.List; + +/** + * Base class for the SettingContentProvider tests. + */ +abstract class BaseSettingsProviderTest extends AndroidTestCase { + protected static final int SETTING_TYPE_GLOBAL = 1; + protected static final int SETTING_TYPE_SECURE = 2; + protected static final int SETTING_TYPE_SYSTEM = 3; + + protected static final String FAKE_SETTING_NAME = "fake_setting_name"; + protected static final String FAKE_SETTING_NAME_1 = "fake_setting_name1"; + protected static final String FAKE_SETTING_VALUE = "fake_setting_value"; + protected static final String FAKE_SETTING_VALUE_1 = "fake_setting_value_1"; + + private static final String[] NAME_VALUE_COLUMNS = new String[] { + Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE + }; + + protected int mSecondaryUserId = UserHandle.USER_OWNER; + + @Override + public void setContext(Context context) { + super.setContext(context); + + UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + List<UserInfo> users = userManager.getUsers(); + final int userCount = users.size(); + for (int i = 0; i < userCount; i++) { + UserInfo user = users.get(i); + if (!user.isPrimary() && !user.isManagedProfile()) { + mSecondaryUserId = user.id; + break; + } + } + } + + protected void setStringViaFrontEndApiSetting(int type, String name, String value, int userId) { + ContentResolver contentResolver = getContext().getContentResolver(); + + switch (type) { + case SETTING_TYPE_GLOBAL: { + Settings.Global.putStringForUser(contentResolver, name, value, userId); + } break; + + case SETTING_TYPE_SECURE: { + Settings.Secure.putStringForUser(contentResolver, name, value, userId); + } break; + + case SETTING_TYPE_SYSTEM: { + Settings.System.putStringForUser(contentResolver, name, value, userId); + } break; + + default: { + throw new IllegalArgumentException("Invalid type: " + type); + } + } + } + + protected String getStringViaFrontEndApiSetting(int type, String name, int userId) { + ContentResolver contentResolver = getContext().getContentResolver(); + + switch (type) { + case SETTING_TYPE_GLOBAL: { + return Settings.Global.getStringForUser(contentResolver, name, userId); + } + + case SETTING_TYPE_SECURE: { + return Settings.Secure.getStringForUser(contentResolver, name, userId); + } + + case SETTING_TYPE_SYSTEM: { + return Settings.System.getStringForUser(contentResolver, name, userId); + } + + default: { + throw new IllegalArgumentException("Invalid type: " + type); + } + } + } + + protected Uri insertStringViaProviderApi(int type, String name, String value, + boolean withTableRowUri) { + Uri uri = getBaseUriForType(type); + if (withTableRowUri) { + uri = Uri.withAppendedPath(uri, name); + } + ContentValues values = new ContentValues(); + values.put(Settings.NameValueTable.NAME, name); + values.put(Settings.NameValueTable.VALUE, value); + + return getContext().getContentResolver().insert(uri, values); + } + + protected int deleteStringViaProviderApi(int type, String name) { + Uri uri = getBaseUriForType(type); + return getContext().getContentResolver().delete(uri, "name=?", new String[]{name}); + } + + protected int updateStringViaProviderApiSetting(int type, String name, String value) { + Uri uri = getBaseUriForType(type); + ContentValues values = new ContentValues(); + values.put(Settings.NameValueTable.NAME, name); + values.put(Settings.NameValueTable.VALUE, value); + return getContext().getContentResolver().update(uri, values, "name=?", + new String[]{name}); + } + + protected String queryStringViaProviderApi(int type, String name) { + return queryStringViaProviderApi(type, name, false, false); + } + + protected String queryStringViaProviderApi(int type, String name, boolean queryStringInQuotes, + boolean appendNameToUri) { + final Uri uri; + final String queryString; + final String[] queryArgs; + + if (appendNameToUri) { + uri = Uri.withAppendedPath(getBaseUriForType(type), name); + queryString = null; + queryArgs = null; + } else { + uri = getBaseUriForType(type); + queryString = queryStringInQuotes ? "(name=?)" : "name=?"; + queryArgs = new String[]{name}; + } + + Cursor cursor = getContext().getContentResolver().query(uri, NAME_VALUE_COLUMNS, + queryString, queryArgs, null); + + if (cursor == null) { + return null; + } + + try { + if (cursor.moveToFirst()) { + final int valueColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.VALUE); + return cursor.getString(valueColumnIdx); + } + } finally { + cursor.close(); + } + + return null; + } + + protected static Uri getBaseUriForType(int type) { + switch (type) { + case SETTING_TYPE_GLOBAL: { + return Settings.Global.CONTENT_URI; + } + + case SETTING_TYPE_SECURE: { + return Settings.Secure.CONTENT_URI; + } + + case SETTING_TYPE_SYSTEM: { + return Settings.System.CONTENT_URI; + } + + default: { + throw new IllegalArgumentException("Invalid type: " + type); + } + } + } +} diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java new file mode 100644 index 0000000..d581f3b --- /dev/null +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java @@ -0,0 +1,121 @@ +/* + * 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.SystemClock; +import android.os.UserHandle; +import android.util.Log; + +/** +* Performance tests for the SettingContentProvider. +*/ +public class SettingsProviderPerformanceTest extends BaseSettingsProviderTest { + private static final String LOG_TAG = "SettingsProviderPerformanceTest"; + + private static final int ITERATION_COUNT = 100; + + private static final int MICRO_SECONDS_IN_MILLISECOND = 1000; + + private static final long MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS = 20; + + public void testSetAndGetPerformanceForGlobalViaFrontEndApi() throws Exception { + // Start with a clean slate. + insertStringViaProviderApi(SETTING_TYPE_GLOBAL, + FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false); + + final long startTimeMicro = SystemClock.currentTimeMicro(); + + try { + for (int i = 0; i < ITERATION_COUNT; i++) { + // Set the setting to its first value. + updateStringViaProviderApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE); + + // Make sure the setting changed. + String firstValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, + FAKE_SETTING_NAME, UserHandle.USER_OWNER); + assertEquals("Setting value didn't change", FAKE_SETTING_VALUE, firstValue); + + // Set the setting to its second value. + updateStringViaProviderApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE_1); + + // Make sure the setting changed. + String secondValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, + FAKE_SETTING_NAME, UserHandle.USER_OWNER); + assertEquals("Setting value didn't change", FAKE_SETTING_VALUE_1, secondValue); + } + } finally { + // Clean up. + deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME); + } + + final long elapsedTimeMicro = SystemClock.currentTimeMicro() - startTimeMicro; + + final long averageTimePerIterationMillis = (long) ((((float) elapsedTimeMicro) + / ITERATION_COUNT) / MICRO_SECONDS_IN_MILLISECOND); + + Log.i(LOG_TAG, "Average time to set and get setting via provider APIs: " + + averageTimePerIterationMillis + " ms"); + + assertTrue("Setting and getting a settings takes too long.", averageTimePerIterationMillis + < MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS); + } + + public void testSetAndGetPerformanceForGlobalViaProviderApi() throws Exception { + // Start with a clean slate. + deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME); + + final long startTimeMicro = SystemClock.currentTimeMicro(); + + try { + for (int i = 0; i < ITERATION_COUNT; i++) { + // Set the setting to its first value. + setStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE, UserHandle.USER_OWNER); + + // Make sure the setting changed. + String firstValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, + FAKE_SETTING_NAME, UserHandle.USER_OWNER); + assertEquals("Setting value didn't change", FAKE_SETTING_VALUE, firstValue); + + // Set the setting to its second value. + setStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE_1, UserHandle.USER_OWNER); + + // Make sure the setting changed. + String secondValue = getStringViaFrontEndApiSetting(SETTING_TYPE_GLOBAL, + FAKE_SETTING_NAME, UserHandle.USER_OWNER); + assertEquals("Setting value didn't change", FAKE_SETTING_VALUE_1, secondValue); + } + } finally { + // Clean up. + deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME); + } + + final long elapsedTimeMicro = SystemClock.currentTimeMicro() - startTimeMicro; + + final long averageTimePerIterationMillis = (long) ((((float) elapsedTimeMicro) + / ITERATION_COUNT) / MICRO_SECONDS_IN_MILLISECOND); + + Log.i(LOG_TAG, "Average time to set and get setting via front-eng APIs: " + + averageTimePerIterationMillis + " ms"); + + assertTrue("Setting and getting a settings takes too long.", averageTimePerIterationMillis + < MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS); + } +} diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java new file mode 100644 index 0000000..b89fb10 --- /dev/null +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java @@ -0,0 +1,429 @@ +/* + * 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.content.ContentResolver; +import android.content.ContentValues; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Tests for the SettingContentProvider. + * + * Before you run this test you must add a secondary user. + */ +public class SettingsProviderTest extends BaseSettingsProviderTest { + private static final String LOG_TAG = "SettingsProviderTest"; + + private static final long WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec + + private static final String[] NAME_VALUE_COLUMNS = new String[]{ + Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE + }; + + private final Object mLock = new Object(); + + public void testSetAndGetGlobalViaFrontEndApiForOwnerUser() throws Exception { + performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_GLOBAL, UserHandle.USER_OWNER); + } + + public void testSetAndGetGlobalViaFrontEndApiForNonOwnerUser() throws Exception { + if (mSecondaryUserId == UserHandle.USER_OWNER) { + Log.w(LOG_TAG, "No secondary user. Skipping " + + "testSetAndGetGlobalViaFrontEndApiForNonOwnerUser"); + return; + } + performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_GLOBAL, mSecondaryUserId); + } + + public void testSetAndGetSecureViaFrontEndApiForOwnerUser() throws Exception { + performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SECURE, UserHandle.USER_OWNER); + } + + public void testSetAndGetSecureViaFrontEndApiForNonOwnerUser() throws Exception { + if (mSecondaryUserId == UserHandle.USER_OWNER) { + Log.w(LOG_TAG, "No secondary user. Skipping " + + "testSetAndGetSecureViaFrontEndApiForNonOwnerUser"); + return; + } + performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SECURE, mSecondaryUserId); + } + + public void testSetAndGetSystemViaFrontEndApiForOwnerUser() throws Exception { + performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SYSTEM, UserHandle.USER_OWNER); + } + + public void testSetAndGetSystemViaFrontEndApiForNonOwnerUser() throws Exception { + if (mSecondaryUserId == UserHandle.USER_OWNER) { + Log.w(LOG_TAG, "No secondary user. Skipping " + + "testSetAndGetSystemViaFrontEndApiForNonOwnerUser"); + return; + } + performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SYSTEM, mSecondaryUserId); + } + + public void testSetAndGetGlobalViaProviderApi() throws Exception { + performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_GLOBAL); + } + + public void testSetAndGetSecureViaProviderApi() throws Exception { + performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_SECURE); + } + + public void testSetAndGetSystemViaProviderApi() throws Exception { + performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_SYSTEM); + } + + public void testSelectAllGlobalViaProviderApi() throws Exception { + setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_GLOBAL, + FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false); + try { + queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(SETTING_TYPE_GLOBAL, + FAKE_SETTING_NAME); + } finally { + deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME); + } + } + + public void testSelectAllSecureViaProviderApi() throws Exception { + setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_SECURE, + FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false); + try { + queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(SETTING_TYPE_SECURE, + FAKE_SETTING_NAME); + } finally { + deleteStringViaProviderApi(SETTING_TYPE_SECURE, FAKE_SETTING_NAME); + } + } + + public void testSelectAllSystemViaProviderApi() throws Exception { + setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_SYSTEM, + FAKE_SETTING_NAME, FAKE_SETTING_VALUE, true); + try { + queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(SETTING_TYPE_SYSTEM, + FAKE_SETTING_NAME); + } finally { + deleteStringViaProviderApi(SETTING_TYPE_SYSTEM, FAKE_SETTING_NAME); + } + } + + public void testQueryUpdateDeleteGlobalViaProviderApi() throws Exception { + doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_GLOBAL); + } + + public void testQueryUpdateDeleteSecureViaProviderApi() throws Exception { + doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_SECURE); + } + + public void testQueryUpdateDeleteSystemViaProviderApi() throws Exception { + doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_SYSTEM); + } + + public void testBulkInsertGlobalViaProviderApi() throws Exception { + toTestBulkInsertViaProviderApiForType(SETTING_TYPE_GLOBAL); + } + + public void testBulkInsertSystemViaProviderApi() throws Exception { + toTestBulkInsertViaProviderApiForType(SETTING_TYPE_SYSTEM); + } + + public void testBulkInsertSecureViaProviderApi() throws Exception { + toTestBulkInsertViaProviderApiForType(SETTING_TYPE_SECURE); + } + + public void testAppCannotRunsSystemOutOfMemoryWritingSystemSettings() throws Exception { + int insertedCount = 0; + try { + for (; insertedCount < 1200; insertedCount++) { + Log.w(LOG_TAG, "Adding app specific setting: " + insertedCount); + insertStringViaProviderApi(SETTING_TYPE_SYSTEM, + String.valueOf(insertedCount), FAKE_SETTING_VALUE, false); + } + fail("Adding app specific settings must be bound."); + } catch (Exception e) { + for (; insertedCount >= 0; insertedCount--) { + Log.w(LOG_TAG, "Removing app specific setting: " + insertedCount); + deleteStringViaProviderApi(SETTING_TYPE_SYSTEM, + String.valueOf(insertedCount)); + } + } + } + + public void testQueryStringInBracketsGlobalViaProviderApiForType() throws Exception { + doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_GLOBAL); + } + + public void testQueryStringInBracketsSecureViaProviderApiForType() throws Exception { + doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_SECURE); + } + + public void testQueryStringInBracketsSystemViaProviderApiForType() throws Exception { + doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_SYSTEM); + } + + public void testQueryStringWithAppendedNameToUriViaProviderApi() throws Exception { + // Make sure we have a clean slate. + deleteStringViaProviderApi(SETTING_TYPE_SYSTEM, FAKE_SETTING_NAME); + + try { + // Insert the setting. + final Uri uri = insertStringViaProviderApi(SETTING_TYPE_SYSTEM, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE, false); + Uri expectUri = Uri.withAppendedPath(getBaseUriForType(SETTING_TYPE_SYSTEM), + FAKE_SETTING_NAME); + assertEquals("Did not get expected Uri.", expectUri, uri); + + // Make sure the first setting is there. + String firstValue = queryStringViaProviderApi(SETTING_TYPE_SYSTEM, FAKE_SETTING_NAME, + false, true); + assertEquals("Setting must be present", FAKE_SETTING_VALUE, firstValue); + } finally { + // Clean up. + deleteStringViaProviderApi(SETTING_TYPE_SYSTEM, FAKE_SETTING_NAME); + } + } + + private void doTestQueryStringInBracketsViaProviderApiForType(int type) { + // Make sure we have a clean slate. + deleteStringViaProviderApi(type, FAKE_SETTING_NAME); + + try { + // Insert the setting. + final Uri uri = insertStringViaProviderApi(type, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE, false); + Uri expectUri = Uri.withAppendedPath(getBaseUriForType(type), FAKE_SETTING_NAME); + assertEquals("Did not get expected Uri.", expectUri, uri); + + // Make sure the first setting is there. + String firstValue = queryStringViaProviderApi(type, FAKE_SETTING_NAME, true, false); + assertEquals("Setting must be present", FAKE_SETTING_VALUE, firstValue); + } finally { + // Clean up. + deleteStringViaProviderApi(type, FAKE_SETTING_NAME); + } + } + + private void toTestBulkInsertViaProviderApiForType(int type) { + // Make sure we have a clean slate. + deleteStringViaProviderApi(type, FAKE_SETTING_NAME); + deleteStringViaProviderApi(type, FAKE_SETTING_NAME_1); + + try { + Uri uri = getBaseUriForType(type); + ContentValues[] allValues = new ContentValues[2]; + + // Insert the first setting. + ContentValues firstValues = new ContentValues(); + firstValues.put(Settings.NameValueTable.NAME, FAKE_SETTING_NAME); + firstValues.put(Settings.NameValueTable.VALUE, FAKE_SETTING_VALUE); + allValues[0] = firstValues; + + // Insert the first setting. + ContentValues secondValues = new ContentValues(); + secondValues.put(Settings.NameValueTable.NAME, FAKE_SETTING_NAME_1); + secondValues.put(Settings.NameValueTable.VALUE, FAKE_SETTING_VALUE_1); + allValues[1] = secondValues; + + // Verify insertion count. + final int insertCount = getContext().getContentResolver().bulkInsert(uri, allValues); + assertSame("Couldn't insert both values", 2, insertCount); + + // Make sure the first setting is there. + String firstValue = queryStringViaProviderApi(type, FAKE_SETTING_NAME); + assertEquals("First setting must be present", FAKE_SETTING_VALUE, firstValue); + + // Make sure the second setting is there. + String secondValue = queryStringViaProviderApi(type, FAKE_SETTING_NAME_1); + assertEquals("Second setting must be present", FAKE_SETTING_VALUE_1, secondValue); + } finally { + // Clean up. + deleteStringViaProviderApi(type, FAKE_SETTING_NAME); + deleteStringViaProviderApi(type, FAKE_SETTING_NAME_1); + } + } + + private void doTestQueryUpdateDeleteGlobalViaProviderApiForType(int type) throws Exception { + // Make sure it is not there. + deleteStringViaProviderApi(type, FAKE_SETTING_NAME); + + // Now selection should return nothing. + String value = queryStringViaProviderApi(type, FAKE_SETTING_NAME); + assertNull("Setting should not be present.", value); + + // Insert the setting. + Uri uri = insertStringViaProviderApi(type, + FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false); + Uri expectUri = Uri.withAppendedPath(getBaseUriForType(type), FAKE_SETTING_NAME); + assertEquals("Did not get expected Uri.", expectUri, uri); + + // Now selection should return the setting. + value = queryStringViaProviderApi(type, FAKE_SETTING_NAME); + assertEquals("Setting should be present.", FAKE_SETTING_VALUE, value); + + // Update the setting. + final int changeCount = updateStringViaProviderApiSetting(type, + FAKE_SETTING_NAME, FAKE_SETTING_VALUE_1); + assertEquals("Did not get expected change count.", 1, changeCount); + + // Now selection should return the new setting. + value = queryStringViaProviderApi(type, FAKE_SETTING_NAME); + assertEquals("Setting should be present.", FAKE_SETTING_VALUE_1, value); + + // Delete the setting. + final int deletedCount = deleteStringViaProviderApi(type, + FAKE_SETTING_NAME); + assertEquals("Did not get expected deleted count", 1, deletedCount); + + // Now selection should return nothing. + value = queryStringViaProviderApi(type, FAKE_SETTING_NAME); + assertNull("Setting should not be present.", value); + } + + private void performSetAndGetSettingTestViaFrontEndApi(int type, int userId) + throws Exception { + try { + // Change the setting and assert a successful change. + setSettingViaFrontEndApiAndAssertSuccessfulChange(type, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE, userId); + } finally { + // Remove the setting. + setStringViaFrontEndApiSetting(type, FAKE_SETTING_NAME, null, userId); + } + } + + private void performSetAndGetSettingTestViaProviderApi(int type) + throws Exception { + try { + // Change the setting and assert a successful change. + setSettingViaProviderApiAndAssertSuccessfulChange(type, FAKE_SETTING_NAME, + FAKE_SETTING_VALUE, true); + } finally { + // Remove the setting. + setSettingViaProviderApiAndAssertSuccessfulChange(type, FAKE_SETTING_NAME, null, + true); + } + } + + private void setSettingViaFrontEndApiAndAssertSuccessfulChange(final int type, + final String name, final String value, final int userId) throws Exception { + setSettingAndAssertSuccessfulChange(new Runnable() { + @Override + public void run() { + setStringViaFrontEndApiSetting(type, name, value, userId); + } + }, type, name, value, userId); + } + + private void setSettingViaProviderApiAndAssertSuccessfulChange(final int type, + final String name, final String value, final boolean withTableRowUri) + throws Exception { + setSettingAndAssertSuccessfulChange(new Runnable() { + @Override + public void run() { + insertStringViaProviderApi(type, name, value, withTableRowUri); + } + }, type, name, value, UserHandle.USER_OWNER); + } + + private void setSettingAndAssertSuccessfulChange(Runnable setCommand, final int type, + final String name, final String value, final int userId) throws Exception { + ContentResolver contentResolver = getContext().getContentResolver(); + + final Uri settingUri = getBaseUriForType(type); + + final AtomicBoolean success = new AtomicBoolean(); + + ContentObserver contentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + public void onChange(boolean selfChange, Uri changeUri, int changeId) { + Log.i(LOG_TAG, "onChange(" + selfChange + ", " + changeUri + ", " + changeId + ")"); + assertEquals("Wrong change Uri", changeUri, settingUri); + assertEquals("Wrong user id", userId, changeId); + String changeValue = getStringViaFrontEndApiSetting(type, name, userId); + assertEquals("Wrong setting value", value, changeValue); + + success.set(true); + + synchronized (mLock) { + mLock.notifyAll(); + } + } + }; + + contentResolver.registerContentObserver(settingUri, false, contentObserver, userId); + + try { + setCommand.run(); + + final long startTimeMillis = SystemClock.uptimeMillis(); + synchronized (mLock) { + if (success.get()) { + return; + } + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + if (elapsedTimeMillis > WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS) { + fail("Could not change setting for " + + WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS + " ms"); + } + final long remainingTimeMillis = WAIT_FOR_SETTING_URI_CHANGE_TIMEOUT_MILLIS + - elapsedTimeMillis; + try { + mLock.wait(remainingTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } finally { + contentResolver.unregisterContentObserver(contentObserver); + } + } + + private void queryAllSettingsViaProviderApiSettingAndAssertSettingPresent(int type, + String name) { + Uri uri = getBaseUriForType(type); + + Cursor cursor = getContext().getContentResolver().query(uri, NAME_VALUE_COLUMNS, + null, null, null); + + if (cursor == null || !cursor.moveToFirst()) { + fail("Nothing selected"); + } + + try { + final int nameColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.NAME); + + while (cursor.moveToNext()) { + String currentName = cursor.getString(nameColumnIdx); + if (name.equals(currentName)) { + return; + } + } + + fail("Not found setting: " + name); + } finally { + cursor.close(); + } + } +} diff --git a/packages/SystemUI/res/layout/super_status_bar.xml b/packages/SystemUI/res/layout/super_status_bar.xml index 6d3f976..532e1b7 100644 --- a/packages/SystemUI/res/layout/super_status_bar.xml +++ b/packages/SystemUI/res/layout/super_status_bar.xml @@ -22,9 +22,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:focusable="true" - android:fitsSystemWindows="true" - android:descendantFocusability="afterDescendants"> + android:fitsSystemWindows="true"> <com.android.systemui.statusbar.BackDropView android:id="@+id/backdrop" @@ -45,7 +43,8 @@ <com.android.systemui.statusbar.ScrimView android:id="@+id/scrim_behind" android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_height="match_parent" + android:importantForAccessibility="no" /> <include layout="@layout/status_bar" android:layout_width="match_parent" @@ -82,6 +81,7 @@ <com.android.systemui.statusbar.ScrimView android:id="@+id/scrim_in_front" android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_height="match_parent" + android:importantForAccessibility="no" /> </com.android.systemui.statusbar.phone.StatusBarWindowView> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 42b50d4..8a73fca 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -244,10 +244,10 @@ <bool name="doze_pulse_on_notifications">true</bool> <!-- Doze: when to pulse after a buzzworthy notification arrives --> - <string name="doze_pulse_schedule" translatable="false">1s,10s,30s,60s,120s</string> + <string name="doze_pulse_schedule" translatable="false">1s,10s,30s,60s</string> <!-- Doze: maximum number of times the notification pulse schedule can be reset --> - <integer name="doze_pulse_schedule_resets">3</integer> + <integer name="doze_pulse_schedule_resets">2</integer> <!-- Doze: duration to avoid false pickup gestures triggered by notification vibrations --> <integer name="doze_pickup_vibration_threshold">2000</integer> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 3b99af1..ca54349 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -1181,14 +1181,15 @@ public abstract class BaseStatusBar extends SystemUI implements } if (mUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) { - final boolean allowed = 0 != Settings.Secure.getIntForUser( + final boolean allowedByUser = 0 != Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle); final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */, userHandle); final boolean allowedByDpm = (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0; - mUsersAllowingPrivateNotifications.append(userHandle, allowed && allowedByDpm); + final boolean allowed = allowedByUser && allowedByDpm; + mUsersAllowingPrivateNotifications.append(userHandle, allowed); return allowed; } diff --git a/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java b/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java index c6b76f1..74391eb 100644 --- a/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java +++ b/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java @@ -72,16 +72,22 @@ public class PacService extends Service { @Override public String resolvePacFile(String host, String url) throws RemoteException { try { + if (host == null) { + throw new IllegalArgumentException("The host must not be null"); + } + if (url == null) { + throw new IllegalArgumentException("The URL must not be null"); + } // Check for characters that could be used for an injection attack. new URL(url); for (char c : host.toCharArray()) { if (!Character.isLetterOrDigit(c) && (c != '.') && (c != '-')) { - throw new RemoteException("Invalid host was passed"); + throw new IllegalArgumentException("Invalid host was passed"); } } return mPacNative.makeProxyRequest(url, host); } catch (MalformedURLException e) { - throw new RemoteException("Invalid URL was passed"); + throw new IllegalArgumentException("Invalid URL was passed"); } } |