SettingsProvider.java revision 4528186e0d65fc68ef0dd1941aa2ac8aefcd55a3
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 17package com.android.providers.settings; 18 19import java.io.FileNotFoundException; 20import java.io.UnsupportedEncodingException; 21import java.security.NoSuchAlgorithmException; 22import java.security.SecureRandom; 23 24import android.app.backup.BackupManager; 25import android.content.ContentProvider; 26import android.content.ContentUris; 27import android.content.ContentValues; 28import android.content.Context; 29import android.content.pm.PackageManager; 30import android.content.res.AssetFileDescriptor; 31import android.database.Cursor; 32import android.database.sqlite.SQLiteDatabase; 33import android.database.sqlite.SQLiteException; 34import android.database.sqlite.SQLiteQueryBuilder; 35import android.media.RingtoneManager; 36import android.net.Uri; 37import android.os.Bundle; 38import android.os.ParcelFileDescriptor; 39import android.os.SystemProperties; 40import android.provider.DrmStore; 41import android.provider.MediaStore; 42import android.provider.Settings; 43import android.text.TextUtils; 44import android.util.Log; 45 46public class SettingsProvider extends ContentProvider { 47 private static final String TAG = "SettingsProvider"; 48 private static final boolean LOCAL_LOGV = false; 49 50 private static final String TABLE_FAVORITES = "favorites"; 51 private static final String TABLE_OLD_FAVORITES = "old_favorites"; 52 53 private static final String[] COLUMN_VALUE = new String[] { "value" }; 54 55 protected DatabaseHelper mOpenHelper; 56 private BackupManager mBackupManager; 57 58 /** 59 * Decode a content URL into the table, projection, and arguments 60 * used to access the corresponding database rows. 61 */ 62 private static class SqlArguments { 63 public String table; 64 public final String where; 65 public final String[] args; 66 67 /** Operate on existing rows. */ 68 SqlArguments(Uri url, String where, String[] args) { 69 if (url.getPathSegments().size() == 1) { 70 this.table = url.getPathSegments().get(0); 71 this.where = where; 72 this.args = args; 73 } else if (url.getPathSegments().size() != 2) { 74 throw new IllegalArgumentException("Invalid URI: " + url); 75 } else if (!TextUtils.isEmpty(where)) { 76 throw new UnsupportedOperationException("WHERE clause not supported: " + url); 77 } else { 78 this.table = url.getPathSegments().get(0); 79 if ("system".equals(this.table) || "secure".equals(this.table)) { 80 this.where = Settings.NameValueTable.NAME + "=?"; 81 this.args = new String[] { url.getPathSegments().get(1) }; 82 } else { 83 this.where = "_id=" + ContentUris.parseId(url); 84 this.args = null; 85 } 86 } 87 } 88 89 /** Insert new rows (no where clause allowed). */ 90 SqlArguments(Uri url) { 91 if (url.getPathSegments().size() == 1) { 92 this.table = url.getPathSegments().get(0); 93 this.where = null; 94 this.args = null; 95 } else { 96 throw new IllegalArgumentException("Invalid URI: " + url); 97 } 98 } 99 } 100 101 /** 102 * Get the content URI of a row added to a table. 103 * @param tableUri of the entire table 104 * @param values found in the row 105 * @param rowId of the row 106 * @return the content URI for this particular row 107 */ 108 private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) { 109 if (tableUri.getPathSegments().size() != 1) { 110 throw new IllegalArgumentException("Invalid URI: " + tableUri); 111 } 112 String table = tableUri.getPathSegments().get(0); 113 if ("system".equals(table) || "secure".equals(table)) { 114 String name = values.getAsString(Settings.NameValueTable.NAME); 115 return Uri.withAppendedPath(tableUri, name); 116 } else { 117 return ContentUris.withAppendedId(tableUri, rowId); 118 } 119 } 120 121 /** 122 * Send a notification when a particular content URI changes. 123 * Modify the system property used to communicate the version of 124 * this table, for tables which have such a property. (The Settings 125 * contract class uses these to provide client-side caches.) 126 * @param uri to send notifications for 127 */ 128 private void sendNotify(Uri uri) { 129 // Update the system property *first*, so if someone is listening for 130 // a notification and then using the contract class to get their data, 131 // the system property will be updated and they'll get the new data. 132 133 boolean backedUpDataChanged = false; 134 String property = null, table = uri.getPathSegments().get(0); 135 if (table.equals("system")) { 136 property = Settings.System.SYS_PROP_SETTING_VERSION; 137 backedUpDataChanged = true; 138 } else if (table.equals("secure")) { 139 property = Settings.Secure.SYS_PROP_SETTING_VERSION; 140 backedUpDataChanged = true; 141 } 142 143 if (property != null) { 144 long version = SystemProperties.getLong(property, 0) + 1; 145 if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version); 146 SystemProperties.set(property, Long.toString(version)); 147 } 148 149 // Inform the backup manager about a data change 150 if (backedUpDataChanged) { 151 mBackupManager.dataChanged(); 152 } 153 // Now send the notification through the content framework. 154 155 String notify = uri.getQueryParameter("notify"); 156 if (notify == null || "true".equals(notify)) { 157 getContext().getContentResolver().notifyChange(uri, null); 158 if (LOCAL_LOGV) Log.v(TAG, "notifying: " + uri); 159 } else { 160 if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri); 161 } 162 } 163 164 /** 165 * Make sure the caller has permission to write this data. 166 * @param args supplied by the caller 167 * @throws SecurityException if the caller is forbidden to write. 168 */ 169 private void checkWritePermissions(SqlArguments args) { 170 if ("secure".equals(args.table) && 171 getContext().checkCallingOrSelfPermission( 172 android.Manifest.permission.WRITE_SECURE_SETTINGS) != 173 PackageManager.PERMISSION_GRANTED) { 174 throw new SecurityException( 175 String.format("Permission denial: writing to secure settings requires %1$s", 176 android.Manifest.permission.WRITE_SECURE_SETTINGS)); 177 } 178 } 179 180 @Override 181 public boolean onCreate() { 182 mOpenHelper = new DatabaseHelper(getContext()); 183 mBackupManager = new BackupManager(getContext()); 184 185 if (!ensureAndroidIdIsSet()) { 186 return false; 187 } 188 189 return true; 190 } 191 192 private boolean ensureAndroidIdIsSet() { 193 final Cursor c = query(Settings.Secure.CONTENT_URI, 194 new String[] { Settings.NameValueTable.VALUE }, 195 Settings.NameValueTable.NAME + "=?", 196 new String[]{Settings.Secure.ANDROID_ID}, null); 197 try { 198 final String value = c.moveToNext() ? c.getString(0) : null; 199 if (value == null) { 200 final SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); 201 String serial = SystemProperties.get("ro.serialno"); 202 if (serial != null) { 203 try { 204 random.setSeed(serial.getBytes("UTF-8")); 205 } catch (UnsupportedEncodingException ignore) { 206 // stick with default seed 207 } 208 } 209 final String newAndroidIdValue = Long.toHexString(random.nextLong()); 210 Log.d(TAG, "Generated and saved new ANDROID_ID"); 211 final ContentValues values = new ContentValues(); 212 values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID); 213 values.put(Settings.NameValueTable.VALUE, newAndroidIdValue); 214 final Uri uri = insert(Settings.Secure.CONTENT_URI, values); 215 if (uri == null) { 216 return false; 217 } 218 } 219 return true; 220 } catch (NoSuchAlgorithmException e) { 221 return false; 222 } finally { 223 c.close(); 224 } 225 } 226 227 /** 228 * Fast path that avoids the use of chatty remoted Cursors. 229 */ 230 @Override 231 public Bundle call(String method, String request, Bundle args) { 232 if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) { 233 return lookupValue("system", request); 234 } 235 236 if (Settings.CALL_METHOD_GET_SECURE.equals(method)) { 237 return lookupValue("secure", request); 238 } 239 return null; 240 } 241 242 // Looks up value 'key' in 'table' and returns either a single-pair Bundle, 243 // possibly with a null value, or null on failure. 244 private Bundle lookupValue(String table, String key) { 245 // TODO: avoid database lookup and serve from in-process cache. 246 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 247 Cursor cursor = null; 248 try { 249 cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key}, 250 null, null, null, null); 251 if (cursor != null && cursor.getCount() == 1) { 252 cursor.moveToFirst(); 253 String value = cursor.getString(0); 254 return Bundle.forPair("value", value); 255 } 256 } catch (SQLiteException e) { 257 Log.w(TAG, "settings lookup error", e); 258 return null; 259 } finally { 260 if (cursor != null) cursor.close(); 261 } 262 return Bundle.forPair("value", null); 263 } 264 265 @Override 266 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) { 267 SqlArguments args = new SqlArguments(url, where, whereArgs); 268 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 269 270 // The favorites table was moved from this provider to a provider inside Home 271 // Home still need to query this table to upgrade from pre-cupcake builds 272 // However, a cupcake+ build with no data does not contain this table which will 273 // cause an exception in the SQL stack. The following line is a special case to 274 // let the caller of the query have a chance to recover and avoid the exception 275 if (TABLE_FAVORITES.equals(args.table)) { 276 return null; 277 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 278 args.table = TABLE_FAVORITES; 279 Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null); 280 if (cursor != null) { 281 boolean exists = cursor.getCount() > 0; 282 cursor.close(); 283 if (!exists) return null; 284 } else { 285 return null; 286 } 287 } 288 289 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 290 qb.setTables(args.table); 291 292 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort); 293 ret.setNotificationUri(getContext().getContentResolver(), url); 294 return ret; 295 } 296 297 @Override 298 public String getType(Uri url) { 299 // If SqlArguments supplies a where clause, then it must be an item 300 // (because we aren't supplying our own where clause). 301 SqlArguments args = new SqlArguments(url, null, null); 302 if (TextUtils.isEmpty(args.where)) { 303 return "vnd.android.cursor.dir/" + args.table; 304 } else { 305 return "vnd.android.cursor.item/" + args.table; 306 } 307 } 308 309 @Override 310 public int bulkInsert(Uri uri, ContentValues[] values) { 311 SqlArguments args = new SqlArguments(uri); 312 if (TABLE_FAVORITES.equals(args.table)) { 313 return 0; 314 } 315 checkWritePermissions(args); 316 317 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 318 db.beginTransaction(); 319 try { 320 int numValues = values.length; 321 for (int i = 0; i < numValues; i++) { 322 if (db.insert(args.table, null, values[i]) < 0) return 0; 323 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]); 324 } 325 db.setTransactionSuccessful(); 326 } finally { 327 db.endTransaction(); 328 } 329 330 sendNotify(uri); 331 return values.length; 332 } 333 334 /* 335 * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED. 336 * This setting contains a list of the currently enabled location providers. 337 * But helper functions in android.providers.Settings can enable or disable 338 * a single provider by using a "+" or "-" prefix before the provider name. 339 */ 340 private boolean parseProviderList(Uri url, ContentValues initialValues) { 341 String value = initialValues.getAsString(Settings.Secure.VALUE); 342 String newProviders = null; 343 if (value != null && value.length() > 1) { 344 char prefix = value.charAt(0); 345 if (prefix == '+' || prefix == '-') { 346 // skip prefix 347 value = value.substring(1); 348 349 // read list of enabled providers into "providers" 350 String providers = ""; 351 String[] columns = {Settings.Secure.VALUE}; 352 String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'"; 353 Cursor cursor = query(url, columns, where, null, null); 354 if (cursor != null && cursor.getCount() == 1) { 355 try { 356 cursor.moveToFirst(); 357 providers = cursor.getString(0); 358 } finally { 359 cursor.close(); 360 } 361 } 362 363 int index = providers.indexOf(value); 364 int end = index + value.length(); 365 // check for commas to avoid matching on partial string 366 if (index > 0 && providers.charAt(index - 1) != ',') index = -1; 367 if (end < providers.length() && providers.charAt(end) != ',') index = -1; 368 369 if (prefix == '+' && index < 0) { 370 // append the provider to the list if not present 371 if (providers.length() == 0) { 372 newProviders = value; 373 } else { 374 newProviders = providers + ',' + value; 375 } 376 } else if (prefix == '-' && index >= 0) { 377 // remove the provider from the list if present 378 // remove leading and trailing commas 379 if (index > 0) index--; 380 if (end < providers.length()) end++; 381 382 newProviders = providers.substring(0, index); 383 if (end < providers.length()) { 384 newProviders += providers.substring(end); 385 } 386 } else { 387 // nothing changed, so no need to update the database 388 return false; 389 } 390 391 if (newProviders != null) { 392 initialValues.put(Settings.Secure.VALUE, newProviders); 393 } 394 } 395 } 396 397 return true; 398 } 399 400 @Override 401 public Uri insert(Uri url, ContentValues initialValues) { 402 SqlArguments args = new SqlArguments(url); 403 if (TABLE_FAVORITES.equals(args.table)) { 404 return null; 405 } 406 checkWritePermissions(args); 407 408 // Special case LOCATION_PROVIDERS_ALLOWED. 409 // Support enabling/disabling a single provider (using "+" or "-" prefix) 410 String name = initialValues.getAsString(Settings.Secure.NAME); 411 if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) { 412 if (!parseProviderList(url, initialValues)) return null; 413 } 414 415 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 416 final long rowId = db.insert(args.table, null, initialValues); 417 if (rowId <= 0) return null; 418 419 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues); 420 url = getUriFor(url, initialValues, rowId); 421 sendNotify(url); 422 return url; 423 } 424 425 @Override 426 public int delete(Uri url, String where, String[] whereArgs) { 427 SqlArguments args = new SqlArguments(url, where, whereArgs); 428 if (TABLE_FAVORITES.equals(args.table)) { 429 return 0; 430 } else if (TABLE_OLD_FAVORITES.equals(args.table)) { 431 args.table = TABLE_FAVORITES; 432 } 433 checkWritePermissions(args); 434 435 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 436 int count = db.delete(args.table, args.where, args.args); 437 if (count > 0) sendNotify(url); 438 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted"); 439 return count; 440 } 441 442 @Override 443 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) { 444 SqlArguments args = new SqlArguments(url, where, whereArgs); 445 if (TABLE_FAVORITES.equals(args.table)) { 446 return 0; 447 } 448 checkWritePermissions(args); 449 450 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 451 int count = db.update(args.table, initialValues, args.where, args.args); 452 if (count > 0) sendNotify(url); 453 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues); 454 return count; 455 } 456 457 @Override 458 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 459 460 /* 461 * When a client attempts to openFile the default ringtone or 462 * notification setting Uri, we will proxy the call to the current 463 * default ringtone's Uri (if it is in the DRM or media provider). 464 */ 465 int ringtoneType = RingtoneManager.getDefaultType(uri); 466 // Above call returns -1 if the Uri doesn't match a default type 467 if (ringtoneType != -1) { 468 Context context = getContext(); 469 470 // Get the current value for the default sound 471 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 472 473 if (soundUri != null) { 474 // Only proxy the openFile call to drm or media providers 475 String authority = soundUri.getAuthority(); 476 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY); 477 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) { 478 479 if (isDrmAuthority) { 480 try { 481 // Check DRM access permission here, since once we 482 // do the below call the DRM will be checking our 483 // permission, not our caller's permission 484 DrmStore.enforceAccessDrmPermission(context); 485 } catch (SecurityException e) { 486 throw new FileNotFoundException(e.getMessage()); 487 } 488 } 489 490 return context.getContentResolver().openFileDescriptor(soundUri, mode); 491 } 492 } 493 } 494 495 return super.openFile(uri, mode); 496 } 497 498 @Override 499 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 500 501 /* 502 * When a client attempts to openFile the default ringtone or 503 * notification setting Uri, we will proxy the call to the current 504 * default ringtone's Uri (if it is in the DRM or media provider). 505 */ 506 int ringtoneType = RingtoneManager.getDefaultType(uri); 507 // Above call returns -1 if the Uri doesn't match a default type 508 if (ringtoneType != -1) { 509 Context context = getContext(); 510 511 // Get the current value for the default sound 512 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType); 513 514 if (soundUri != null) { 515 // Only proxy the openFile call to drm or media providers 516 String authority = soundUri.getAuthority(); 517 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY); 518 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) { 519 520 if (isDrmAuthority) { 521 try { 522 // Check DRM access permission here, since once we 523 // do the below call the DRM will be checking our 524 // permission, not our caller's permission 525 DrmStore.enforceAccessDrmPermission(context); 526 } catch (SecurityException e) { 527 throw new FileNotFoundException(e.getMessage()); 528 } 529 } 530 531 ParcelFileDescriptor pfd = null; 532 try { 533 pfd = context.getContentResolver().openFileDescriptor(soundUri, mode); 534 return new AssetFileDescriptor(pfd, 0, -1); 535 } catch (FileNotFoundException ex) { 536 // fall through and open the fallback ringtone below 537 } 538 } 539 540 try { 541 return super.openAssetFile(soundUri, mode); 542 } catch (FileNotFoundException ex) { 543 // Since a non-null Uri was specified, but couldn't be opened, 544 // fall back to the built-in ringtone. 545 return context.getResources().openRawResourceFd( 546 com.android.internal.R.raw.fallbackring); 547 } 548 } 549 // no need to fall through and have openFile() try again, since we 550 // already know that will fail. 551 throw new FileNotFoundException(); // or return null ? 552 } 553 554 // Note that this will end up calling openFile() above. 555 return super.openAssetFile(uri, mode); 556 } 557} 558