Home | History | Annotate | Download | only in settings
      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