blob: 81d82dea6aef0cdb6eb35fdf1b0ec9605c756c24 [file] [log] [blame]
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001/*
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
17package com.android.providers.settings;
18
-b master501eec92009-07-06 13:53:11 -070019import java.io.FileNotFoundException;
Doug Zongker4f8ff392010-02-03 10:36:40 -080020import java.io.UnsupportedEncodingException;
Fred Quintanac70239e2009-12-17 10:28:33 -080021import java.security.NoSuchAlgorithmException;
Doug Zongker4f8ff392010-02-03 10:36:40 -080022import java.security.SecureRandom;
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -080023import java.util.LinkedHashMap;
24import java.util.Map;
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -070025import java.util.concurrent.atomic.AtomicBoolean;
26import java.util.concurrent.atomic.AtomicInteger;
-b master501eec92009-07-06 13:53:11 -070027
Christopher Tate45281862010-03-05 15:46:30 -080028import android.app.backup.BackupManager;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070029import android.content.ContentProvider;
30import android.content.ContentUris;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.pm.PackageManager;
Marco Nelissen69f593c2009-07-28 09:55:04 -070034import android.content.res.AssetFileDescriptor;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070035import android.database.Cursor;
36import android.database.sqlite.SQLiteDatabase;
Brad Fitzpatrick1877d012010-03-04 17:48:13 -080037import android.database.sqlite.SQLiteException;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070038import android.database.sqlite.SQLiteQueryBuilder;
39import android.media.RingtoneManager;
40import android.net.Uri;
Brad Fitzpatrick1877d012010-03-04 17:48:13 -080041import android.os.Bundle;
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -070042import android.os.FileObserver;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070043import android.os.ParcelFileDescriptor;
44import android.os.SystemProperties;
45import android.provider.DrmStore;
46import android.provider.MediaStore;
47import android.provider.Settings;
48import android.text.TextUtils;
49import android.util.Log;
50
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070051public class SettingsProvider extends ContentProvider {
52 private static final String TAG = "SettingsProvider";
53 private static final boolean LOCAL_LOGV = false;
54
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080055 private static final String TABLE_FAVORITES = "favorites";
56 private static final String TABLE_OLD_FAVORITES = "old_favorites";
57
Brad Fitzpatrick1877d012010-03-04 17:48:13 -080058 private static final String[] COLUMN_VALUE = new String[] { "value" };
59
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -080060 // Cache for settings, access-ordered for acting as LRU.
61 // Guarded by themselves.
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -070062 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);
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -080071
Brad Fitzpatrick342984a2010-03-09 16:59:30 -080072 // 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
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -080077 private static final Bundle NULL_SETTING = Bundle.forPair("value", null);
78
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -070079 // 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
James Wylder074da8f2009-02-25 08:38:31 -060083 protected DatabaseHelper mOpenHelper;
Amith Yamasani8823c0a82009-07-07 14:30:17 -070084 private BackupManager mBackupManager;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070085
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 {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080091 public String table;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070092 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);
Dianne Hackborn24117ce2010-07-12 15:54:38 -070099 if (!DatabaseHelper.isValidTable(this.table)) {
100 throw new IllegalArgumentException("Bad root path: " + this.table);
101 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700102 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);
Dianne Hackborn24117ce2010-07-12 15:54:38 -0700110 if (!DatabaseHelper.isValidTable(this.table)) {
111 throw new IllegalArgumentException("Bad root path: " + this.table);
112 }
Doug Zongkeraed8f8e2010-01-07 18:07:50 -0800113 if ("system".equals(this.table) || "secure".equals(this.table)) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700114 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);
Dianne Hackborn24117ce2010-07-12 15:54:38 -0700127 if (!DatabaseHelper.isValidTable(this.table)) {
128 throw new IllegalArgumentException("Bad root path: " + this.table);
129 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700130 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);
Doug Zongkeraed8f8e2010-01-07 18:07:50 -0800150 if ("system".equals(table) || "secure".equals(table)) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700151 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
Amith Yamasanid1582142009-07-08 20:04:55 -0700170 boolean backedUpDataChanged = false;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700171 String property = null, table = uri.getPathSegments().get(0);
172 if (table.equals("system")) {
173 property = Settings.System.SYS_PROP_SETTING_VERSION;
Amith Yamasanid1582142009-07-08 20:04:55 -0700174 backedUpDataChanged = true;
The Android Open Source Projectf013e1a2008-12-17 18:05:43 -0800175 } else if (table.equals("secure")) {
176 property = Settings.Secure.SYS_PROP_SETTING_VERSION;
Amith Yamasanid1582142009-07-08 20:04:55 -0700177 backedUpDataChanged = true;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700178 }
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
-b master501eec92009-07-06 13:53:11 -0700186 // Inform the backup manager about a data change
Amith Yamasanid1582142009-07-08 20:04:55 -0700187 if (backedUpDataChanged) {
188 mBackupManager.dataChanged();
189 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700190 // 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) {
The Android Open Source Projectf013e1a2008-12-17 18:05:43 -0800207 if ("secure".equals(args.table) &&
The Android Open Source Projectf013e1a2008-12-17 18:05:43 -0800208 getContext().checkCallingOrSelfPermission(
Doug Zongkeraed8f8e2010-01-07 18:07:50 -0800209 android.Manifest.permission.WRITE_SECURE_SETTINGS) !=
210 PackageManager.PERMISSION_GRANTED) {
Brett Chabot16dd82c2009-06-18 17:00:48 -0700211 throw new SecurityException(
Doug Zongkeraed8f8e2010-01-07 18:07:50 -0800212 String.format("Permission denial: writing to secure settings requires %1$s",
213 android.Manifest.permission.WRITE_SECURE_SETTINGS));
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700214 }
215 }
216
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700217 // 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
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700254 @Override
255 public boolean onCreate() {
256 mOpenHelper = new DatabaseHelper(getContext());
Amith Yamasani8823c0a82009-07-07 14:30:17 -0700257 mBackupManager = new BackupManager(getContext());
Fred Quintanac70239e2009-12-17 10:28:33 -0800258
259 if (!ensureAndroidIdIsSet()) {
260 return false;
261 }
262
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700263 // 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();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700273 return true;
274 }
275
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700276 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 + "' fullycached=" +
315 cache.fullyMatchesDisk());
316 }
317 } finally {
318 c.close();
319 }
320 }
321
Fred Quintanac70239e2009-12-17 10:28:33 -0800322 private boolean ensureAndroidIdIsSet() {
323 final Cursor c = query(Settings.Secure.CONTENT_URI,
324 new String[] { Settings.NameValueTable.VALUE },
325 Settings.NameValueTable.NAME + "=?",
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800326 new String[] { Settings.Secure.ANDROID_ID }, null);
Fred Quintanac70239e2009-12-17 10:28:33 -0800327 try {
328 final String value = c.moveToNext() ? c.getString(0) : null;
329 if (value == null) {
330 final SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
Doug Zongker0fe27cf2010-08-19 13:38:26 -0700331 String serial = SystemProperties.get("ro.serialno", "");
332 random.setSeed(
333 (serial + System.nanoTime() + new SecureRandom().nextLong()).getBytes());
Fred Quintanac70239e2009-12-17 10:28:33 -0800334 final String newAndroidIdValue = Long.toHexString(random.nextLong());
Doug Zongker0fe27cf2010-08-19 13:38:26 -0700335 Log.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue + "]");
Fred Quintanac70239e2009-12-17 10:28:33 -0800336 final ContentValues values = new ContentValues();
337 values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID);
338 values.put(Settings.NameValueTable.VALUE, newAndroidIdValue);
339 final Uri uri = insert(Settings.Secure.CONTENT_URI, values);
340 if (uri == null) {
341 return false;
342 }
343 }
344 return true;
345 } catch (NoSuchAlgorithmException e) {
346 return false;
347 } finally {
348 c.close();
349 }
350 }
351
Brad Fitzpatrick1877d012010-03-04 17:48:13 -0800352 /**
353 * Fast path that avoids the use of chatty remoted Cursors.
354 */
355 @Override
356 public Bundle call(String method, String request, Bundle args) {
357 if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) {
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800358 return lookupValue("system", sSystemCache, request);
Brad Fitzpatrick1877d012010-03-04 17:48:13 -0800359 }
Brad Fitzpatrick1877d012010-03-04 17:48:13 -0800360 if (Settings.CALL_METHOD_GET_SECURE.equals(method)) {
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800361 return lookupValue("secure", sSecureCache, request);
Brad Fitzpatrick1877d012010-03-04 17:48:13 -0800362 }
363 return null;
364 }
365
366 // Looks up value 'key' in 'table' and returns either a single-pair Bundle,
367 // possibly with a null value, or null on failure.
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800368 private Bundle lookupValue(String table, SettingsCache cache, String key) {
369 synchronized (cache) {
370 if (cache.containsKey(key)) {
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700371 Bundle value = cache.get(key);
372 if (value != TOO_LARGE_TO_CACHE_MARKER) {
373 return value;
374 }
375 // else we fall through and read the value from disk
376 } else if (cache.fullyMatchesDisk()) {
377 // Fast path (very common). Don't even try touch disk
378 // if we know we've slurped it all in. Trying to
379 // touch the disk would mean waiting for yaffs2 to
380 // give us access, which could takes hundreds of
381 // milliseconds. And we're very likely being called
382 // from somebody's UI thread...
383 return NULL_SETTING;
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800384 }
385 }
386
Brad Fitzpatrick1877d012010-03-04 17:48:13 -0800387 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
388 Cursor cursor = null;
389 try {
390 cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key},
391 null, null, null, null);
392 if (cursor != null && cursor.getCount() == 1) {
393 cursor.moveToFirst();
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800394 return cache.putIfAbsent(key, cursor.getString(0));
Brad Fitzpatrick1877d012010-03-04 17:48:13 -0800395 }
396 } catch (SQLiteException e) {
397 Log.w(TAG, "settings lookup error", e);
398 return null;
399 } finally {
400 if (cursor != null) cursor.close();
401 }
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800402 cache.putIfAbsent(key, null);
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800403 return NULL_SETTING;
Brad Fitzpatrick1877d012010-03-04 17:48:13 -0800404 }
405
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700406 @Override
407 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
408 SqlArguments args = new SqlArguments(url, where, whereArgs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800409 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
410
The Android Open Source Projectf013e1a2008-12-17 18:05:43 -0800411 // The favorites table was moved from this provider to a provider inside Home
412 // Home still need to query this table to upgrade from pre-cupcake builds
413 // However, a cupcake+ build with no data does not contain this table which will
414 // cause an exception in the SQL stack. The following line is a special case to
415 // let the caller of the query have a chance to recover and avoid the exception
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800416 if (TABLE_FAVORITES.equals(args.table)) {
417 return null;
418 } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
419 args.table = TABLE_FAVORITES;
420 Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null);
421 if (cursor != null) {
422 boolean exists = cursor.getCount() > 0;
423 cursor.close();
424 if (!exists) return null;
425 } else {
426 return null;
427 }
428 }
The Android Open Source Projectf013e1a2008-12-17 18:05:43 -0800429
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700430 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
431 qb.setTables(args.table);
432
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700433 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
434 ret.setNotificationUri(getContext().getContentResolver(), url);
435 return ret;
436 }
437
438 @Override
439 public String getType(Uri url) {
440 // If SqlArguments supplies a where clause, then it must be an item
441 // (because we aren't supplying our own where clause).
442 SqlArguments args = new SqlArguments(url, null, null);
443 if (TextUtils.isEmpty(args.where)) {
444 return "vnd.android.cursor.dir/" + args.table;
445 } else {
446 return "vnd.android.cursor.item/" + args.table;
447 }
448 }
449
450 @Override
451 public int bulkInsert(Uri uri, ContentValues[] values) {
452 SqlArguments args = new SqlArguments(uri);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800453 if (TABLE_FAVORITES.equals(args.table)) {
454 return 0;
455 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700456 checkWritePermissions(args);
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800457 SettingsCache cache = SettingsCache.forTable(args.table);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700458
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700459 sKnownMutationsInFlight.incrementAndGet();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700460 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
461 db.beginTransaction();
462 try {
463 int numValues = values.length;
464 for (int i = 0; i < numValues; i++) {
465 if (db.insert(args.table, null, values[i]) < 0) return 0;
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800466 SettingsCache.populate(cache, values[i]);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700467 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
468 }
469 db.setTransactionSuccessful();
470 } finally {
471 db.endTransaction();
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700472 sKnownMutationsInFlight.decrementAndGet();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700473 }
474
475 sendNotify(uri);
476 return values.length;
477 }
478
Mike Lockwoodbd2a7122009-04-02 23:41:33 -0700479 /*
480 * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
481 * This setting contains a list of the currently enabled location providers.
482 * But helper functions in android.providers.Settings can enable or disable
483 * a single provider by using a "+" or "-" prefix before the provider name.
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800484 *
485 * @returns whether the database needs to be updated or not, also modifying
486 * 'initialValues' if needed.
Mike Lockwoodbd2a7122009-04-02 23:41:33 -0700487 */
488 private boolean parseProviderList(Uri url, ContentValues initialValues) {
489 String value = initialValues.getAsString(Settings.Secure.VALUE);
490 String newProviders = null;
491 if (value != null && value.length() > 1) {
492 char prefix = value.charAt(0);
493 if (prefix == '+' || prefix == '-') {
494 // skip prefix
495 value = value.substring(1);
496
497 // read list of enabled providers into "providers"
498 String providers = "";
499 String[] columns = {Settings.Secure.VALUE};
500 String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'";
501 Cursor cursor = query(url, columns, where, null, null);
502 if (cursor != null && cursor.getCount() == 1) {
503 try {
504 cursor.moveToFirst();
505 providers = cursor.getString(0);
506 } finally {
507 cursor.close();
508 }
509 }
510
511 int index = providers.indexOf(value);
512 int end = index + value.length();
513 // check for commas to avoid matching on partial string
514 if (index > 0 && providers.charAt(index - 1) != ',') index = -1;
515 if (end < providers.length() && providers.charAt(end) != ',') index = -1;
516
517 if (prefix == '+' && index < 0) {
518 // append the provider to the list if not present
519 if (providers.length() == 0) {
520 newProviders = value;
521 } else {
522 newProviders = providers + ',' + value;
523 }
524 } else if (prefix == '-' && index >= 0) {
525 // remove the provider from the list if present
Mike Lockwoodbdc7f892010-04-21 18:24:57 -0400526 // remove leading or trailing comma
527 if (index > 0) {
528 index--;
529 } else if (end < providers.length()) {
530 end++;
531 }
Mike Lockwoodbd2a7122009-04-02 23:41:33 -0700532
533 newProviders = providers.substring(0, index);
534 if (end < providers.length()) {
535 newProviders += providers.substring(end);
536 }
537 } else {
538 // nothing changed, so no need to update the database
539 return false;
540 }
541
542 if (newProviders != null) {
543 initialValues.put(Settings.Secure.VALUE, newProviders);
544 }
545 }
546 }
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800547
Mike Lockwoodbd2a7122009-04-02 23:41:33 -0700548 return true;
549 }
550
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700551 @Override
552 public Uri insert(Uri url, ContentValues initialValues) {
553 SqlArguments args = new SqlArguments(url);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800554 if (TABLE_FAVORITES.equals(args.table)) {
555 return null;
556 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700557 checkWritePermissions(args);
558
Mike Lockwoodbd2a7122009-04-02 23:41:33 -0700559 // Special case LOCATION_PROVIDERS_ALLOWED.
560 // Support enabling/disabling a single provider (using "+" or "-" prefix)
561 String name = initialValues.getAsString(Settings.Secure.NAME);
562 if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
563 if (!parseProviderList(url, initialValues)) return null;
564 }
565
Brad Fitzpatrick547a96b2010-03-09 17:58:53 -0800566 SettingsCache cache = SettingsCache.forTable(args.table);
567 String value = initialValues.getAsString(Settings.NameValueTable.VALUE);
568 if (SettingsCache.isRedundantSetValue(cache, name, value)) {
569 return Uri.withAppendedPath(url, name);
570 }
571
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700572 sKnownMutationsInFlight.incrementAndGet();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700573 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
574 final long rowId = db.insert(args.table, null, initialValues);
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700575 sKnownMutationsInFlight.decrementAndGet();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700576 if (rowId <= 0) return null;
577
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800578 SettingsCache.populate(cache, initialValues); // before we notify
579
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700580 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues);
581 url = getUriFor(url, initialValues, rowId);
582 sendNotify(url);
583 return url;
584 }
585
586 @Override
587 public int delete(Uri url, String where, String[] whereArgs) {
588 SqlArguments args = new SqlArguments(url, where, whereArgs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800589 if (TABLE_FAVORITES.equals(args.table)) {
590 return 0;
591 } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
592 args.table = TABLE_FAVORITES;
593 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700594 checkWritePermissions(args);
595
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700596 sKnownMutationsInFlight.incrementAndGet();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700597 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
598 int count = db.delete(args.table, args.where, args.args);
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700599 sKnownMutationsInFlight.decrementAndGet();
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800600 if (count > 0) {
601 SettingsCache.wipe(args.table); // before we notify
602 sendNotify(url);
603 }
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700604 startAsyncCachePopulation();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700605 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
606 return count;
607 }
608
609 @Override
610 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
611 SqlArguments args = new SqlArguments(url, where, whereArgs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800612 if (TABLE_FAVORITES.equals(args.table)) {
613 return 0;
614 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700615 checkWritePermissions(args);
616
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700617 sKnownMutationsInFlight.incrementAndGet();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700618 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700619 sKnownMutationsInFlight.decrementAndGet();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700620 int count = db.update(args.table, initialValues, args.where, args.args);
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800621 if (count > 0) {
622 SettingsCache.wipe(args.table); // before we notify
623 sendNotify(url);
624 }
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700625 startAsyncCachePopulation();
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700626 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
627 return count;
628 }
629
630 @Override
631 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
632
633 /*
634 * When a client attempts to openFile the default ringtone or
635 * notification setting Uri, we will proxy the call to the current
636 * default ringtone's Uri (if it is in the DRM or media provider).
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700637 */
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700638 int ringtoneType = RingtoneManager.getDefaultType(uri);
639 // Above call returns -1 if the Uri doesn't match a default type
640 if (ringtoneType != -1) {
641 Context context = getContext();
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700642
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700643 // Get the current value for the default sound
644 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700645
Marco Nelissen69f593c2009-07-28 09:55:04 -0700646 if (soundUri != null) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700647 // Only proxy the openFile call to drm or media providers
648 String authority = soundUri.getAuthority();
649 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY);
650 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) {
651
652 if (isDrmAuthority) {
653 try {
654 // Check DRM access permission here, since once we
655 // do the below call the DRM will be checking our
656 // permission, not our caller's permission
657 DrmStore.enforceAccessDrmPermission(context);
658 } catch (SecurityException e) {
659 throw new FileNotFoundException(e.getMessage());
660 }
661 }
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700662
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700663 return context.getContentResolver().openFileDescriptor(soundUri, mode);
664 }
665 }
666 }
667
668 return super.openFile(uri, mode);
669 }
Marco Nelissen69f593c2009-07-28 09:55:04 -0700670
671 @Override
672 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
673
674 /*
675 * When a client attempts to openFile the default ringtone or
676 * notification setting Uri, we will proxy the call to the current
677 * default ringtone's Uri (if it is in the DRM or media provider).
678 */
679 int ringtoneType = RingtoneManager.getDefaultType(uri);
680 // Above call returns -1 if the Uri doesn't match a default type
681 if (ringtoneType != -1) {
682 Context context = getContext();
683
684 // Get the current value for the default sound
685 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
686
687 if (soundUri != null) {
688 // Only proxy the openFile call to drm or media providers
689 String authority = soundUri.getAuthority();
690 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY);
691 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) {
692
693 if (isDrmAuthority) {
694 try {
695 // Check DRM access permission here, since once we
696 // do the below call the DRM will be checking our
697 // permission, not our caller's permission
698 DrmStore.enforceAccessDrmPermission(context);
699 } catch (SecurityException e) {
700 throw new FileNotFoundException(e.getMessage());
701 }
702 }
703
704 ParcelFileDescriptor pfd = null;
705 try {
706 pfd = context.getContentResolver().openFileDescriptor(soundUri, mode);
707 return new AssetFileDescriptor(pfd, 0, -1);
708 } catch (FileNotFoundException ex) {
709 // fall through and open the fallback ringtone below
710 }
711 }
712
713 try {
714 return super.openAssetFile(soundUri, mode);
715 } catch (FileNotFoundException ex) {
716 // Since a non-null Uri was specified, but couldn't be opened,
717 // fall back to the built-in ringtone.
718 return context.getResources().openRawResourceFd(
719 com.android.internal.R.raw.fallbackring);
720 }
721 }
722 // no need to fall through and have openFile() try again, since we
723 // already know that will fail.
724 throw new FileNotFoundException(); // or return null ?
725 }
726
727 // Note that this will end up calling openFile() above.
728 return super.openAssetFile(uri, mode);
729 }
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800730
731 /**
732 * In-memory LRU Cache of system and secure settings, along with
733 * associated helper functions to keep cache coherent with the
734 * database.
735 */
736 private static final class SettingsCache extends LinkedHashMap<String, Bundle> {
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800737
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700738 private final String mCacheName;
739 private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped.
740
741 public SettingsCache(String name) {
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800742 super(MAX_CACHE_ENTRIES, 0.75f /* load factor */, true /* access ordered */);
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700743 mCacheName = name;
744 }
745
746 /**
747 * Is the whole database table slurped into this cache?
748 */
749 public boolean fullyMatchesDisk() {
750 synchronized (this) {
751 return mCacheFullyMatchesDisk;
752 }
753 }
754
755 public void setFullyMatchesDisk(boolean value) {
756 synchronized (this) {
757 mCacheFullyMatchesDisk = value;
758 }
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800759 }
760
761 @Override
762 protected boolean removeEldestEntry(Map.Entry eldest) {
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700763 if (size() <= MAX_CACHE_ENTRIES) {
764 return false;
765 }
766 synchronized (this) {
767 mCacheFullyMatchesDisk = false;
768 }
769 return true;
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800770 }
771
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800772 /**
773 * Atomic cache population, conditional on size of value and if
774 * we lost a race.
775 *
776 * @returns a Bundle to send back to the client from call(), even
777 * if we lost the race.
778 */
779 public Bundle putIfAbsent(String key, String value) {
780 Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value);
781 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
782 synchronized (this) {
783 if (!containsKey(key)) {
784 put(key, bundle);
785 }
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800786 }
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800787 }
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800788 return bundle;
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800789 }
790
791 public static SettingsCache forTable(String tableName) {
792 if ("system".equals(tableName)) {
793 return SettingsProvider.sSystemCache;
794 }
795 if ("secure".equals(tableName)) {
796 return SettingsProvider.sSecureCache;
797 }
798 return null;
799 }
800
801 /**
802 * Populates a key in a given (possibly-null) cache.
803 */
804 public static void populate(SettingsCache cache, ContentValues contentValues) {
805 if (cache == null) {
806 return;
807 }
808 String name = contentValues.getAsString(Settings.NameValueTable.NAME);
809 if (name == null) {
810 Log.w(TAG, "null name populating settings cache.");
811 return;
812 }
813 String value = contentValues.getAsString(Settings.NameValueTable.VALUE);
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700814 cache.populate(name, value);
815 }
816
817 public void populate(String name, String value) {
818 synchronized (this) {
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800819 if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700820 put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value));
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800821 } else {
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700822 put(name, TOO_LARGE_TO_CACHE_MARKER);
Brad Fitzpatrick342984a2010-03-09 16:59:30 -0800823 }
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800824 }
825 }
826
827 /**
828 * Used for wiping a whole cache on deletes when we're not
829 * sure what exactly was deleted or changed.
830 */
831 public static void wipe(String tableName) {
832 SettingsCache cache = SettingsCache.forTable(tableName);
833 if (cache == null) {
834 return;
835 }
836 synchronized (cache) {
837 cache.clear();
Brad Fitzpatrickf366a9b2010-08-24 16:14:07 -0700838 cache.mCacheFullyMatchesDisk = true;
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800839 }
840 }
841
Brad Fitzpatrick547a96b2010-03-09 17:58:53 -0800842 /**
843 * For suppressing duplicate/redundant settings inserts early,
844 * checking our cache first (but without faulting it in),
845 * before going to sqlite with the mutation.
846 */
847 public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) {
848 if (cache == null) return false;
849 synchronized (cache) {
850 Bundle bundle = cache.get(name);
851 if (bundle == null) return false;
852 String oldValue = bundle.getPairValue();
853 if (oldValue == null && value == null) return true;
854 if ((oldValue == null) != (value == null)) return false;
855 return oldValue.equals(value);
856 }
857 }
Brad Fitzpatrick1bd62bd2010-03-08 18:30:52 -0800858 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700859}