SettingsProvider.java revision 38e7a60fd7fecdf1c6593724111a92147b4c50ff
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.providers.settings; 18 19import java.io.FileNotFoundException; 20import java.security.SecureRandom; 21import java.util.HashSet; 22import java.util.concurrent.atomic.AtomicBoolean; 23import java.util.concurrent.atomic.AtomicInteger; 24 25import android.app.ActivityManager; 26import android.app.AppOpsManager; 27import android.app.backup.BackupManager; 28import android.content.BroadcastReceiver; 29import android.content.ContentProvider; 30import android.content.ContentUris; 31import android.content.ContentValues; 32import android.content.Context; 33import android.content.Intent; 34import android.content.IntentFilter; 35import android.content.pm.PackageManager; 36import android.content.pm.UserInfo; 37import android.content.res.AssetFileDescriptor; 38import android.database.AbstractCursor; 39import android.database.Cursor; 40import android.database.sqlite.SQLiteDatabase; 41import android.database.sqlite.SQLiteException; 42import android.database.sqlite.SQLiteQueryBuilder; 43import android.media.RingtoneManager; 44import android.net.Uri; 45import android.os.Binder; 46import android.os.Bundle; 47import android.os.DropBoxManager; 48import android.os.FileObserver; 49import android.os.ParcelFileDescriptor; 50import android.os.SystemProperties; 51import android.os.UserHandle; 52import android.os.UserManager; 53import android.provider.MediaStore; 54import android.provider.Settings; 55import android.text.TextUtils; 56import android.util.Log; 57import android.util.LruCache; 58import android.util.Slog; 59import android.util.SparseArray; 60 61public class SettingsProvider extends ContentProvider { 62 private static final String TAG = "SettingsProvider"; 63 private static final boolean LOCAL_LOGV = false; 64 65 private static final String TABLE_SYSTEM = "system"; 66 private static final String TABLE_SECURE = "secure"; 67 private static final String TABLE_GLOBAL = "global"; 68 private static final String TABLE_FAVORITES = "favorites"; 69 private static final String TABLE_OLD_FAVORITES = "old_favorites"; 70 71 private static final String[] COLUMN_VALUE = new String[] { "value" }; 72 73 // Caches for each user's settings, access-ordered for acting as LRU. 74 // Guarded by themselves. 75 private static final int MAX_CACHE_ENTRIES = 200; 76 private static final SparseArray<SettingsCache> sSystemCaches 77 = new SparseArray<SettingsCache>(); 78 private static final SparseArray<SettingsCache> sSecureCaches 79 = new SparseArray<SettingsCache>(); 80 private static final SettingsCache sGlobalCache = new SettingsCache(TABLE_GLOBAL); 81 82 // The count of how many known (handled by SettingsProvider) 83 // database mutations are currently being handled for this user. 84 // Used by file observers to not reload the database when it's ourselves 85 // modifying it. 86 private static final SparseArray<AtomicInteger> sKnownMutationsInFlight 87 = new SparseArray<AtomicInteger>(); 88 89 // Each defined user has their own settings 90 protected final SparseArray<DatabaseHelper> mOpenHelpers = new SparseArray<DatabaseHelper>(); 91 92 // Over this size we don't reject loading or saving settings but 93 // we do consider them broken/malicious and don't keep them in 94 // memory at least: 95 private static final int MAX_CACHE_ENTRY_SIZE = 500; 96 97 private static final Bundle NULL_SETTING = Bundle.forPair("value", null); 98 99 // Used as a sentinel value in an instance equality test when we 100 // want to cache the existence of a key, but not store its value. 101 private static final Bundle TOO_LARGE_TO_CACHE_MARKER = Bundle.forPair("_dummy", null); 102 103 private UserManager mUserManager; 104 private BackupManager mBackupManager; 105 106 /** 107 * Settings which need to be treated as global/shared in multi-user environments. 108 */ 109 static final HashSet<String> sSecureGlobalKeys; 110 static final HashSet<String> sSystemGlobalKeys; 111 112 private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid"; 113 114 static { 115 // Keys (name column) from the 'secure' table that are now in the owner user's 'global' 116 // table, shared across all users 117 // These must match Settings.Secure.MOVED_TO_GLOBAL 118 sSecureGlobalKeys = new HashSet<String>(); 119 Settings.Secure.getMovedKeys(sSecureGlobalKeys); 120 121 // Keys from the 'system' table now moved to 'global' 122 // These must match Settings.System.MOVED_TO_GLOBAL 123 sSystemGlobalKeys = new HashSet<String>(); 124 Settings.System.getNonLegacyMovedKeys(sSystemGlobalKeys); 125 } 126 127 private boolean settingMovedToGlobal(final String name) { 128 return sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name); 129 } 130 131 /** 132 * Decode a content URL into the table, projection, and arguments 133 * used to access the corresponding database rows. 134 */ 135 private static class SqlArguments { 136 public String table; 137 public final String where; 138 public final String[] args; 139 140 /** Operate on existing rows. */ 141 SqlArguments(Uri url, String where, String[] args) { 142 if (url.getPathSegments().size() == 1) { 143 // of the form content://settings/secure, arbitrary where clause 144 this.table = url.getPathSegments().get(0); 145 if (!DatabaseHelper.isValidTable(this.table)) { 146 throw new IllegalArgumentException("Bad root path: " + this.table); 147 } 148 this.where = where; 149 this.args = args; 150 } else if (url.getPathSegments().size() != 2) { 151 throw new IllegalArgumentException("Invalid URI: " + url); 152 } else if (!TextUtils.isEmpty(where)) { 153 throw new UnsupportedOperationException("WHERE clause not supported: " + url); 154 } else { 155 // of the form content://settings/secure/element_name, no where clause 156 this.table = url.getPathSegments().get(0); 157 if (!DatabaseHelper.isValidTable(this.table)) { 158 throw new IllegalArgumentException("Bad root path: " + this.table); 159 } 160 if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table) || 161 TABLE_GLOBAL.equals(this.table)) { 162 this.where = Settings.NameValueTable.NAME + "=?"; 163 final String name = url.getPathSegments().get(1); 164 this.args = new String[] { name }; 165 // Rewrite the table for known-migrated names 166 if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table)) { 167 if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) { 168 this.table = TABLE_GLOBAL; 169 } 170 } 171 } else { 172 // of the form content://bookmarks/19 173 this.where = "_id=" + ContentUris.parseId(url); 174 this.args = null; 175 } 176 } 177 } 178 179 /** Insert new rows (no where clause allowed). */ 180 SqlArguments(Uri url) { 181 if (url.getPathSegments().size() == 1) { 182 this.table = url.getPathSegments().get(0); 183 if (!DatabaseHelper.isValidTable(this.table)) { 184 throw new IllegalArgumentException("Bad root path: " + this.table); 185 } 186 this.where = null; 187 this.args = null; 188 } else { 189 throw new IllegalArgumentException("Invalid URI: " + url); 190 } 191 } 192 } 193 194 /** 195 * Get the content URI of a row added to a table. 196 * @param tableUri of the entire table 197 * @param values found in the row 198 * @param rowId of the row 199 * @return the content URI for this particular row 200 */ 201 private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) { 202 if (tableUri.getPathSegments().size() != 1) { 203 throw new IllegalArgumentException("Invalid URI: " + tableUri); 204 } 205 String table = tableUri.getPathSegments().get(0); 206 if (TABLE_SYSTEM.equals(table) || 207 TABLE_SECURE.equals(table) || 208 TABLE_GLOBAL.equals(table)) { 209 String name = values.getAsString(Settings.NameValueTable.NAME); 210 return Uri.withAppendedPath(tableUri, name); 211 } else { 212 return ContentUris.withAppendedId(tableUri, rowId); 213 } 214 } 215 216 /** 217 * Send a notification when a particular content URI changes. 218 * Modify the system property used to communicate the version of 219 * this table, for tables which have such a property. (The Settings 220 * contract class uses these to provide client-side caches.) 221 * @param uri to send notifications for 222 */ 223 private void sendNotify(Uri uri, int userHandle) { 224 // Update the system property *first*, so if someone is listening for 225 // a notification and then using the contract class to get their data, 226 // the system property will be updated and they'll get the new data. 227 228 boolean backedUpDataChanged = false; 229 String property = null, table = uri.getPathSegments().get(0); 230 final boolean isGlobal = table.equals(TABLE_GLOBAL); 231 if (table.equals(TABLE_SYSTEM)) { 232 property = Settings.System.SYS_PROP_SETTING_VERSION; 233 backedUpDataChanged = true; 234 } else if (table.equals(TABLE_SECURE)) { 235 property = Settings.Secure.SYS_PROP_SETTING_VERSION; 236 backedUpDataChanged = true; 237 } else if (isGlobal) { 238 property = Settings.Global.SYS_PROP_SETTING_VERSION; // this one is global 239 backedUpDataChanged = true; 240 } 241 242 if (property != null) { 243 long version = SystemProperties.getLong(property, 0) + 1; 244 if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version); 245 SystemProperties.set(property, Long.toString(version)); 246 } 247 248 // Inform the backup manager about a data change 249 if (backedUpDataChanged) { 250 mBackupManager.dataChanged(); 251 } 252 // Now send the notification through the content framework. 253 254 String notify = uri.getQueryParameter("notify"); 255 if (notify == null || "true".equals(notify)) { 256 final int notifyTarget = isGlobal ? UserHandle.USER_ALL : userHandle; 257 final long oldId = Binder.clearCallingIdentity(); 258 try { 259 getContext().getContentResolver().notifyChange(uri, null, true, notifyTarget); 260 } finally { 261 Binder.restoreCallingIdentity(oldId); 262 } 263 if (LOCAL_LOGV) Log.v(TAG, "notifying for " + notifyTarget + ": " + uri); 264 } else { 265 if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri); 266 } 267 } 268 269 /** 270 * Make sure the caller has permission to write this data. 271 * @param args supplied by the caller 272 * @throws SecurityException if the caller is forbidden to write. 273 */ 274 private void checkWritePermissions(SqlArguments args) { 275 if ((TABLE_SECURE.equals(args.table) || TABLE_GLOBAL.equals(args.table)) && 276 getContext().checkCallingOrSelfPermission( 277 android.Manifest.permission.WRITE_SECURE_SETTINGS) != 278 PackageManager.PERMISSION_GRANTED) { 279 throw new SecurityException( 280 String.format("Permission denial: writing to secure settings requires %1$s", 281 android.Manifest.permission.WRITE_SECURE_SETTINGS)); 282 } 283 } 284 285 // FileObserver for external modifications to the database file. 286 // Note that this is for platform developers only with 287 // userdebug/eng builds who should be able to tinker with the 288 // sqlite database out from under the SettingsProvider, which is 289 // normally the exclusive owner of the database. But we keep this 290 // enabled all the time to minimize development-vs-user 291 // differences in testing. 292 private static SparseArray<SettingsFileObserver> sObserverInstances 293 = new SparseArray<SettingsFileObserver>(); 294 private class SettingsFileObserver extends FileObserver { 295 private final AtomicBoolean mIsDirty = new AtomicBoolean(false); 296 private final int mUserHandle; 297 private final String mPath; 298 299 public SettingsFileObserver(int userHandle, String path) { 300 super(path, FileObserver.CLOSE_WRITE | 301 FileObserver.CREATE | FileObserver.DELETE | 302 FileObserver.MOVED_TO | FileObserver.MODIFY); 303 mUserHandle = userHandle; 304 mPath = path; 305 } 306 307 public void onEvent(int event, String path) { 308 int modsInFlight = sKnownMutationsInFlight.get(mUserHandle).get(); 309 if (modsInFlight > 0) { 310 // our own modification. 311 return; 312 } 313 Log.d(TAG, "User " + mUserHandle + " external modification to " + mPath 314 + "; event=" + event); 315 if (!mIsDirty.compareAndSet(false, true)) { 316 // already handled. (we get a few update events 317 // during an sqlite write) 318 return; 319 } 320 Log.d(TAG, "User " + mUserHandle + " updating our caches for " + mPath); 321 fullyPopulateCaches(mUserHandle); 322 mIsDirty.set(false); 323 } 324 } 325 326 @Override 327 public boolean onCreate() { 328 mBackupManager = new BackupManager(getContext()); 329 mUserManager = UserManager.get(getContext()); 330 331 setAppOps(AppOpsManager.OP_NONE, AppOpsManager.OP_WRITE_SETTINGS); 332 establishDbTracking(UserHandle.USER_OWNER); 333 334 IntentFilter userFilter = new IntentFilter(); 335 userFilter.addAction(Intent.ACTION_USER_REMOVED); 336 getContext().registerReceiver(new BroadcastReceiver() { 337 @Override 338 public void onReceive(Context context, Intent intent) { 339 if (intent.getAction().equals(Intent.ACTION_USER_REMOVED)) { 340 final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 341 UserHandle.USER_OWNER); 342 if (userHandle != UserHandle.USER_OWNER) { 343 onUserRemoved(userHandle); 344 } 345 } 346 } 347 }, userFilter); 348 return true; 349 } 350 351 void onUserRemoved(int userHandle) { 352 synchronized (this) { 353 // the db file itself will be deleted automatically, but we need to tear down 354 // our caches and other internal bookkeeping. 355 FileObserver observer = sObserverInstances.get(userHandle); 356 if (observer != null) { 357 observer.stopWatching(); 358 sObserverInstances.delete(userHandle); 359 } 360 361 mOpenHelpers.delete(userHandle); 362 sSystemCaches.delete(userHandle); 363 sSecureCaches.delete(userHandle); 364 sKnownMutationsInFlight.delete(userHandle); 365 } 366 } 367 368 private void establishDbTracking(int userHandle) { 369 if (LOCAL_LOGV) { 370 Slog.i(TAG, "Installing settings db helper and caches for user " + userHandle); 371 } 372 373 DatabaseHelper dbhelper; 374 375 synchronized (this) { 376 dbhelper = mOpenHelpers.get(userHandle); 377 if (dbhelper == null) { 378 dbhelper = new DatabaseHelper(getContext(), userHandle); 379 mOpenHelpers.append(userHandle, dbhelper); 380 381 sSystemCaches.append(userHandle, new SettingsCache(TABLE_SYSTEM)); 382 sSecureCaches.append(userHandle, new SettingsCache(TABLE_SECURE)); 383 sKnownMutationsInFlight.append(userHandle, new AtomicInteger(0)); 384 } 385 } 386 387 // Initialization of the db *outside* the locks. It's possible that racing 388 // threads might wind up here, the second having read the cache entries 389 // written by the first, but that's benign: the SQLite helper implementation 390 // manages concurrency itself, and it's important that we not run the db 391 // initialization with any of our own locks held, so we're fine. 392 SQLiteDatabase db = dbhelper.getWritableDatabase(); 393 394 // Watch for external modifications to the database files, 395 // keeping our caches in sync. We synchronize the observer set 396 // separately, and of course it has to run after the db file 397 // itself was set up by the DatabaseHelper. 398 synchronized (sObserverInstances) { 399 if (sObserverInstances.get(userHandle) == null) { 400 SettingsFileObserver observer = new SettingsFileObserver(userHandle, db.getPath()); 401 sObserverInstances.append(userHandle, observer); 402 observer.startWatching(); 403 } 404 } 405 406 ensureAndroidIdIsSet(userHandle); 407 408 startAsyncCachePopulation(userHandle); 409 } 410 411 class CachePrefetchThread extends Thread { 412 private int mUserHandle; 413 414 CachePrefetchThread(int userHandle) { 415 super("populate-settings-caches"); 416 mUserHandle = userHandle; 417 } 418 419 @Override 420 public void run() { 421 fullyPopulateCaches(mUserHandle); 422 } 423 } 424 425 private void startAsyncCachePopulation(int userHandle) { 426 new CachePrefetchThread(userHandle).start(); 427 } 428 429 private void fullyPopulateCaches(final int userHandle) { 430 DatabaseHelper dbHelper = mOpenHelpers.get(userHandle); 431 // Only populate the globals cache once, for the owning user 432 if (userHandle == UserHandle.USER_OWNER) { 433 fullyPopulateCache(dbHelper, TABLE_GLOBAL, sGlobalCache); 434 } 435 fullyPopulateCache(dbHelper, TABLE_SECURE, sSecureCaches.get(userHandle)); 436 fullyPopulateCache(dbHelper, TABLE_SYSTEM, sSystemCaches.get(userHandle)); 437 } 438 439 // Slurp all values (if sane in number & size) into cache. 440 private void fullyPopulateCache(DatabaseHelper dbHelper, String table, SettingsCache cache) { 441 SQLiteDatabase db = dbHelper.getReadableDatabase(); 442 Cursor c = db.query( 443 table, 444 new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE }, 445 null, null, null, null, null, 446 "" + (MAX_CACHE_ENTRIES + 1) /* limit */); 447 try { 448 synchronized (cache) { 449 cache.evictAll(); 450 cache.setFullyMatchesDisk(true); // optimistic 451 int rows = 0; 452 while (c.moveToNext()) { 453 rows++; 454 String name = c.getString(0); 455 String value = c.getString(1); 456 cache.populate(name, value); 457 } 458 if (rows > MAX_CACHE_ENTRIES) { 459 // Somewhat redundant, as removeEldestEntry() will 460 // have already done this, but to be explicit: 461 cache.setFullyMatchesDisk(false); 462 Log.d(TAG, "row count exceeds max cache entries for table " + table); 463 } 464 if (LOCAL_LOGV) Log.d(TAG, "cache for settings table '" + table 465 + "' rows=" + rows + "; fullycached=" + cache.fullyMatchesDisk()); 466 } 467 } finally { 468 c.close(); 469 } 470 } 471 472 private boolean ensureAndroidIdIsSet(int userHandle) { 473 final Cursor c = queryForUser(Settings.Secure.CONTENT_URI, 474 new String[] { Settings.NameValueTable.VALUE }, 475 Settings.NameValueTable.NAME + "=?", 476 new String[] { Settings.Secure.ANDROID_ID }, null, 477 userHandle); 478 try { 479 final String value = c.moveToNext() ? c.getString(0) : null; 480 if (value == null) { 481 // sanity-check the user before touching the db 482 final UserInfo user = mUserManager.getUserInfo(userHandle); 483 if (user == null) { 484 // can happen due to races when deleting users; treat as benign 485 return false; 486 } 487 488 final SecureRandom random = new SecureRandom(); 489 final String newAndroidIdValue = Long.toHexString(random.nextLong()); 490 final ContentValues values = new ContentValues(); 491 values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID); 492 values.put(Settings.NameValueTable.VALUE, newAndroidIdValue); 493 final Uri uri = insertForUser(Settings.Secure.CONTENT_URI, values, userHandle); 494 if (uri == null) { 495 Slog.e(TAG, "Unable to generate new ANDROID_ID for user " + userHandle); 496 return false; 497 } 498 Slog.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue 499 + "] for user " + userHandle); 500 // Write a dropbox entry if it's a restricted profile 501 if (user.isRestricted()) { 502 DropBoxManager dbm = (DropBoxManager) 503 getContext().getSystemService(Context.DROPBOX_SERVICE); 504 if (dbm != null && dbm.isTagEnabled(DROPBOX_TAG_USERLOG)) { 505 dbm.addText(DROPBOX_TAG_USERLOG, System.currentTimeMillis() 506 + ",restricted_profile_ssaid," 507 + newAndroidIdValue + "\n"); 508 } 509 } 510 } 511 return true; 512 } finally { 513 c.close(); 514 } 515 } 516 517 // Lazy-initialize the settings caches for non-primary users 518 private SettingsCache getOrConstructCache(int callingUser, SparseArray<SettingsCache> which) { 519 getOrEstablishDatabase(callingUser); // ignore return value; we don't need it 520 return which.get(callingUser); 521 } 522 523 // Lazy initialize the database helper and caches for this user, if necessary 524 private DatabaseHelper getOrEstablishDatabase(int callingUser) { 525 long oldId = Binder.clearCallingIdentity(); 526 try { 527 DatabaseHelper dbHelper = mOpenHelpers.get(callingUser); 528 if (null == dbHelper) { 529 establishDbTracking(callingUser); 530 dbHelper = mOpenHelpers.get(callingUser); 531 } 532 return dbHelper; 533 } finally { 534 Binder.restoreCallingIdentity(oldId); 535 } 536 } 537 538 public SettingsCache cacheForTable(final int callingUser, String tableName) { 539 if (TABLE_SYSTEM.equals(tableName)) { 540 return getOrConstructCache(callingUser, sSystemCaches); 541 } 542 if (TABLE_SECURE.equals(tableName)) { 543 return getOrConstructCache(callingUser, sSecureCaches); 544 } 545 if (TABLE_GLOBAL.equals(tableName)) { 546 return sGlobalCache; 547 } 548 return null; 549 } 550 551 /** 552 * Used for wiping a whole cache on deletes when we're not 553 * sure what exactly was deleted or changed. 554 */ 555 public void invalidateCache(final int callingUser, String tableName) { 556 SettingsCache cache = cacheForTable(callingUser, tableName); 557 if (cache == null) { 558 return; 559 } 560 synchronized (cache) { 561 cache.evictAll(); 562 cache.mCacheFullyMatchesDisk = false; 563 } 564 } 565 566 /** 567 * Fast path that avoids the use of chatty remoted Cursors. 568 */ 569 @Override 570 public Bundle callFromPackage(String callingPackage, String method, String request, 571 Bundle args) { 572 int callingUser = UserHandle.getCallingUserId(); 573 if (args != null) { 574 int reqUser = args.getInt(Settings.CALL_METHOD_USER_KEY, callingUser); 575 if (reqUser != callingUser) { 576 callingUser = ActivityManager.handleIncomingUser(Binder.getCallingPid(), 577 Binder.getCallingUid(), reqUser, false, true, 578 "get/set setting for user", null); 579 if (LOCAL_LOGV) Slog.v(TAG, " access setting for user " + callingUser); 580 } 581 } 582 583 // Note: we assume that get/put operations for moved-to-global names have already 584 // been directed to the new location on the caller side (otherwise we'd fix them 585 // up here). 586 DatabaseHelper dbHelper; 587 SettingsCache cache; 588 589 // Get methods 590 if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) { 591 if (LOCAL_LOGV) Slog.v(TAG, "call(system:" + request + ") for " + callingUser); 592 dbHelper = getOrEstablishDatabase(callingUser); 593 cache = sSystemCaches.get(callingUser); 594 return lookupValue(dbHelper, TABLE_SYSTEM, cache, request); 595 } 596 if (Settings.CALL_METHOD_GET_SECURE.equals(method)) { 597 if (LOCAL_LOGV) Slog.v(TAG, "call(secure:" + request + ") for " + callingUser); 598 dbHelper = getOrEstablishDatabase(callingUser); 599 cache = sSecureCaches.get(callingUser); 600 return lookupValue(dbHelper, TABLE_SECURE, cache, request); 601 } 602 if (Settings.CALL_METHOD_GET_GLOBAL.equals(method)) { 603 if (LOCAL_LOGV) Slog.v(TAG, "call(global:" + request + ") for " + callingUser); 604 // fast path: owner db & cache are immutable after onCreate() so we need not 605 // guard on the attempt to look them up 606 return lookupValue(getOrEstablishDatabase(UserHandle.USER_OWNER), TABLE_GLOBAL, 607 sGlobalCache, request); 608 } 609 610 // Put methods - new value is in the args bundle under the key named by 611 // the Settings.NameValueTable.VALUE static. 612 final String newValue = (args == null) 613 ? null : args.getString(Settings.NameValueTable.VALUE); 614 615 // Framework can't do automatic permission checking for calls, so we need 616 // to do it here. 617 if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.WRITE_SETTINGS) 618 != PackageManager.PERMISSION_GRANTED) { 619 throw new SecurityException( 620 String.format("Permission denial: writing to settings requires %1$s", 621 android.Manifest.permission.WRITE_SETTINGS)); 622 } 623 624 // Also need to take care of app op. 625 if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SETTINGS, Binder.getCallingUid(), 626 callingPackage) != AppOpsManager.MODE_ALLOWED) { 627 return null; 628 } 629 630 final ContentValues values = new ContentValues(); 631 values.put(Settings.NameValueTable.NAME, request); 632 values.put(Settings.NameValueTable.VALUE, newValue); 633 if (Settings.CALL_METHOD_PUT_SYSTEM.equals(method)) { 634 if (LOCAL_LOGV) Slog.v(TAG, "call_put(system:" + request + "=" + newValue + ") for " + callingUser); 635 insertForUser(Settings.System.CONTENT_URI, values, callingUser); 636 } else if (Settings.CALL_METHOD_PUT_SECURE.equals(method)) { 637 if (LOCAL_LOGV) Slog.v(TAG, "call_put(secure:" + request + "=" + newValue + ") for " + callingUser); 638 insertForUser(Settings.Secure.CONTENT_URI, values, callingUser); 639 } else if (Settings.CALL_METHOD_PUT_GLOBAL.equals(method)) { 640 if (LOCAL_LOGV) Slog.v(TAG, "call_put(global:" + request + "=" + newValue + ") for " + callingUser); 641 insertForUser(Settings.Global.CONTENT_URI, values, callingUser); 642 } else { 643 Slog.w(TAG, "call() with invalid method: " + method); 644 } 645 646 return null; 647 } 648 649 // Looks up value 'key' in 'table' and returns either a single-pair Bundle, 650 // possibly with a null value, or null on failure. 651 private Bundle lookupValue(DatabaseHelper dbHelper, String table, 652 final SettingsCache cache, String key) { 653 if (cache == null) { 654 Slog.e(TAG, "cache is null for user " + UserHandle.getCallingUserId() + " : key=" + key); 655 return null; 656 } 657 synchronized (cache) { 658 Bundle value = cache.get(key); 659 if (value != null) { 660 if (value != TOO_LARGE_TO_CACHE_MARKER) { 661 return value; 662 } 663 // else we fall through and read the value from disk 664 } else if (cache.fullyMatchesDisk()) { 665 // Fast path (very common). Don't even try touch disk 666 // if we know we've slurped it all in. Trying to 667 // touch the disk would mean waiting for yaffs2 to 668 // give us access, which could takes hundreds of 669 // milliseconds. And we're very likely being called 670 // from somebody's UI thread... 671 return NULL_SETTING; 672 } 673 } 674 675 SQLiteDatabase db = dbHelper.getReadableDatabase(); 676 Cursor cursor = null; 677 try { 678 cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key}, 679 null, null, null, null); 680 if (cursor != null && cursor.getCount() == 1) { 681 cursor.moveToFirst(); 682 return cache.putIfAbsent(key, cursor.getString(0)); 683 } 684 } catch (SQLiteException e) { 685 Log.w(TAG, "settings lookup error", e); 686 return null; 687 } finally { 688 if (cursor != null) cursor.close(); 689 } 690 cache.putIfAbsent(key, null); 691 return NULL_SETTING; 692 } 693 694 @Override 695 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) { 696 return queryForUser(url, select, where, whereArgs, sort, UserHandle.getCallingUserId()); 697 } 698 699 private Cursor queryForUser(Uri url, String[] select, String where, String[] whereArgs, 700 String sort, int forUser) { 701 if (LOCAL_LOGV) Slog.v(TAG, "query(" + url + ") for user " + forUser); 702 SqlArguments args = new SqlArguments(url, where, whereArgs); 703 DatabaseHelper dbH; 704 dbH = getOrEstablishDatabase( 705 TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : forUser); 706 SQLiteDatabase db = dbH.getReadableDatabase(); 707 708 // The favorites table was moved from this provider to a provider inside Home 709 // Home still need to query this table to upgrade from pre-cupcake builds 710 // However, a cupcake+ build with no data does not contain this table which will 711 // cause an exception in the SQL stack. The following line is a special case to 712 // let the caller of the query have a chance to recover and avoid the exception 713 if (TABLE_FAVORITES.equals(args.table)) { 714 return null; 715 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 716 args.table = TABLE_FAVORITES; 717 Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null); 718 if (cursor != null) { 719 boolean exists = cursor.getCount() > 0; 720 cursor.close(); 721 if (!exists) return null; 722 } else { 723 return null; 724 } 725 } 726 727 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 728 qb.setTables(args.table); 729 730 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort); 731 // the default Cursor interface does not support per-user observation 732 try { 733 AbstractCursor c = (AbstractCursor) ret; 734 c.setNotificationUri(getContext().getContentResolver(), url, forUser); 735 } catch (ClassCastException e) { 736 // details of the concrete Cursor implementation have changed and this code has 737 // not been updated to match -- complain and fail hard. 738 Log.wtf(TAG, "Incompatible cursor derivation!"); 739 throw e; 740 } 741 return ret; 742 } 743 744 @Override 745 public String getType(Uri url) { 746 // If SqlArguments supplies a where clause, then it must be an item 747 // (because we aren't supplying our own where clause). 748 SqlArguments args = new SqlArguments(url, null, null); 749 if (TextUtils.isEmpty(args.where)) { 750 return "vnd.android.cursor.dir/" + args.table; 751 } else { 752 return "vnd.android.cursor.item/" + args.table; 753 } 754 } 755 756 @Override 757 public int bulkInsert(Uri uri, ContentValues[] values) { 758 final int callingUser = UserHandle.getCallingUserId(); 759 if (LOCAL_LOGV) Slog.v(TAG, "bulkInsert() for user " + callingUser); 760 SqlArguments args = new SqlArguments(uri); 761 if (TABLE_FAVORITES.equals(args.table)) { 762 return 0; 763 } 764 checkWritePermissions(args); 765 SettingsCache cache = cacheForTable(callingUser, args.table); 766 767 final AtomicInteger mutationCount = sKnownMutationsInFlight.get(callingUser); 768 mutationCount.incrementAndGet(); 769 DatabaseHelper dbH = getOrEstablishDatabase( 770 TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : callingUser); 771 SQLiteDatabase db = dbH.getWritableDatabase(); 772 db.beginTransaction(); 773 try { 774 int numValues = values.length; 775 for (int i = 0; i < numValues; i++) { 776 if (db.insert(args.table, null, values[i]) < 0) return 0; 777 SettingsCache.populate(cache, values[i]); 778 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]); 779 } 780 db.setTransactionSuccessful(); 781 } finally { 782 db.endTransaction(); 783 mutationCount.decrementAndGet(); 784 } 785 786 sendNotify(uri, callingUser); 787 return values.length; 788 } 789 790 /* 791 * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED. 792 * This setting contains a list of the currently enabled location providers. 793 * But helper functions in android.providers.Settings can enable or disable 794 * a single provider by using a "+" or "-" prefix before the provider name. 795 * 796 * @returns whether the database needs to be updated or not, also modifying 797 * 'initialValues' if needed. 798 */ 799 private boolean parseProviderList(Uri url, ContentValues initialValues, int desiredUser) { 800 String value = initialValues.getAsString(Settings.Secure.VALUE); 801 String newProviders = null; 802 if (value != null && value.length() > 1) { 803 char prefix = value.charAt(0); 804 if (prefix == '+' || prefix == '-') { 805 // skip prefix 806 value = value.substring(1); 807 808 // read list of enabled providers into "providers" 809 String providers = ""; 810 String[] columns = {Settings.Secure.VALUE}; 811 String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'"; 812 Cursor cursor = queryForUser(url, columns, where, null, null, desiredUser); 813 if (cursor != null && cursor.getCount() == 1) { 814 try { 815 cursor.moveToFirst(); 816 providers = cursor.getString(0); 817 } finally { 818 cursor.close(); 819 } 820 } 821 822 int index = providers.indexOf(value); 823 int end = index + value.length(); 824 // check for commas to avoid matching on partial string 825 if (index > 0 && providers.charAt(index - 1) != ',') index = -1; 826 if (end < providers.length() && providers.charAt(end) != ',') index = -1; 827 828 if (prefix == '+' && index < 0) { 829 // append the provider to the list if not present 830 if (providers.length() == 0) { 831 newProviders = value; 832 } else { 833 newProviders = providers + ',' + value; 834 } 835 } else if (prefix == '-' && index >= 0) { 836 // remove the provider from the list if present 837 // remove leading or trailing comma 838 if (index > 0) { 839 index--; 840 } else if (end < providers.length()) { 841 end++; 842 } 843 844 newProviders = providers.substring(0, index); 845 if (end < providers.length()) { 846 newProviders += providers.substring(end); 847 } 848 } else { 849 // nothing changed, so no need to update the database 850 return false; 851 } 852 853 if (newProviders != null) { 854 initialValues.put(Settings.Secure.VALUE, newProviders); 855 } 856 } 857 } 858 859 return true; 860 } 861 862 @Override 863 public Uri insert(Uri url, ContentValues initialValues) { 864 return insertForUser(url, initialValues, UserHandle.getCallingUserId()); 865 } 866 867 // Settings.put*ForUser() always winds up here, so this is where we apply 868 // policy around permission to write settings for other users. 869 private Uri insertForUser(Uri url, ContentValues initialValues, int desiredUserHandle) { 870 final int callingUser = UserHandle.getCallingUserId(); 871 if (callingUser != desiredUserHandle) { 872 getContext().enforceCallingOrSelfPermission( 873 android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, 874 "Not permitted to access settings for other users"); 875 } 876 877 if (LOCAL_LOGV) Slog.v(TAG, "insert(" + url + ") for user " + desiredUserHandle 878 + " by " + callingUser); 879 880 SqlArguments args = new SqlArguments(url); 881 if (TABLE_FAVORITES.equals(args.table)) { 882 return null; 883 } 884 885 // Special case LOCATION_PROVIDERS_ALLOWED. 886 // Support enabling/disabling a single provider (using "+" or "-" prefix) 887 String name = initialValues.getAsString(Settings.Secure.NAME); 888 if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) { 889 if (!parseProviderList(url, initialValues, desiredUserHandle)) return null; 890 } 891 892 // If this is an insert() of a key that has been migrated to the global store, 893 // redirect the operation to that store 894 if (name != null) { 895 if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) { 896 if (!TABLE_GLOBAL.equals(args.table)) { 897 if (LOCAL_LOGV) Slog.i(TAG, "Rewrite of insert() of now-global key " + name); 898 } 899 args.table = TABLE_GLOBAL; // next condition will rewrite the user handle 900 } 901 } 902 903 // Check write permissions only after determining which table the insert will touch 904 checkWritePermissions(args); 905 906 // The global table is stored under the owner, always 907 if (TABLE_GLOBAL.equals(args.table)) { 908 desiredUserHandle = UserHandle.USER_OWNER; 909 } 910 911 SettingsCache cache = cacheForTable(desiredUserHandle, args.table); 912 String value = initialValues.getAsString(Settings.NameValueTable.VALUE); 913 if (SettingsCache.isRedundantSetValue(cache, name, value)) { 914 return Uri.withAppendedPath(url, name); 915 } 916 917 final AtomicInteger mutationCount = sKnownMutationsInFlight.get(desiredUserHandle); 918 mutationCount.incrementAndGet(); 919 DatabaseHelper dbH = getOrEstablishDatabase(desiredUserHandle); 920 SQLiteDatabase db = dbH.getWritableDatabase(); 921 final long rowId = db.insert(args.table, null, initialValues); 922 mutationCount.decrementAndGet(); 923 if (rowId <= 0) return null; 924 925 SettingsCache.populate(cache, initialValues); // before we notify 926 927 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues 928 + " for user " + desiredUserHandle); 929 // Note that we use the original url here, not the potentially-rewritten table name 930 url = getUriFor(url, initialValues, rowId); 931 sendNotify(url, desiredUserHandle); 932 return url; 933 } 934 935 @Override 936 public int delete(Uri url, String where, String[] whereArgs) { 937 int callingUser = UserHandle.getCallingUserId(); 938 if (LOCAL_LOGV) Slog.v(TAG, "delete() for user " + callingUser); 939 SqlArguments args = new SqlArguments(url, where, whereArgs); 940 if (TABLE_FAVORITES.equals(args.table)) { 941 return 0; 942 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 943 args.table = TABLE_FAVORITES; 944 } else if (TABLE_GLOBAL.equals(args.table)) { 945 callingUser = UserHandle.USER_OWNER; 946 } 947 checkWritePermissions(args); 948 949 final AtomicInteger mutationCount = sKnownMutationsInFlight.get(callingUser); 950 mutationCount.incrementAndGet(); 951 DatabaseHelper dbH = getOrEstablishDatabase(callingUser); 952 SQLiteDatabase db = dbH.getWritableDatabase(); 953 int count = db.delete(args.table, args.where, args.args); 954 mutationCount.decrementAndGet(); 955 if (count > 0) { 956 invalidateCache(callingUser, args.table); // before we notify 957 sendNotify(url, callingUser); 958 } 959 startAsyncCachePopulation(callingUser); 960 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted"); 961 return count; 962 } 963 964 @Override 965 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) { 966 // NOTE: update() is never called by the front-end Settings API, and updates that 967 // wind up affecting rows in Secure that are globally shared will not have the 968 // intended effect (the update will be invisible to the rest of the system). 969 // This should have no practical effect, since writes to the Secure db can only 970 // be done by system code, and that code should be using the correct API up front. 971 int callingUser = UserHandle.getCallingUserId(); 972 if (LOCAL_LOGV) Slog.v(TAG, "update() for user " + callingUser); 973 SqlArguments args = new SqlArguments(url, where, whereArgs); 974 if (TABLE_FAVORITES.equals(args.table)) { 975 return 0; 976 } else if (TABLE_GLOBAL.equals(args.table)) { 977 callingUser = UserHandle.USER_OWNER; 978 } 979 checkWritePermissions(args); 980 981 final AtomicInteger mutationCount = sKnownMutationsInFlight.get(callingUser); 982 mutationCount.incrementAndGet(); 983 DatabaseHelper dbH = getOrEstablishDatabase(callingUser); 984 SQLiteDatabase db = dbH.getWritableDatabase(); 985 int count = db.update(args.table, initialValues, args.where, args.args); 986 mutationCount.decrementAndGet(); 987 if (count > 0) { 988 invalidateCache(callingUser, args.table); // before we notify 989 sendNotify(url, callingUser); 990 } 991 startAsyncCachePopulation(callingUser); 992 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues); 993 return count; 994 } 995 996 @Override 997 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 998 999 /* 1000 * When a client attempts to openFile the default ringtone or 1001 * notification setting Uri, we will proxy the call to the current 1002 * default ringtone's Uri (if it is in the media provider). 1003 */ 1004 int ringtoneType = RingtoneManager.getDefaultType(uri); 1005 // Above call returns -1 if the Uri doesn't match a default type 1006 if (ringtoneType != -1) { 1007 Context context = getContext(); 1008 1009 // Get the current value for the default sound 1010 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 1011 1012 if (soundUri != null) { 1013 // Proxy the openFile call to media provider 1014 String authority = soundUri.getAuthority(); 1015 if (authority.equals(MediaStore.AUTHORITY)) { 1016 return context.getContentResolver().openFileDescriptor(soundUri, mode); 1017 } 1018 } 1019 } 1020 1021 return super.openFile(uri, mode); 1022 } 1023 1024 @Override 1025 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 1026 1027 /* 1028 * When a client attempts to openFile the default ringtone or 1029 * notification setting Uri, we will proxy the call to the current 1030 * default ringtone's Uri (if it is in the media provider). 1031 */ 1032 int ringtoneType = RingtoneManager.getDefaultType(uri); 1033 // Above call returns -1 if the Uri doesn't match a default type 1034 if (ringtoneType != -1) { 1035 Context context = getContext(); 1036 1037 // Get the current value for the default sound 1038 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 1039 1040 if (soundUri != null) { 1041 // Proxy the openFile call to media provider 1042 String authority = soundUri.getAuthority(); 1043 if (authority.equals(MediaStore.AUTHORITY)) { 1044 ParcelFileDescriptor pfd = null; 1045 try { 1046 pfd = context.getContentResolver().openFileDescriptor(soundUri, mode); 1047 return new AssetFileDescriptor(pfd, 0, -1); 1048 } catch (FileNotFoundException ex) { 1049 // fall through and open the fallback ringtone below 1050 } 1051 } 1052 1053 try { 1054 return super.openAssetFile(soundUri, mode); 1055 } catch (FileNotFoundException ex) { 1056 // Since a non-null Uri was specified, but couldn't be opened, 1057 // fall back to the built-in ringtone. 1058 return context.getResources().openRawResourceFd( 1059 com.android.internal.R.raw.fallbackring); 1060 } 1061 } 1062 // no need to fall through and have openFile() try again, since we 1063 // already know that will fail. 1064 throw new FileNotFoundException(); // or return null ? 1065 } 1066 1067 // Note that this will end up calling openFile() above. 1068 return super.openAssetFile(uri, mode); 1069 } 1070 1071 /** 1072 * In-memory LRU Cache of system and secure settings, along with 1073 * associated helper functions to keep cache coherent with the 1074 * database. 1075 */ 1076 private static final class SettingsCache extends LruCache<String, Bundle> { 1077 1078 private final String mCacheName; 1079 private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped. 1080 1081 public SettingsCache(String name) { 1082 super(MAX_CACHE_ENTRIES); 1083 mCacheName = name; 1084 } 1085 1086 /** 1087 * Is the whole database table slurped into this cache? 1088 */ 1089 public boolean fullyMatchesDisk() { 1090 synchronized (this) { 1091 return mCacheFullyMatchesDisk; 1092 } 1093 } 1094 1095 public void setFullyMatchesDisk(boolean value) { 1096 synchronized (this) { 1097 mCacheFullyMatchesDisk = value; 1098 } 1099 } 1100 1101 @Override 1102 protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) { 1103 if (evicted) { 1104 mCacheFullyMatchesDisk = false; 1105 } 1106 } 1107 1108 /** 1109 * Atomic cache population, conditional on size of value and if 1110 * we lost a race. 1111 * 1112 * @returns a Bundle to send back to the client from call(), even 1113 * if we lost the race. 1114 */ 1115 public Bundle putIfAbsent(String key, String value) { 1116 Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value); 1117 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) { 1118 synchronized (this) { 1119 if (get(key) == null) { 1120 put(key, bundle); 1121 } 1122 } 1123 } 1124 return bundle; 1125 } 1126 1127 /** 1128 * Populates a key in a given (possibly-null) cache. 1129 */ 1130 public static void populate(SettingsCache cache, ContentValues contentValues) { 1131 if (cache == null) { 1132 return; 1133 } 1134 String name = contentValues.getAsString(Settings.NameValueTable.NAME); 1135 if (name == null) { 1136 Log.w(TAG, "null name populating settings cache."); 1137 return; 1138 } 1139 String value = contentValues.getAsString(Settings.NameValueTable.VALUE); 1140 cache.populate(name, value); 1141 } 1142 1143 public void populate(String name, String value) { 1144 synchronized (this) { 1145 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) { 1146 put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value)); 1147 } else { 1148 put(name, TOO_LARGE_TO_CACHE_MARKER); 1149 } 1150 } 1151 } 1152 1153 /** 1154 * For suppressing duplicate/redundant settings inserts early, 1155 * checking our cache first (but without faulting it in), 1156 * before going to sqlite with the mutation. 1157 */ 1158 public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) { 1159 if (cache == null) return false; 1160 synchronized (cache) { 1161 Bundle bundle = cache.get(name); 1162 if (bundle == null) return false; 1163 String oldValue = bundle.getPairValue(); 1164 if (oldValue == null && value == null) return true; 1165 if ((oldValue == null) != (value == null)) return false; 1166 return oldValue.equals(value); 1167 } 1168 } 1169 } 1170} 1171