SettingsProvider.java revision 47847f3f4dcf2a0dbea0bc0e4f02528e21d37a88
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.concurrent.atomic.AtomicBoolean; 22import java.util.concurrent.atomic.AtomicInteger; 23 24import android.app.backup.BackupManager; 25import android.content.ContentProvider; 26import android.content.ContentUris; 27import android.content.ContentValues; 28import android.content.Context; 29import android.content.pm.PackageManager; 30import android.content.res.AssetFileDescriptor; 31import android.database.Cursor; 32import android.database.sqlite.SQLiteDatabase; 33import android.database.sqlite.SQLiteException; 34import android.database.sqlite.SQLiteQueryBuilder; 35import android.media.RingtoneManager; 36import android.net.Uri; 37import android.os.Bundle; 38import android.os.FileObserver; 39import android.os.ParcelFileDescriptor; 40import android.os.SystemProperties; 41import android.provider.DrmStore; 42import android.provider.MediaStore; 43import android.provider.Settings; 44import android.text.TextUtils; 45import android.util.Log; 46import android.util.LruCache; 47 48public class SettingsProvider extends ContentProvider { 49 private static final String TAG = "SettingsProvider"; 50 private static final boolean LOCAL_LOGV = false; 51 52 private static final String TABLE_FAVORITES = "favorites"; 53 private static final String TABLE_OLD_FAVORITES = "old_favorites"; 54 55 private static final String[] COLUMN_VALUE = new String[] { "value" }; 56 57 // Cache for settings, access-ordered for acting as LRU. 58 // Guarded by themselves. 59 private static final int MAX_CACHE_ENTRIES = 200; 60 private static final SettingsCache sSystemCache = new SettingsCache("system"); 61 private static final SettingsCache sSecureCache = new SettingsCache("secure"); 62 63 // The count of how many known (handled by SettingsProvider) 64 // database mutations are currently being handled. Used by 65 // sFileObserver to not reload the database when it's ourselves 66 // modifying it. 67 private static final AtomicInteger sKnownMutationsInFlight = new AtomicInteger(0); 68 69 // Over this size we don't reject loading or saving settings but 70 // we do consider them broken/malicious and don't keep them in 71 // memory at least: 72 private static final int MAX_CACHE_ENTRY_SIZE = 500; 73 74 private static final Bundle NULL_SETTING = Bundle.forPair("value", null); 75 76 // Used as a sentinel value in an instance equality test when we 77 // want to cache the existence of a key, but not store its value. 78 private static final Bundle TOO_LARGE_TO_CACHE_MARKER = Bundle.forPair("_dummy", null); 79 80 protected DatabaseHelper mOpenHelper; 81 private BackupManager mBackupManager; 82 83 /** 84 * Decode a content URL into the table, projection, and arguments 85 * used to access the corresponding database rows. 86 */ 87 private static class SqlArguments { 88 public String table; 89 public final String where; 90 public final String[] args; 91 92 /** Operate on existing rows. */ 93 SqlArguments(Uri url, String where, String[] args) { 94 if (url.getPathSegments().size() == 1) { 95 this.table = url.getPathSegments().get(0); 96 if (!DatabaseHelper.isValidTable(this.table)) { 97 throw new IllegalArgumentException("Bad root path: " + this.table); 98 } 99 this.where = where; 100 this.args = args; 101 } else if (url.getPathSegments().size() != 2) { 102 throw new IllegalArgumentException("Invalid URI: " + url); 103 } else if (!TextUtils.isEmpty(where)) { 104 throw new UnsupportedOperationException("WHERE clause not supported: " + url); 105 } else { 106 this.table = url.getPathSegments().get(0); 107 if (!DatabaseHelper.isValidTable(this.table)) { 108 throw new IllegalArgumentException("Bad root path: " + this.table); 109 } 110 if ("system".equals(this.table) || "secure".equals(this.table)) { 111 this.where = Settings.NameValueTable.NAME + "=?"; 112 this.args = new String[] { url.getPathSegments().get(1) }; 113 } else { 114 this.where = "_id=" + ContentUris.parseId(url); 115 this.args = null; 116 } 117 } 118 } 119 120 /** Insert new rows (no where clause allowed). */ 121 SqlArguments(Uri url) { 122 if (url.getPathSegments().size() == 1) { 123 this.table = url.getPathSegments().get(0); 124 if (!DatabaseHelper.isValidTable(this.table)) { 125 throw new IllegalArgumentException("Bad root path: " + this.table); 126 } 127 this.where = null; 128 this.args = null; 129 } else { 130 throw new IllegalArgumentException("Invalid URI: " + url); 131 } 132 } 133 } 134 135 /** 136 * Get the content URI of a row added to a table. 137 * @param tableUri of the entire table 138 * @param values found in the row 139 * @param rowId of the row 140 * @return the content URI for this particular row 141 */ 142 private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) { 143 if (tableUri.getPathSegments().size() != 1) { 144 throw new IllegalArgumentException("Invalid URI: " + tableUri); 145 } 146 String table = tableUri.getPathSegments().get(0); 147 if ("system".equals(table) || "secure".equals(table)) { 148 String name = values.getAsString(Settings.NameValueTable.NAME); 149 return Uri.withAppendedPath(tableUri, name); 150 } else { 151 return ContentUris.withAppendedId(tableUri, rowId); 152 } 153 } 154 155 /** 156 * Send a notification when a particular content URI changes. 157 * Modify the system property used to communicate the version of 158 * this table, for tables which have such a property. (The Settings 159 * contract class uses these to provide client-side caches.) 160 * @param uri to send notifications for 161 */ 162 private void sendNotify(Uri uri) { 163 // Update the system property *first*, so if someone is listening for 164 // a notification and then using the contract class to get their data, 165 // the system property will be updated and they'll get the new data. 166 167 boolean backedUpDataChanged = false; 168 String property = null, table = uri.getPathSegments().get(0); 169 if (table.equals("system")) { 170 property = Settings.System.SYS_PROP_SETTING_VERSION; 171 backedUpDataChanged = true; 172 } else if (table.equals("secure")) { 173 property = Settings.Secure.SYS_PROP_SETTING_VERSION; 174 backedUpDataChanged = true; 175 } 176 177 if (property != null) { 178 long version = SystemProperties.getLong(property, 0) + 1; 179 if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version); 180 SystemProperties.set(property, Long.toString(version)); 181 } 182 183 // Inform the backup manager about a data change 184 if (backedUpDataChanged) { 185 mBackupManager.dataChanged(); 186 } 187 // Now send the notification through the content framework. 188 189 String notify = uri.getQueryParameter("notify"); 190 if (notify == null || "true".equals(notify)) { 191 getContext().getContentResolver().notifyChange(uri, null); 192 if (LOCAL_LOGV) Log.v(TAG, "notifying: " + uri); 193 } else { 194 if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri); 195 } 196 } 197 198 /** 199 * Make sure the caller has permission to write this data. 200 * @param args supplied by the caller 201 * @throws SecurityException if the caller is forbidden to write. 202 */ 203 private void checkWritePermissions(SqlArguments args) { 204 if ("secure".equals(args.table) && 205 getContext().checkCallingOrSelfPermission( 206 android.Manifest.permission.WRITE_SECURE_SETTINGS) != 207 PackageManager.PERMISSION_GRANTED) { 208 throw new SecurityException( 209 String.format("Permission denial: writing to secure settings requires %1$s", 210 android.Manifest.permission.WRITE_SECURE_SETTINGS)); 211 } 212 } 213 214 // FileObserver for external modifications to the database file. 215 // Note that this is for platform developers only with 216 // userdebug/eng builds who should be able to tinker with the 217 // sqlite database out from under the SettingsProvider, which is 218 // normally the exclusive owner of the database. But we keep this 219 // enabled all the time to minimize development-vs-user 220 // differences in testing. 221 private static SettingsFileObserver sObserverInstance; 222 private class SettingsFileObserver extends FileObserver { 223 private final AtomicBoolean mIsDirty = new AtomicBoolean(false); 224 private final String mPath; 225 226 public SettingsFileObserver(String path) { 227 super(path, FileObserver.CLOSE_WRITE | 228 FileObserver.CREATE | FileObserver.DELETE | 229 FileObserver.MOVED_TO | FileObserver.MODIFY); 230 mPath = path; 231 } 232 233 public void onEvent(int event, String path) { 234 int modsInFlight = sKnownMutationsInFlight.get(); 235 if (modsInFlight > 0) { 236 // our own modification. 237 return; 238 } 239 Log.d(TAG, "external modification to " + mPath + "; event=" + event); 240 if (!mIsDirty.compareAndSet(false, true)) { 241 // already handled. (we get a few update events 242 // during an sqlite write) 243 return; 244 } 245 Log.d(TAG, "updating our caches for " + mPath); 246 fullyPopulateCaches(); 247 mIsDirty.set(false); 248 } 249 } 250 251 @Override 252 public boolean onCreate() { 253 mOpenHelper = new DatabaseHelper(getContext()); 254 mBackupManager = new BackupManager(getContext()); 255 256 if (!ensureAndroidIdIsSet()) { 257 return false; 258 } 259 260 // Watch for external modifications to the database file, 261 // keeping our cache in sync. 262 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 263 sObserverInstance = new SettingsFileObserver(db.getPath()); 264 sObserverInstance.startWatching(); 265 startAsyncCachePopulation(); 266 return true; 267 } 268 269 private void startAsyncCachePopulation() { 270 new Thread("populate-settings-caches") { 271 public void run() { 272 fullyPopulateCaches(); 273 } 274 }.start(); 275 } 276 277 private void fullyPopulateCaches() { 278 fullyPopulateCache("secure", sSecureCache); 279 fullyPopulateCache("system", sSystemCache); 280 } 281 282 // Slurp all values (if sane in number & size) into cache. 283 private void fullyPopulateCache(String table, SettingsCache cache) { 284 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 285 Cursor c = db.query( 286 table, 287 new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE }, 288 null, null, null, null, null, 289 "" + (MAX_CACHE_ENTRIES + 1) /* limit */); 290 try { 291 synchronized (cache) { 292 cache.evictAll(); 293 cache.setFullyMatchesDisk(true); // optimistic 294 int rows = 0; 295 while (c.moveToNext()) { 296 rows++; 297 String name = c.getString(0); 298 String value = c.getString(1); 299 cache.populate(name, value); 300 } 301 if (rows > MAX_CACHE_ENTRIES) { 302 // Somewhat redundant, as removeEldestEntry() will 303 // have already done this, but to be explicit: 304 cache.setFullyMatchesDisk(false); 305 Log.d(TAG, "row count exceeds max cache entries for table " + table); 306 } 307 Log.d(TAG, "cache for settings table '" + table + "' rows=" + rows + "; fullycached=" + 308 cache.fullyMatchesDisk()); 309 } 310 } finally { 311 c.close(); 312 } 313 } 314 315 private boolean ensureAndroidIdIsSet() { 316 final Cursor c = query(Settings.Secure.CONTENT_URI, 317 new String[] { Settings.NameValueTable.VALUE }, 318 Settings.NameValueTable.NAME + "=?", 319 new String[] { Settings.Secure.ANDROID_ID }, null); 320 try { 321 final String value = c.moveToNext() ? c.getString(0) : null; 322 if (value == null) { 323 final SecureRandom random = new SecureRandom(); 324 final String newAndroidIdValue = Long.toHexString(random.nextLong()); 325 Log.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue + "]"); 326 final ContentValues values = new ContentValues(); 327 values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID); 328 values.put(Settings.NameValueTable.VALUE, newAndroidIdValue); 329 final Uri uri = insert(Settings.Secure.CONTENT_URI, values); 330 if (uri == null) { 331 return false; 332 } 333 } 334 return true; 335 } finally { 336 c.close(); 337 } 338 } 339 340 /** 341 * Fast path that avoids the use of chatty remoted Cursors. 342 */ 343 @Override 344 public Bundle call(String method, String request, Bundle args) { 345 if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) { 346 return lookupValue("system", sSystemCache, request); 347 } 348 if (Settings.CALL_METHOD_GET_SECURE.equals(method)) { 349 return lookupValue("secure", sSecureCache, request); 350 } 351 return null; 352 } 353 354 // Looks up value 'key' in 'table' and returns either a single-pair Bundle, 355 // possibly with a null value, or null on failure. 356 private Bundle lookupValue(String table, SettingsCache cache, String key) { 357 synchronized (cache) { 358 Bundle value = cache.get(key); 359 if (value != null) { 360 if (value != TOO_LARGE_TO_CACHE_MARKER) { 361 return value; 362 } 363 // else we fall through and read the value from disk 364 } else if (cache.fullyMatchesDisk()) { 365 // Fast path (very common). Don't even try touch disk 366 // if we know we've slurped it all in. Trying to 367 // touch the disk would mean waiting for yaffs2 to 368 // give us access, which could takes hundreds of 369 // milliseconds. And we're very likely being called 370 // from somebody's UI thread... 371 return NULL_SETTING; 372 } 373 } 374 375 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 376 Cursor cursor = null; 377 try { 378 cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key}, 379 null, null, null, null); 380 if (cursor != null && cursor.getCount() == 1) { 381 cursor.moveToFirst(); 382 return cache.putIfAbsent(key, cursor.getString(0)); 383 } 384 } catch (SQLiteException e) { 385 Log.w(TAG, "settings lookup error", e); 386 return null; 387 } finally { 388 if (cursor != null) cursor.close(); 389 } 390 cache.putIfAbsent(key, null); 391 return NULL_SETTING; 392 } 393 394 @Override 395 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) { 396 SqlArguments args = new SqlArguments(url, where, whereArgs); 397 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 398 399 // The favorites table was moved from this provider to a provider inside Home 400 // Home still need to query this table to upgrade from pre-cupcake builds 401 // However, a cupcake+ build with no data does not contain this table which will 402 // cause an exception in the SQL stack. The following line is a special case to 403 // let the caller of the query have a chance to recover and avoid the exception 404 if (TABLE_FAVORITES.equals(args.table)) { 405 return null; 406 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 407 args.table = TABLE_FAVORITES; 408 Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null); 409 if (cursor != null) { 410 boolean exists = cursor.getCount() > 0; 411 cursor.close(); 412 if (!exists) return null; 413 } else { 414 return null; 415 } 416 } 417 418 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 419 qb.setTables(args.table); 420 421 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort); 422 ret.setNotificationUri(getContext().getContentResolver(), url); 423 return ret; 424 } 425 426 @Override 427 public String getType(Uri url) { 428 // If SqlArguments supplies a where clause, then it must be an item 429 // (because we aren't supplying our own where clause). 430 SqlArguments args = new SqlArguments(url, null, null); 431 if (TextUtils.isEmpty(args.where)) { 432 return "vnd.android.cursor.dir/" + args.table; 433 } else { 434 return "vnd.android.cursor.item/" + args.table; 435 } 436 } 437 438 @Override 439 public int bulkInsert(Uri uri, ContentValues[] values) { 440 SqlArguments args = new SqlArguments(uri); 441 if (TABLE_FAVORITES.equals(args.table)) { 442 return 0; 443 } 444 checkWritePermissions(args); 445 SettingsCache cache = SettingsCache.forTable(args.table); 446 447 sKnownMutationsInFlight.incrementAndGet(); 448 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 449 db.beginTransaction(); 450 try { 451 int numValues = values.length; 452 for (int i = 0; i < numValues; i++) { 453 if (db.insert(args.table, null, values[i]) < 0) return 0; 454 SettingsCache.populate(cache, values[i]); 455 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]); 456 } 457 db.setTransactionSuccessful(); 458 } finally { 459 db.endTransaction(); 460 sKnownMutationsInFlight.decrementAndGet(); 461 } 462 463 sendNotify(uri); 464 return values.length; 465 } 466 467 /* 468 * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED. 469 * This setting contains a list of the currently enabled location providers. 470 * But helper functions in android.providers.Settings can enable or disable 471 * a single provider by using a "+" or "-" prefix before the provider name. 472 * 473 * @returns whether the database needs to be updated or not, also modifying 474 * 'initialValues' if needed. 475 */ 476 private boolean parseProviderList(Uri url, ContentValues initialValues) { 477 String value = initialValues.getAsString(Settings.Secure.VALUE); 478 String newProviders = null; 479 if (value != null && value.length() > 1) { 480 char prefix = value.charAt(0); 481 if (prefix == '+' || prefix == '-') { 482 // skip prefix 483 value = value.substring(1); 484 485 // read list of enabled providers into "providers" 486 String providers = ""; 487 String[] columns = {Settings.Secure.VALUE}; 488 String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'"; 489 Cursor cursor = query(url, columns, where, null, null); 490 if (cursor != null && cursor.getCount() == 1) { 491 try { 492 cursor.moveToFirst(); 493 providers = cursor.getString(0); 494 } finally { 495 cursor.close(); 496 } 497 } 498 499 int index = providers.indexOf(value); 500 int end = index + value.length(); 501 // check for commas to avoid matching on partial string 502 if (index > 0 && providers.charAt(index - 1) != ',') index = -1; 503 if (end < providers.length() && providers.charAt(end) != ',') index = -1; 504 505 if (prefix == '+' && index < 0) { 506 // append the provider to the list if not present 507 if (providers.length() == 0) { 508 newProviders = value; 509 } else { 510 newProviders = providers + ',' + value; 511 } 512 } else if (prefix == '-' && index >= 0) { 513 // remove the provider from the list if present 514 // remove leading or trailing comma 515 if (index > 0) { 516 index--; 517 } else if (end < providers.length()) { 518 end++; 519 } 520 521 newProviders = providers.substring(0, index); 522 if (end < providers.length()) { 523 newProviders += providers.substring(end); 524 } 525 } else { 526 // nothing changed, so no need to update the database 527 return false; 528 } 529 530 if (newProviders != null) { 531 initialValues.put(Settings.Secure.VALUE, newProviders); 532 } 533 } 534 } 535 536 return true; 537 } 538 539 @Override 540 public Uri insert(Uri url, ContentValues initialValues) { 541 SqlArguments args = new SqlArguments(url); 542 if (TABLE_FAVORITES.equals(args.table)) { 543 return null; 544 } 545 checkWritePermissions(args); 546 547 // Special case LOCATION_PROVIDERS_ALLOWED. 548 // Support enabling/disabling a single provider (using "+" or "-" prefix) 549 String name = initialValues.getAsString(Settings.Secure.NAME); 550 if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) { 551 if (!parseProviderList(url, initialValues)) return null; 552 } 553 554 SettingsCache cache = SettingsCache.forTable(args.table); 555 String value = initialValues.getAsString(Settings.NameValueTable.VALUE); 556 if (SettingsCache.isRedundantSetValue(cache, name, value)) { 557 return Uri.withAppendedPath(url, name); 558 } 559 560 sKnownMutationsInFlight.incrementAndGet(); 561 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 562 final long rowId = db.insert(args.table, null, initialValues); 563 sKnownMutationsInFlight.decrementAndGet(); 564 if (rowId <= 0) return null; 565 566 SettingsCache.populate(cache, initialValues); // before we notify 567 568 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues); 569 url = getUriFor(url, initialValues, rowId); 570 sendNotify(url); 571 return url; 572 } 573 574 @Override 575 public int delete(Uri url, String where, String[] whereArgs) { 576 SqlArguments args = new SqlArguments(url, where, whereArgs); 577 if (TABLE_FAVORITES.equals(args.table)) { 578 return 0; 579 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 580 args.table = TABLE_FAVORITES; 581 } 582 checkWritePermissions(args); 583 584 sKnownMutationsInFlight.incrementAndGet(); 585 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 586 int count = db.delete(args.table, args.where, args.args); 587 sKnownMutationsInFlight.decrementAndGet(); 588 if (count > 0) { 589 SettingsCache.invalidate(args.table); // before we notify 590 sendNotify(url); 591 } 592 startAsyncCachePopulation(); 593 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted"); 594 return count; 595 } 596 597 @Override 598 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) { 599 SqlArguments args = new SqlArguments(url, where, whereArgs); 600 if (TABLE_FAVORITES.equals(args.table)) { 601 return 0; 602 } 603 checkWritePermissions(args); 604 605 sKnownMutationsInFlight.incrementAndGet(); 606 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 607 int count = db.update(args.table, initialValues, args.where, args.args); 608 sKnownMutationsInFlight.decrementAndGet(); 609 if (count > 0) { 610 SettingsCache.invalidate(args.table); // before we notify 611 sendNotify(url); 612 } 613 startAsyncCachePopulation(); 614 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues); 615 return count; 616 } 617 618 @Override 619 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 620 621 /* 622 * When a client attempts to openFile the default ringtone or 623 * notification setting Uri, we will proxy the call to the current 624 * default ringtone's Uri (if it is in the DRM or media provider). 625 */ 626 int ringtoneType = RingtoneManager.getDefaultType(uri); 627 // Above call returns -1 if the Uri doesn't match a default type 628 if (ringtoneType != -1) { 629 Context context = getContext(); 630 631 // Get the current value for the default sound 632 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 633 634 if (soundUri != null) { 635 // Only proxy the openFile call to drm or media providers 636 String authority = soundUri.getAuthority(); 637 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY); 638 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) { 639 640 if (isDrmAuthority) { 641 try { 642 // Check DRM access permission here, since once we 643 // do the below call the DRM will be checking our 644 // permission, not our caller's permission 645 DrmStore.enforceAccessDrmPermission(context); 646 } catch (SecurityException e) { 647 throw new FileNotFoundException(e.getMessage()); 648 } 649 } 650 651 return context.getContentResolver().openFileDescriptor(soundUri, mode); 652 } 653 } 654 } 655 656 return super.openFile(uri, mode); 657 } 658 659 @Override 660 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 661 662 /* 663 * When a client attempts to openFile the default ringtone or 664 * notification setting Uri, we will proxy the call to the current 665 * default ringtone's Uri (if it is in the DRM or media provider). 666 */ 667 int ringtoneType = RingtoneManager.getDefaultType(uri); 668 // Above call returns -1 if the Uri doesn't match a default type 669 if (ringtoneType != -1) { 670 Context context = getContext(); 671 672 // Get the current value for the default sound 673 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 674 675 if (soundUri != null) { 676 // Only proxy the openFile call to drm or media providers 677 String authority = soundUri.getAuthority(); 678 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY); 679 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) { 680 681 if (isDrmAuthority) { 682 try { 683 // Check DRM access permission here, since once we 684 // do the below call the DRM will be checking our 685 // permission, not our caller's permission 686 DrmStore.enforceAccessDrmPermission(context); 687 } catch (SecurityException e) { 688 throw new FileNotFoundException(e.getMessage()); 689 } 690 } 691 692 ParcelFileDescriptor pfd = null; 693 try { 694 pfd = context.getContentResolver().openFileDescriptor(soundUri, mode); 695 return new AssetFileDescriptor(pfd, 0, -1); 696 } catch (FileNotFoundException ex) { 697 // fall through and open the fallback ringtone below 698 } 699 } 700 701 try { 702 return super.openAssetFile(soundUri, mode); 703 } catch (FileNotFoundException ex) { 704 // Since a non-null Uri was specified, but couldn't be opened, 705 // fall back to the built-in ringtone. 706 return context.getResources().openRawResourceFd( 707 com.android.internal.R.raw.fallbackring); 708 } 709 } 710 // no need to fall through and have openFile() try again, since we 711 // already know that will fail. 712 throw new FileNotFoundException(); // or return null ? 713 } 714 715 // Note that this will end up calling openFile() above. 716 return super.openAssetFile(uri, mode); 717 } 718 719 /** 720 * In-memory LRU Cache of system and secure settings, along with 721 * associated helper functions to keep cache coherent with the 722 * database. 723 */ 724 private static final class SettingsCache extends LruCache<String, Bundle> { 725 726 private final String mCacheName; 727 private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped. 728 729 public SettingsCache(String name) { 730 super(MAX_CACHE_ENTRIES); 731 mCacheName = name; 732 } 733 734 /** 735 * Is the whole database table slurped into this cache? 736 */ 737 public boolean fullyMatchesDisk() { 738 synchronized (this) { 739 return mCacheFullyMatchesDisk; 740 } 741 } 742 743 public void setFullyMatchesDisk(boolean value) { 744 synchronized (this) { 745 mCacheFullyMatchesDisk = value; 746 } 747 } 748 749 @Override 750 protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) { 751 if (evicted) { 752 mCacheFullyMatchesDisk = false; 753 } 754 } 755 756 /** 757 * Atomic cache population, conditional on size of value and if 758 * we lost a race. 759 * 760 * @returns a Bundle to send back to the client from call(), even 761 * if we lost the race. 762 */ 763 public Bundle putIfAbsent(String key, String value) { 764 Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value); 765 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) { 766 synchronized (this) { 767 if (get(key) == null) { 768 put(key, bundle); 769 } 770 } 771 } 772 return bundle; 773 } 774 775 public static SettingsCache forTable(String tableName) { 776 if ("system".equals(tableName)) { 777 return SettingsProvider.sSystemCache; 778 } 779 if ("secure".equals(tableName)) { 780 return SettingsProvider.sSecureCache; 781 } 782 return null; 783 } 784 785 /** 786 * Populates a key in a given (possibly-null) cache. 787 */ 788 public static void populate(SettingsCache cache, ContentValues contentValues) { 789 if (cache == null) { 790 return; 791 } 792 String name = contentValues.getAsString(Settings.NameValueTable.NAME); 793 if (name == null) { 794 Log.w(TAG, "null name populating settings cache."); 795 return; 796 } 797 String value = contentValues.getAsString(Settings.NameValueTable.VALUE); 798 cache.populate(name, value); 799 } 800 801 public void populate(String name, String value) { 802 synchronized (this) { 803 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) { 804 put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value)); 805 } else { 806 put(name, TOO_LARGE_TO_CACHE_MARKER); 807 } 808 } 809 } 810 811 /** 812 * Used for wiping a whole cache on deletes when we're not 813 * sure what exactly was deleted or changed. 814 */ 815 public static void invalidate(String tableName) { 816 SettingsCache cache = SettingsCache.forTable(tableName); 817 if (cache == null) { 818 return; 819 } 820 synchronized (cache) { 821 cache.evictAll(); 822 cache.mCacheFullyMatchesDisk = false; 823 } 824 } 825 826 /** 827 * For suppressing duplicate/redundant settings inserts early, 828 * checking our cache first (but without faulting it in), 829 * before going to sqlite with the mutation. 830 */ 831 public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) { 832 if (cache == null) return false; 833 synchronized (cache) { 834 Bundle bundle = cache.get(name); 835 if (bundle == null) return false; 836 String oldValue = bundle.getPairValue(); 837 if (oldValue == null && value == null) return true; 838 if ((oldValue == null) != (value == null)) return false; 839 return oldValue.equals(value); 840 } 841 } 842 } 843} 844