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