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.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. */
     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). */
    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      */
    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      */
    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      */
    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 
    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 
    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
    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 
    276     private void startAsyncCachePopulation() {
    277         new Thread("populate-settings-caches") {
    278             public void run() {
    279                 fullyPopulateCaches();
    280             }
    281         }.start();
    282     }
    283 
    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.
    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 
    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
    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.
    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
    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
    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
    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      */
    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
    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
    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
    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
    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
    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 
    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          */
    744         public boolean fullyMatchesDisk() {
    745             synchronized (this) {
    746                 return mCacheFullyMatchesDisk;
    747             }
    748         }
    749 
    750         public void setFullyMatchesDisk(boolean value) {
    751             synchronized (this) {
    752                 mCacheFullyMatchesDisk = value;
    753             }
    754         }
    755 
    756         @Override
    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          */
    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 
    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          */
    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 
    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          */
    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          */
    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