blob: c0de9a5b00a095371def577fb518b67d3782654d [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Amith Yamasani220f4d62009-07-02 02:34:14 -070019import java.io.FileNotFoundException;
20
Amith Yamasani8823c0a82009-07-07 14:30:17 -070021import android.backup.BackupManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.content.ContentProvider;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.pm.PackageManager;
27import android.database.Cursor;
28import android.database.sqlite.SQLiteDatabase;
29import android.database.sqlite.SQLiteQueryBuilder;
30import android.media.RingtoneManager;
31import android.net.Uri;
32import android.os.ParcelFileDescriptor;
Amith Yamasani220f4d62009-07-02 02:34:14 -070033import android.os.ServiceManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034import android.os.SystemProperties;
35import android.provider.DrmStore;
36import android.provider.MediaStore;
37import android.provider.Settings;
38import android.text.TextUtils;
39import android.util.Log;
40
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041public class SettingsProvider extends ContentProvider {
42 private static final String TAG = "SettingsProvider";
43 private static final boolean LOCAL_LOGV = false;
44
45 private static final String TABLE_FAVORITES = "favorites";
46 private static final String TABLE_OLD_FAVORITES = "old_favorites";
47
48 private DatabaseHelper mOpenHelper;
Amith Yamasani8823c0a82009-07-07 14:30:17 -070049
50 private BackupManager mBackupManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051
52 /**
53 * Decode a content URL into the table, projection, and arguments
54 * used to access the corresponding database rows.
55 */
56 private static class SqlArguments {
57 public String table;
58 public final String where;
59 public final String[] args;
60
61 /** Operate on existing rows. */
62 SqlArguments(Uri url, String where, String[] args) {
63 if (url.getPathSegments().size() == 1) {
64 this.table = url.getPathSegments().get(0);
65 this.where = where;
66 this.args = args;
67 } else if (url.getPathSegments().size() != 2) {
68 throw new IllegalArgumentException("Invalid URI: " + url);
69 } else if (!TextUtils.isEmpty(where)) {
70 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
71 } else {
72 this.table = url.getPathSegments().get(0);
73 if ("gservices".equals(this.table) || "system".equals(this.table)
74 || "secure".equals(this.table)) {
75 this.where = Settings.NameValueTable.NAME + "=?";
76 this.args = new String[] { url.getPathSegments().get(1) };
77 } else {
78 this.where = "_id=" + ContentUris.parseId(url);
79 this.args = null;
80 }
81 }
82 }
83
84 /** Insert new rows (no where clause allowed). */
85 SqlArguments(Uri url) {
86 if (url.getPathSegments().size() == 1) {
87 this.table = url.getPathSegments().get(0);
88 this.where = null;
89 this.args = null;
90 } else {
91 throw new IllegalArgumentException("Invalid URI: " + url);
92 }
93 }
94 }
95
96 /**
97 * Get the content URI of a row added to a table.
98 * @param tableUri of the entire table
99 * @param values found in the row
100 * @param rowId of the row
101 * @return the content URI for this particular row
102 */
103 private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) {
104 if (tableUri.getPathSegments().size() != 1) {
105 throw new IllegalArgumentException("Invalid URI: " + tableUri);
106 }
107 String table = tableUri.getPathSegments().get(0);
108 if ("gservices".equals(table) || "system".equals(table)
109 || "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
Amith Yamasanid1582142009-07-08 20:04:55 -0700129 boolean backedUpDataChanged = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800130 String property = null, table = uri.getPathSegments().get(0);
131 if (table.equals("system")) {
132 property = Settings.System.SYS_PROP_SETTING_VERSION;
Amith Yamasanid1582142009-07-08 20:04:55 -0700133 backedUpDataChanged = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800134 } else if (table.equals("secure")) {
135 property = Settings.Secure.SYS_PROP_SETTING_VERSION;
Amith Yamasanid1582142009-07-08 20:04:55 -0700136 backedUpDataChanged = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800137 } else if (table.equals("gservices")) {
138 property = Settings.Gservices.SYS_PROP_SETTING_VERSION;
139 }
140
141 if (property != null) {
142 long version = SystemProperties.getLong(property, 0) + 1;
143 if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version);
144 SystemProperties.set(property, Long.toString(version));
145 }
146
Amith Yamasani220f4d62009-07-02 02:34:14 -0700147 // Inform the backup manager about a data change
Amith Yamasanid1582142009-07-08 20:04:55 -0700148 if (backedUpDataChanged) {
149 mBackupManager.dataChanged();
150 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800151 // Now send the notification through the content framework.
152
153 String notify = uri.getQueryParameter("notify");
154 if (notify == null || "true".equals(notify)) {
155 getContext().getContentResolver().notifyChange(uri, null);
156 if (LOCAL_LOGV) Log.v(TAG, "notifying: " + uri);
157 } else {
158 if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri);
159 }
160 }
161
162 /**
163 * Make sure the caller has permission to write this data.
164 * @param args supplied by the caller
165 * @throws SecurityException if the caller is forbidden to write.
166 */
167 private void checkWritePermissions(SqlArguments args) {
168 if ("secure".equals(args.table) &&
169 getContext().checkCallingOrSelfPermission(
170 android.Manifest.permission.WRITE_SECURE_SETTINGS) !=
171 PackageManager.PERMISSION_GRANTED) {
Brett Chabot16dd82c2009-06-18 17:00:48 -0700172 throw new SecurityException(
Brett Chabot31a88fa2009-06-18 20:44:42 -0700173 String.format("Permission denial: writing to secure settings requires %1$s",
Brett Chabot16dd82c2009-06-18 17:00:48 -0700174 android.Manifest.permission.WRITE_SECURE_SETTINGS));
175
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800176 // TODO: Move gservices into its own provider so we don't need this nonsense.
177 } else if ("gservices".equals(args.table) &&
178 getContext().checkCallingOrSelfPermission(
179 android.Manifest.permission.WRITE_GSERVICES) !=
180 PackageManager.PERMISSION_GRANTED) {
Brett Chabot16dd82c2009-06-18 17:00:48 -0700181 throw new SecurityException(
Brett Chabot31a88fa2009-06-18 20:44:42 -0700182 String.format("Permission denial: writing to gservices settings requires %1$s",
Brett Chabot16dd82c2009-06-18 17:00:48 -0700183 android.Manifest.permission.WRITE_GSERVICES));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800184 }
185 }
186
187 @Override
188 public boolean onCreate() {
189 mOpenHelper = new DatabaseHelper(getContext());
Amith Yamasani8823c0a82009-07-07 14:30:17 -0700190 mBackupManager = new BackupManager(getContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800191 return true;
192 }
193
194 @Override
195 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
196 SqlArguments args = new SqlArguments(url, where, whereArgs);
197 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
198
199 // The favorites table was moved from this provider to a provider inside Home
200 // Home still need to query this table to upgrade from pre-cupcake builds
201 // However, a cupcake+ build with no data does not contain this table which will
202 // cause an exception in the SQL stack. The following line is a special case to
203 // let the caller of the query have a chance to recover and avoid the exception
204 if (TABLE_FAVORITES.equals(args.table)) {
205 return null;
206 } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
207 args.table = TABLE_FAVORITES;
208 Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null);
209 if (cursor != null) {
210 boolean exists = cursor.getCount() > 0;
211 cursor.close();
212 if (!exists) return null;
213 } else {
214 return null;
215 }
216 }
217
218 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
219 qb.setTables(args.table);
220
221 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
222 ret.setNotificationUri(getContext().getContentResolver(), url);
223 return ret;
224 }
225
226 @Override
227 public String getType(Uri url) {
228 // If SqlArguments supplies a where clause, then it must be an item
229 // (because we aren't supplying our own where clause).
230 SqlArguments args = new SqlArguments(url, null, null);
231 if (TextUtils.isEmpty(args.where)) {
232 return "vnd.android.cursor.dir/" + args.table;
233 } else {
234 return "vnd.android.cursor.item/" + args.table;
235 }
236 }
237
238 @Override
239 public int bulkInsert(Uri uri, ContentValues[] values) {
240 SqlArguments args = new SqlArguments(uri);
241 if (TABLE_FAVORITES.equals(args.table)) {
242 return 0;
243 }
244 checkWritePermissions(args);
245
246 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
247 db.beginTransaction();
248 try {
249 int numValues = values.length;
250 for (int i = 0; i < numValues; i++) {
251 if (db.insert(args.table, null, values[i]) < 0) return 0;
252 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
253 }
254 db.setTransactionSuccessful();
255 } finally {
256 db.endTransaction();
257 }
258
259 sendNotify(uri);
260 return values.length;
261 }
262
Mike Lockwood9637d472009-04-02 21:41:57 -0700263 /*
264 * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
265 * This setting contains a list of the currently enabled location providers.
266 * But helper functions in android.providers.Settings can enable or disable
267 * a single provider by using a "+" or "-" prefix before the provider name.
268 */
269 private boolean parseProviderList(Uri url, ContentValues initialValues) {
270 String value = initialValues.getAsString(Settings.Secure.VALUE);
271 String newProviders = null;
272 if (value != null && value.length() > 1) {
273 char prefix = value.charAt(0);
274 if (prefix == '+' || prefix == '-') {
275 // skip prefix
276 value = value.substring(1);
277
278 // read list of enabled providers into "providers"
279 String providers = "";
280 String[] columns = {Settings.Secure.VALUE};
281 String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'";
282 Cursor cursor = query(url, columns, where, null, null);
283 if (cursor != null && cursor.getCount() == 1) {
284 try {
285 cursor.moveToFirst();
286 providers = cursor.getString(0);
287 } finally {
288 cursor.close();
289 }
290 }
291
292 int index = providers.indexOf(value);
293 int end = index + value.length();
294 // check for commas to avoid matching on partial string
295 if (index > 0 && providers.charAt(index - 1) != ',') index = -1;
296 if (end < providers.length() && providers.charAt(end) != ',') index = -1;
297
298 if (prefix == '+' && index < 0) {
299 // append the provider to the list if not present
300 if (providers.length() == 0) {
301 newProviders = value;
302 } else {
303 newProviders = providers + ',' + value;
304 }
305 } else if (prefix == '-' && index >= 0) {
306 // remove the provider from the list if present
307 // remove leading and trailing commas
308 if (index > 0) index--;
309 if (end < providers.length()) end++;
310
311 newProviders = providers.substring(0, index);
312 if (end < providers.length()) {
313 newProviders += providers.substring(end);
314 }
315 } else {
316 // nothing changed, so no need to update the database
317 return false;
318 }
319
320 if (newProviders != null) {
321 initialValues.put(Settings.Secure.VALUE, newProviders);
322 }
323 }
324 }
325
326 return true;
327 }
328
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800329 @Override
330 public Uri insert(Uri url, ContentValues initialValues) {
331 SqlArguments args = new SqlArguments(url);
332 if (TABLE_FAVORITES.equals(args.table)) {
333 return null;
334 }
335 checkWritePermissions(args);
336
Mike Lockwood9637d472009-04-02 21:41:57 -0700337 // Special case LOCATION_PROVIDERS_ALLOWED.
338 // Support enabling/disabling a single provider (using "+" or "-" prefix)
339 String name = initialValues.getAsString(Settings.Secure.NAME);
340 if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
341 if (!parseProviderList(url, initialValues)) return null;
342 }
343
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800344 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
345 final long rowId = db.insert(args.table, null, initialValues);
346 if (rowId <= 0) return null;
347
348 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues);
349 url = getUriFor(url, initialValues, rowId);
350 sendNotify(url);
351 return url;
352 }
353
354 @Override
355 public int delete(Uri url, String where, String[] whereArgs) {
356 SqlArguments args = new SqlArguments(url, where, whereArgs);
357 if (TABLE_FAVORITES.equals(args.table)) {
358 return 0;
359 } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
360 args.table = TABLE_FAVORITES;
361 }
362 checkWritePermissions(args);
363
364 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
365 int count = db.delete(args.table, args.where, args.args);
366 if (count > 0) sendNotify(url);
367 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
368 return count;
369 }
370
371 @Override
372 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
373 SqlArguments args = new SqlArguments(url, where, whereArgs);
374 if (TABLE_FAVORITES.equals(args.table)) {
375 return 0;
376 }
377 checkWritePermissions(args);
378
379 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
380 int count = db.update(args.table, initialValues, args.where, args.args);
381 if (count > 0) sendNotify(url);
382 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
383 return count;
384 }
385
386 @Override
387 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
388
389 /*
390 * When a client attempts to openFile the default ringtone or
391 * notification setting Uri, we will proxy the call to the current
392 * default ringtone's Uri (if it is in the DRM or media provider).
393 */
394 int ringtoneType = RingtoneManager.getDefaultType(uri);
395 // Above call returns -1 if the Uri doesn't match a default type
396 if (ringtoneType != -1) {
397 Context context = getContext();
398
399 // Get the current value for the default sound
400 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
401 if (soundUri == null) {
402 // Fallback on any valid ringtone Uri
403 soundUri = RingtoneManager.getValidRingtoneUri(context);
404 }
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}