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