• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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