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