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 17 package com.android.providers.settings; 18 19 import java.io.FileNotFoundException; 20 import java.security.SecureRandom; 21 import java.util.concurrent.atomic.AtomicBoolean; 22 import java.util.concurrent.atomic.AtomicInteger; 23 24 import android.app.backup.BackupManager; 25 import android.content.ContentProvider; 26 import android.content.ContentUris; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.content.res.AssetFileDescriptor; 31 import android.database.Cursor; 32 import android.database.sqlite.SQLiteDatabase; 33 import android.database.sqlite.SQLiteException; 34 import android.database.sqlite.SQLiteQueryBuilder; 35 import android.media.RingtoneManager; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.FileObserver; 39 import android.os.ParcelFileDescriptor; 40 import android.os.SystemProperties; 41 import android.provider.DrmStore; 42 import android.provider.MediaStore; 43 import android.provider.Settings; 44 import android.text.TextUtils; 45 import android.util.Log; 46 import android.util.LruCache; 47 48 public 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 db.enableWriteAheadLogging(); 264 sObserverInstance = new SettingsFileObserver(db.getPath()); 265 sObserverInstance.startWatching(); 266 startAsyncCachePopulation(); 267 return true; 268 } 269 270 private void startAsyncCachePopulation() { 271 new Thread("populate-settings-caches") { 272 public void run() { 273 fullyPopulateCaches(); 274 } 275 }.start(); 276 } 277 278 private void fullyPopulateCaches() { 279 fullyPopulateCache("secure", sSecureCache); 280 fullyPopulateCache("system", sSystemCache); 281 } 282 283 // Slurp all values (if sane in number & size) into cache. 284 private void fullyPopulateCache(String table, SettingsCache cache) { 285 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 286 Cursor c = db.query( 287 table, 288 new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE }, 289 null, null, null, null, null, 290 "" + (MAX_CACHE_ENTRIES + 1) /* limit */); 291 try { 292 synchronized (cache) { 293 cache.evictAll(); 294 cache.setFullyMatchesDisk(true); // optimistic 295 int rows = 0; 296 while (c.moveToNext()) { 297 rows++; 298 String name = c.getString(0); 299 String value = c.getString(1); 300 cache.populate(name, value); 301 } 302 if (rows > MAX_CACHE_ENTRIES) { 303 // Somewhat redundant, as removeEldestEntry() will 304 // have already done this, but to be explicit: 305 cache.setFullyMatchesDisk(false); 306 Log.d(TAG, "row count exceeds max cache entries for table " + table); 307 } 308 Log.d(TAG, "cache for settings table '" + table + "' rows=" + rows + "; fullycached=" + 309 cache.fullyMatchesDisk()); 310 } 311 } finally { 312 c.close(); 313 } 314 } 315 316 private boolean ensureAndroidIdIsSet() { 317 final Cursor c = query(Settings.Secure.CONTENT_URI, 318 new String[] { Settings.NameValueTable.VALUE }, 319 Settings.NameValueTable.NAME + "=?", 320 new String[] { Settings.Secure.ANDROID_ID }, null); 321 try { 322 final String value = c.moveToNext() ? c.getString(0) : null; 323 if (value == null) { 324 final SecureRandom random = new SecureRandom(); 325 final String newAndroidIdValue = Long.toHexString(random.nextLong()); 326 Log.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue + "]"); 327 final ContentValues values = new ContentValues(); 328 values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID); 329 values.put(Settings.NameValueTable.VALUE, newAndroidIdValue); 330 final Uri uri = insert(Settings.Secure.CONTENT_URI, values); 331 if (uri == null) { 332 return false; 333 } 334 } 335 return true; 336 } finally { 337 c.close(); 338 } 339 } 340 341 /** 342 * Fast path that avoids the use of chatty remoted Cursors. 343 */ 344 @Override 345 public Bundle call(String method, String request, Bundle args) { 346 if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) { 347 return lookupValue("system", sSystemCache, request); 348 } 349 if (Settings.CALL_METHOD_GET_SECURE.equals(method)) { 350 return lookupValue("secure", sSecureCache, request); 351 } 352 return null; 353 } 354 355 // Looks up value 'key' in 'table' and returns either a single-pair Bundle, 356 // possibly with a null value, or null on failure. 357 private Bundle lookupValue(String table, SettingsCache cache, String key) { 358 synchronized (cache) { 359 Bundle value = cache.get(key); 360 if (value != null) { 361 if (value != TOO_LARGE_TO_CACHE_MARKER) { 362 return value; 363 } 364 // else we fall through and read the value from disk 365 } else if (cache.fullyMatchesDisk()) { 366 // Fast path (very common). Don't even try touch disk 367 // if we know we've slurped it all in. Trying to 368 // touch the disk would mean waiting for yaffs2 to 369 // give us access, which could takes hundreds of 370 // milliseconds. And we're very likely being called 371 // from somebody's UI thread... 372 return NULL_SETTING; 373 } 374 } 375 376 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 377 Cursor cursor = null; 378 try { 379 cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key}, 380 null, null, null, null); 381 if (cursor != null && cursor.getCount() == 1) { 382 cursor.moveToFirst(); 383 return cache.putIfAbsent(key, cursor.getString(0)); 384 } 385 } catch (SQLiteException e) { 386 Log.w(TAG, "settings lookup error", e); 387 return null; 388 } finally { 389 if (cursor != null) cursor.close(); 390 } 391 cache.putIfAbsent(key, null); 392 return NULL_SETTING; 393 } 394 395 @Override 396 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) { 397 SqlArguments args = new SqlArguments(url, where, whereArgs); 398 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 399 400 // The favorites table was moved from this provider to a provider inside Home 401 // Home still need to query this table to upgrade from pre-cupcake builds 402 // However, a cupcake+ build with no data does not contain this table which will 403 // cause an exception in the SQL stack. The following line is a special case to 404 // let the caller of the query have a chance to recover and avoid the exception 405 if (TABLE_FAVORITES.equals(args.table)) { 406 return null; 407 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 408 args.table = TABLE_FAVORITES; 409 Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null); 410 if (cursor != null) { 411 boolean exists = cursor.getCount() > 0; 412 cursor.close(); 413 if (!exists) return null; 414 } else { 415 return null; 416 } 417 } 418 419 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 420 qb.setTables(args.table); 421 422 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort); 423 ret.setNotificationUri(getContext().getContentResolver(), url); 424 return ret; 425 } 426 427 @Override 428 public String getType(Uri url) { 429 // If SqlArguments supplies a where clause, then it must be an item 430 // (because we aren't supplying our own where clause). 431 SqlArguments args = new SqlArguments(url, null, null); 432 if (TextUtils.isEmpty(args.where)) { 433 return "vnd.android.cursor.dir/" + args.table; 434 } else { 435 return "vnd.android.cursor.item/" + args.table; 436 } 437 } 438 439 @Override 440 public int bulkInsert(Uri uri, ContentValues[] values) { 441 SqlArguments args = new SqlArguments(uri); 442 if (TABLE_FAVORITES.equals(args.table)) { 443 return 0; 444 } 445 checkWritePermissions(args); 446 SettingsCache cache = SettingsCache.forTable(args.table); 447 448 sKnownMutationsInFlight.incrementAndGet(); 449 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 450 db.beginTransaction(); 451 try { 452 int numValues = values.length; 453 for (int i = 0; i < numValues; i++) { 454 if (db.insert(args.table, null, values[i]) < 0) return 0; 455 SettingsCache.populate(cache, values[i]); 456 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]); 457 } 458 db.setTransactionSuccessful(); 459 } finally { 460 db.endTransaction(); 461 sKnownMutationsInFlight.decrementAndGet(); 462 } 463 464 sendNotify(uri); 465 return values.length; 466 } 467 468 /* 469 * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED. 470 * This setting contains a list of the currently enabled location providers. 471 * But helper functions in android.providers.Settings can enable or disable 472 * a single provider by using a "+" or "-" prefix before the provider name. 473 * 474 * @returns whether the database needs to be updated or not, also modifying 475 * 'initialValues' if needed. 476 */ 477 private boolean parseProviderList(Uri url, ContentValues initialValues) { 478 String value = initialValues.getAsString(Settings.Secure.VALUE); 479 String newProviders = null; 480 if (value != null && value.length() > 1) { 481 char prefix = value.charAt(0); 482 if (prefix == '+' || prefix == '-') { 483 // skip prefix 484 value = value.substring(1); 485 486 // read list of enabled providers into "providers" 487 String providers = ""; 488 String[] columns = {Settings.Secure.VALUE}; 489 String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'"; 490 Cursor cursor = query(url, columns, where, null, null); 491 if (cursor != null && cursor.getCount() == 1) { 492 try { 493 cursor.moveToFirst(); 494 providers = cursor.getString(0); 495 } finally { 496 cursor.close(); 497 } 498 } 499 500 int index = providers.indexOf(value); 501 int end = index + value.length(); 502 // check for commas to avoid matching on partial string 503 if (index > 0 && providers.charAt(index - 1) != ',') index = -1; 504 if (end < providers.length() && providers.charAt(end) != ',') index = -1; 505 506 if (prefix == '+' && index < 0) { 507 // append the provider to the list if not present 508 if (providers.length() == 0) { 509 newProviders = value; 510 } else { 511 newProviders = providers + ',' + value; 512 } 513 } else if (prefix == '-' && index >= 0) { 514 // remove the provider from the list if present 515 // remove leading or trailing comma 516 if (index > 0) { 517 index--; 518 } else if (end < providers.length()) { 519 end++; 520 } 521 522 newProviders = providers.substring(0, index); 523 if (end < providers.length()) { 524 newProviders += providers.substring(end); 525 } 526 } else { 527 // nothing changed, so no need to update the database 528 return false; 529 } 530 531 if (newProviders != null) { 532 initialValues.put(Settings.Secure.VALUE, newProviders); 533 } 534 } 535 } 536 537 return true; 538 } 539 540 @Override 541 public Uri insert(Uri url, ContentValues initialValues) { 542 SqlArguments args = new SqlArguments(url); 543 if (TABLE_FAVORITES.equals(args.table)) { 544 return null; 545 } 546 checkWritePermissions(args); 547 548 // Special case LOCATION_PROVIDERS_ALLOWED. 549 // Support enabling/disabling a single provider (using "+" or "-" prefix) 550 String name = initialValues.getAsString(Settings.Secure.NAME); 551 if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) { 552 if (!parseProviderList(url, initialValues)) return null; 553 } 554 555 SettingsCache cache = SettingsCache.forTable(args.table); 556 String value = initialValues.getAsString(Settings.NameValueTable.VALUE); 557 if (SettingsCache.isRedundantSetValue(cache, name, value)) { 558 return Uri.withAppendedPath(url, name); 559 } 560 561 sKnownMutationsInFlight.incrementAndGet(); 562 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 563 final long rowId = db.insert(args.table, null, initialValues); 564 sKnownMutationsInFlight.decrementAndGet(); 565 if (rowId <= 0) return null; 566 567 SettingsCache.populate(cache, initialValues); // before we notify 568 569 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues); 570 url = getUriFor(url, initialValues, rowId); 571 sendNotify(url); 572 return url; 573 } 574 575 @Override 576 public int delete(Uri url, String where, String[] whereArgs) { 577 SqlArguments args = new SqlArguments(url, where, whereArgs); 578 if (TABLE_FAVORITES.equals(args.table)) { 579 return 0; 580 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 581 args.table = TABLE_FAVORITES; 582 } 583 checkWritePermissions(args); 584 585 sKnownMutationsInFlight.incrementAndGet(); 586 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 587 int count = db.delete(args.table, args.where, args.args); 588 sKnownMutationsInFlight.decrementAndGet(); 589 if (count > 0) { 590 SettingsCache.invalidate(args.table); // before we notify 591 sendNotify(url); 592 } 593 startAsyncCachePopulation(); 594 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted"); 595 return count; 596 } 597 598 @Override 599 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) { 600 SqlArguments args = new SqlArguments(url, where, whereArgs); 601 if (TABLE_FAVORITES.equals(args.table)) { 602 return 0; 603 } 604 checkWritePermissions(args); 605 606 sKnownMutationsInFlight.incrementAndGet(); 607 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 608 int count = db.update(args.table, initialValues, args.where, args.args); 609 sKnownMutationsInFlight.decrementAndGet(); 610 if (count > 0) { 611 SettingsCache.invalidate(args.table); // before we notify 612 sendNotify(url); 613 } 614 startAsyncCachePopulation(); 615 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues); 616 return count; 617 } 618 619 @Override 620 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 621 622 /* 623 * When a client attempts to openFile the default ringtone or 624 * notification setting Uri, we will proxy the call to the current 625 * default ringtone's Uri (if it is in the DRM or media provider). 626 */ 627 int ringtoneType = RingtoneManager.getDefaultType(uri); 628 // Above call returns -1 if the Uri doesn't match a default type 629 if (ringtoneType != -1) { 630 Context context = getContext(); 631 632 // Get the current value for the default sound 633 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 634 635 if (soundUri != null) { 636 // Only proxy the openFile call to drm or media providers 637 String authority = soundUri.getAuthority(); 638 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY); 639 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) { 640 641 if (isDrmAuthority) { 642 try { 643 // Check DRM access permission here, since once we 644 // do the below call the DRM will be checking our 645 // permission, not our caller's permission 646 DrmStore.enforceAccessDrmPermission(context); 647 } catch (SecurityException e) { 648 throw new FileNotFoundException(e.getMessage()); 649 } 650 } 651 652 return context.getContentResolver().openFileDescriptor(soundUri, mode); 653 } 654 } 655 } 656 657 return super.openFile(uri, mode); 658 } 659 660 @Override 661 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 662 663 /* 664 * When a client attempts to openFile the default ringtone or 665 * notification setting Uri, we will proxy the call to the current 666 * default ringtone's Uri (if it is in the DRM or media provider). 667 */ 668 int ringtoneType = RingtoneManager.getDefaultType(uri); 669 // Above call returns -1 if the Uri doesn't match a default type 670 if (ringtoneType != -1) { 671 Context context = getContext(); 672 673 // Get the current value for the default sound 674 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 675 676 if (soundUri != null) { 677 // Only proxy the openFile call to drm or media providers 678 String authority = soundUri.getAuthority(); 679 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY); 680 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) { 681 682 if (isDrmAuthority) { 683 try { 684 // Check DRM access permission here, since once we 685 // do the below call the DRM will be checking our 686 // permission, not our caller's permission 687 DrmStore.enforceAccessDrmPermission(context); 688 } catch (SecurityException e) { 689 throw new FileNotFoundException(e.getMessage()); 690 } 691 } 692 693 ParcelFileDescriptor pfd = null; 694 try { 695 pfd = context.getContentResolver().openFileDescriptor(soundUri, mode); 696 return new AssetFileDescriptor(pfd, 0, -1); 697 } catch (FileNotFoundException ex) { 698 // fall through and open the fallback ringtone below 699 } 700 } 701 702 try { 703 return super.openAssetFile(soundUri, mode); 704 } catch (FileNotFoundException ex) { 705 // Since a non-null Uri was specified, but couldn't be opened, 706 // fall back to the built-in ringtone. 707 return context.getResources().openRawResourceFd( 708 com.android.internal.R.raw.fallbackring); 709 } 710 } 711 // no need to fall through and have openFile() try again, since we 712 // already know that will fail. 713 throw new FileNotFoundException(); // or return null ? 714 } 715 716 // Note that this will end up calling openFile() above. 717 return super.openAssetFile(uri, mode); 718 } 719 720 /** 721 * In-memory LRU Cache of system and secure settings, along with 722 * associated helper functions to keep cache coherent with the 723 * database. 724 */ 725 private static final class SettingsCache extends LruCache<String, Bundle> { 726 727 private final String mCacheName; 728 private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped. 729 730 public SettingsCache(String name) { 731 super(MAX_CACHE_ENTRIES); 732 mCacheName = name; 733 } 734 735 /** 736 * Is the whole database table slurped into this cache? 737 */ 738 public boolean fullyMatchesDisk() { 739 synchronized (this) { 740 return mCacheFullyMatchesDisk; 741 } 742 } 743 744 public void setFullyMatchesDisk(boolean value) { 745 synchronized (this) { 746 mCacheFullyMatchesDisk = value; 747 } 748 } 749 750 @Override 751 protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) { 752 if (evicted) { 753 mCacheFullyMatchesDisk = false; 754 } 755 } 756 757 /** 758 * Atomic cache population, conditional on size of value and if 759 * we lost a race. 760 * 761 * @returns a Bundle to send back to the client from call(), even 762 * if we lost the race. 763 */ 764 public Bundle putIfAbsent(String key, String value) { 765 Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value); 766 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) { 767 synchronized (this) { 768 if (get(key) == null) { 769 put(key, bundle); 770 } 771 } 772 } 773 return bundle; 774 } 775 776 public static SettingsCache forTable(String tableName) { 777 if ("system".equals(tableName)) { 778 return SettingsProvider.sSystemCache; 779 } 780 if ("secure".equals(tableName)) { 781 return SettingsProvider.sSecureCache; 782 } 783 return null; 784 } 785 786 /** 787 * Populates a key in a given (possibly-null) cache. 788 */ 789 public static void populate(SettingsCache cache, ContentValues contentValues) { 790 if (cache == null) { 791 return; 792 } 793 String name = contentValues.getAsString(Settings.NameValueTable.NAME); 794 if (name == null) { 795 Log.w(TAG, "null name populating settings cache."); 796 return; 797 } 798 String value = contentValues.getAsString(Settings.NameValueTable.VALUE); 799 cache.populate(name, value); 800 } 801 802 public void populate(String name, String value) { 803 synchronized (this) { 804 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) { 805 put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value)); 806 } else { 807 put(name, TOO_LARGE_TO_CACHE_MARKER); 808 } 809 } 810 } 811 812 /** 813 * Used for wiping a whole cache on deletes when we're not 814 * sure what exactly was deleted or changed. 815 */ 816 public static void invalidate(String tableName) { 817 SettingsCache cache = SettingsCache.forTable(tableName); 818 if (cache == null) { 819 return; 820 } 821 synchronized (cache) { 822 cache.evictAll(); 823 cache.mCacheFullyMatchesDisk = false; 824 } 825 } 826 827 /** 828 * For suppressing duplicate/redundant settings inserts early, 829 * checking our cache first (but without faulting it in), 830 * before going to sqlite with the mutation. 831 */ 832 public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) { 833 if (cache == null) return false; 834 synchronized (cache) { 835 Bundle bundle = cache.get(name); 836 if (bundle == null) return false; 837 String oldValue = bundle.getPairValue(); 838 if (oldValue == null && value == null) return true; 839 if ((oldValue == null) != (value == null)) return false; 840 return oldValue.equals(value); 841 } 842 } 843 } 844 } 845