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