summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/PrintSpooler/res/values-si-rLK/strings.xml2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java35
-rw-r--r--packages/SettingsLib/tests/src/com/android/settingslib/wifi/WifiTrackerTest.java23
-rw-r--r--packages/SettingsProvider/Android.mk3
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java115
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/EventLogTags.logtags5
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java19
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java2586
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java575
-rw-r--r--packages/SettingsProvider/test/Android.mk13
-rw-r--r--packages/SettingsProvider/test/AndroidManifest.xml35
-rw-r--r--packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java196
-rw-r--r--packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java121
-rw-r--r--packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java429
-rw-r--r--packages/SystemUI/res/layout/super_status_bar.xml10
-rw-r--r--packages/SystemUI/res/values/config.xml4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java5
-rw-r--r--packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java10
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");
}
}