/* * Copyright (C) 2007 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 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.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.PackageManager; import android.content.pm.UserInfo; import android.database.AbstractCursor; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.DropBoxManager; import android.os.FileObserver; 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.Slog; import android.util.SparseArray; public class SettingsProvider extends ContentProvider { private static final String TAG = "SettingsProvider"; private static final boolean LOCAL_LOGV = false; private static final boolean USER_CHECK_THROWS = true; private static final String TABLE_SYSTEM = "system"; private static final String TABLE_SECURE = "secure"; private static final String TABLE_GLOBAL = "global"; private static final String TABLE_FAVORITES = "favorites"; private static final String TABLE_OLD_FAVORITES = "old_favorites"; private static final String[] COLUMN_VALUE = new String[] { "value" }; // 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 sSystemCaches = new SparseArray(); private static final SparseArray sSecureCaches = new SparseArray(); private static final SettingsCache sGlobalCache = new SettingsCache(TABLE_GLOBAL); // 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 sKnownMutationsInFlight = new SparseArray(); // Each defined user has their own settings protected final SparseArray mOpenHelpers = new SparseArray(); // Keep the list of managed profiles synced here private List mManagedProfiles = null; // 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; private static final Bundle NULL_SETTING = Bundle.forPair("value", null); // 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); private UserManager mUserManager; private BackupManager mBackupManager; /** * Settings which need to be treated as global/shared in multi-user environments. */ static final HashSet sSecureGlobalKeys; static final HashSet sSystemGlobalKeys; // Settings that cannot be modified if associated user restrictions are enabled. static final Map sRestrictedKeys; private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid"; static final HashSet sSecureCloneToManagedKeys; static final HashSet sSystemCloneToManagedKeys; 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(); Settings.Secure.getMovedKeys(sSecureGlobalKeys); // Keys from the 'system' table now moved to 'global' // These must match Settings.System.MOVED_TO_GLOBAL sSystemGlobalKeys = new HashSet(); Settings.System.getNonLegacyMovedKeys(sSystemGlobalKeys); sRestrictedKeys = new HashMap(); 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); sSecureCloneToManagedKeys = new HashSet(); for (int i = 0; i < Settings.Secure.CLONE_TO_MANAGED_PROFILE.length; i++) { sSecureCloneToManagedKeys.add(Settings.Secure.CLONE_TO_MANAGED_PROFILE[i]); } sSystemCloneToManagedKeys = new HashSet(); for (int i = 0; i < Settings.System.CLONE_TO_MANAGED_PROFILE.length; i++) { sSystemCloneToManagedKeys.add(Settings.System.CLONE_TO_MANAGED_PROFILE[i]); } } private boolean settingMovedToGlobal(final String name) { return sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name); } /** * 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); } 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); } 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; } } } else { // of the form content://bookmarks/19 this.where = "_id=" + ContentUris.parseId(url); this.args = 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); } this.where = null; this.args = null; } else { throw new IllegalArgumentException("Invalid URI: " + url); } } } /** * 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); } } /** * 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); } if (LOCAL_LOGV) Log.v(TAG, "notifying for " + notifyTarget + ": " + uri); } else { if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri); } } /** * 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)); } } 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."); } } // 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 sObserverInstances = new SparseArray(); 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; } 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()); setAppOps(AppOpsManager.OP_NONE, AppOpsManager.OP_WRITE_SETTINGS); establishDbTracking(UserHandle.USER_OWNER); IntentFilter userFilter = new IntentFilter(); userFilter.addAction(Intent.ACTION_USER_REMOVED); userFilter.addAction(Intent.ACTION_USER_ADDED); getContext().registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final int userHandle = 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(); } } }, userFilter); onProfilesChanged(); return 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); } mOpenHelpers.delete(userHandle); sSystemCaches.delete(userHandle); sSecureCaches.delete(userHandle); sKnownMutationsInFlight.delete(userHandle); onProfilesChanged(); } } /** * 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 void establishDbTracking(int userHandle) { if (LOCAL_LOGV) { Slog.i(TAG, "Installing settings db helper and caches for user " + userHandle); } DatabaseHelper dbhelper; synchronized (this) { dbhelper = mOpenHelpers.get(userHandle); if (dbhelper == null) { dbhelper = new DatabaseHelper(getContext(), userHandle); mOpenHelpers.append(userHandle, dbhelper); sSystemCaches.append(userHandle, new SettingsCache(TABLE_SYSTEM)); sSecureCaches.append(userHandle, new SettingsCache(TABLE_SECURE)); sKnownMutationsInFlight.append(userHandle, new AtomicInteger(0)); } } // 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(); } } ensureAndroidIdIsSet(userHandle); startAsyncCachePopulation(userHandle); } class CachePrefetchThread extends Thread { private int mUserHandle; CachePrefetchThread(int userHandle) { super("populate-settings-caches"); mUserHandle = userHandle; } @Override public void run() { fullyPopulateCaches(mUserHandle); } } private void startAsyncCachePopulation(int userHandle) { new CachePrefetchThread(userHandle).start(); } private void fullyPopulateCaches(final int userHandle) { DatabaseHelper dbHelper; synchronized (this) { dbHelper = mOpenHelpers.get(userHandle); } if (dbHelper == null) { // User is gone. return; } // Only populate the globals cache once, for the owning user if (userHandle == UserHandle.USER_OWNER) { fullyPopulateCache(dbHelper, TABLE_GLOBAL, sGlobalCache); } fullyPopulateCache(dbHelper, TABLE_SECURE, sSecureCaches.get(userHandle)); fullyPopulateCache(dbHelper, TABLE_SYSTEM, sSystemCaches.get(userHandle)); } // Slurp all values (if sane in number & size) into cache. private void fullyPopulateCache(DatabaseHelper dbHelper, String table, SettingsCache cache) { SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor c = db.query( table, new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE }, null, null, null, null, null, "" + (MAX_CACHE_ENTRIES + 1) /* limit */); try { synchronized (cache) { cache.evictAll(); cache.setFullyMatchesDisk(true); // optimistic int rows = 0; while (c.moveToNext()) { rows++; String name = c.getString(0); String value = c.getString(1); cache.populate(name, value); } if (rows > MAX_CACHE_ENTRIES) { // Somewhat redundant, as removeEldestEntry() will // have already done this, but to be explicit: cache.setFullyMatchesDisk(false); Log.d(TAG, "row count exceeds max cache entries for table " + table); } if (LOCAL_LOGV) Log.d(TAG, "cache for settings table '" + table + "' rows=" + rows + "; fullycached=" + cache.fullyMatchesDisk()); } } finally { c.close(); } } 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; } 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(); } } // Lazy-initialize the settings caches for non-primary users private SettingsCache getOrConstructCache(int callingUser, SparseArray which) { getOrEstablishDatabase(callingUser); // ignore return value; we don't need it return which.get(callingUser); } // 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); } } long oldId = Binder.clearCallingIdentity(); try { DatabaseHelper dbHelper; synchronized (this) { dbHelper = mOpenHelpers.get(callingUser); } if (null == dbHelper) { establishDbTracking(callingUser); synchronized (this) { dbHelper = mOpenHelpers.get(callingUser); } } return dbHelper; } finally { Binder.restoreCallingIdentity(oldId); } } public SettingsCache cacheForTable(final int callingUser, String tableName) { if (TABLE_SYSTEM.equals(tableName)) { return getOrConstructCache(callingUser, sSystemCaches); } if (TABLE_SECURE.equals(tableName)) { return getOrConstructCache(callingUser, sSecureCaches); } if (TABLE_GLOBAL.equals(tableName)) { return sGlobalCache; } return null; } /** * 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; } synchronized (cache) { cache.evictAll(); cache.mCacheFullyMatchesDisk = false; } } /** * 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; } } /** * 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); } } // 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; // 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)); } // Also need to take care of app op. if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SETTINGS, Binder.getCallingUid(), getCallingPackage()) != AppOpsManager.MODE_ALLOWED) { return null; } final ContentValues values = new ContentValues(); values.put(Settings.NameValueTable.NAME, request); values.put(Settings.NameValueTable.VALUE, newValue); if (Settings.CALL_METHOD_PUT_SYSTEM.equals(method)) { if (LOCAL_LOGV) { Slog.v(TAG, "call_put(system:" + request + "=" + newValue + ") for " + callingUser); } // Extra check for USER_OWNER to optimize for the 99% if (callingUser != UserHandle.USER_OWNER && shouldShadowParentProfile(callingUser, sSystemCloneToManagedKeys, request)) { // Don't write these settings, as they are cloned from the parent profile return null; } insertForUser(Settings.System.CONTENT_URI, values, callingUser); // Clone the settings to the managed profiles so that notifications can be sent out if (callingUser == UserHandle.USER_OWNER && mManagedProfiles != null && sSystemCloneToManagedKeys.contains(request)) { final long token = Binder.clearCallingIdentity(); try { for (int i = mManagedProfiles.size() - 1; i >= 0; i--) { if (LOCAL_LOGV) { Slog.v(TAG, "putting to additional user " + mManagedProfiles.get(i).id); } insertForUser(Settings.System.CONTENT_URI, values, mManagedProfiles.get(i).id); } } finally { Binder.restoreCallingIdentity(token); } } } else if (Settings.CALL_METHOD_PUT_SECURE.equals(method)) { if (LOCAL_LOGV) { Slog.v(TAG, "call_put(secure:" + request + "=" + newValue + ") for " + callingUser); } // 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); } } } else if (Settings.CALL_METHOD_PUT_GLOBAL.equals(method)) { if (LOCAL_LOGV) { Slog.v(TAG, "call_put(global:" + request + "=" + newValue + ") for " + callingUser); } insertForUser(Settings.Global.CONTENT_URI, values, callingUser); } else { Slog.w(TAG, "call() with invalid method: " + method); } return null; } /** * 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 keys, String name) { return isManagedProfile(userId) && keys.contains(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; } 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; } } 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(); } cache.putIfAbsent(key, null); return NULL_SETTING; } @Override public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) { return queryForUser(url, select, where, whereArgs, sort, UserHandle.getCallingUserId()); } private 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; } } SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(args.table); 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; } @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; } } @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; } checkWritePermissions(args); SettingsCache cache = cacheForTable(callingUser, args.table); final AtomicInteger mutationCount; synchronized (this) { mutationCount = sKnownMutationsInFlight.get(callingUser); } if (mutationCount != null) { mutationCount.incrementAndGet(); } DatabaseHelper dbH = getOrEstablishDatabase( TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : callingUser); SQLiteDatabase db = dbH.getWritableDatabase(); db.beginTransaction(); try { int numValues = values.length; for (int i = 0; i < numValues; i++) { checkUserRestrictions(values[i].getAsString(Settings.Secure.NAME), callingUser); if (db.insert(args.table, null, values[i]) < 0) return 0; SettingsCache.populate(cache, values[i]); if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]); } db.setTransactionSuccessful(); } finally { db.endTransaction(); if (mutationCount != null) { mutationCount.decrementAndGet(); } } sendNotify(uri, callingUser); return values.length; } /* * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED. * This setting contains a list of the currently enabled location providers. * But helper functions in android.providers.Settings can enable or disable * a single provider by using a "+" or "-" prefix before the provider name. * * @returns whether the database needs to be updated or not, also modifying * 'initialValues' if needed. */ private boolean parseProviderList(Uri url, ContentValues initialValues, int desiredUser) { String value = initialValues.getAsString(Settings.Secure.VALUE); String newProviders = null; if (value != null && value.length() > 1) { char prefix = value.charAt(0); if (prefix == '+' || prefix == '-') { // skip prefix value = value.substring(1); // read list of enabled providers into "providers" String providers = ""; String[] columns = {Settings.Secure.VALUE}; String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'"; Cursor cursor = queryForUser(url, columns, where, null, null, desiredUser); if (cursor != null && cursor.getCount() == 1) { try { cursor.moveToFirst(); providers = cursor.getString(0); } finally { cursor.close(); } } 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; 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++; } newProviders = providers.substring(0, index); if (end < providers.length()) { newProviders += providers.substring(end); } } else { // nothing changed, so no need to update the database return false; } if (newProviders != null) { initialValues.put(Settings.Secure.VALUE, newProviders); } } } return true; } @Override public Uri insert(Uri url, ContentValues initialValues) { return insertForUser(url, initialValues, UserHandle.getCallingUserId()); } // 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"); } 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; } // 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; } // 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 } } // Check write permissions only after determining which table the insert will touch checkWritePermissions(args); checkUserRestrictions(name, desiredUserHandle); // The global table is stored under the owner, always if (TABLE_GLOBAL.equals(args.table)) { desiredUserHandle = UserHandle.USER_OWNER; } SettingsCache cache = cacheForTable(desiredUserHandle, args.table); String value = initialValues.getAsString(Settings.NameValueTable.VALUE); if (SettingsCache.isRedundantSetValue(cache, name, value)) { return Uri.withAppendedPath(url, name); } final AtomicInteger mutationCount; synchronized (this) { mutationCount = sKnownMutationsInFlight.get(callingUser); } if (mutationCount != null) { mutationCount.incrementAndGet(); } DatabaseHelper dbH = getOrEstablishDatabase(desiredUserHandle); SQLiteDatabase db = dbH.getWritableDatabase(); final long rowId = db.insert(args.table, null, initialValues); if (mutationCount != null) { mutationCount.decrementAndGet(); } if (rowId <= 0) return null; SettingsCache.populate(cache, initialValues); // before we notify 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; } @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; } checkWritePermissions(args); final AtomicInteger mutationCount; synchronized (this) { mutationCount = sKnownMutationsInFlight.get(callingUser); } if (mutationCount != null) { mutationCount.incrementAndGet(); } DatabaseHelper dbH = getOrEstablishDatabase(callingUser); SQLiteDatabase db = dbH.getWritableDatabase(); int count = db.delete(args.table, args.where, args.args); if (mutationCount != null) { mutationCount.decrementAndGet(); } if (count > 0) { invalidateCache(callingUser, args.table); // before we notify sendNotify(url, callingUser); } 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; } checkWritePermissions(args); checkUserRestrictions(initialValues.getAsString(Settings.Secure.NAME), callingUser); final AtomicInteger mutationCount; synchronized (this) { mutationCount = sKnownMutationsInFlight.get(callingUser); } if (mutationCount != null) { mutationCount.incrementAndGet(); } DatabaseHelper dbH = getOrEstablishDatabase(callingUser); SQLiteDatabase db = dbH.getWritableDatabase(); int count = db.update(args.table, initialValues, args.where, args.args); if (mutationCount != null) { mutationCount.decrementAndGet(); } if (count > 0) { invalidateCache(callingUser, args.table); // before we notify sendNotify(url, callingUser); } 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"); } /** * 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 { private final String mCacheName; private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped. public SettingsCache(String name) { super(MAX_CACHE_ENTRIES); mCacheName = name; } /** * Is the whole database table slurped into this cache? */ public boolean fullyMatchesDisk() { synchronized (this) { return mCacheFullyMatchesDisk; } } public void setFullyMatchesDisk(boolean value) { synchronized (this) { mCacheFullyMatchesDisk = value; } } @Override protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) { if (evicted) { mCacheFullyMatchesDisk = false; } } /** * 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); } } } return bundle; } /** * Populates a key in a given (possibly-null) cache. */ public static void populate(SettingsCache cache, ContentValues contentValues) { if (cache == null) { return; } String name = contentValues.getAsString(Settings.NameValueTable.NAME); if (name == null) { Log.w(TAG, "null name populating settings cache."); return; } String value = contentValues.getAsString(Settings.NameValueTable.VALUE); cache.populate(name, value); } public void populate(String name, String value) { synchronized (this) { if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) { put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value)); } else { put(name, TOO_LARGE_TO_CACHE_MARKER); } } } /** * 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); } } } }