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