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