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