blob: 18a69c28557ba54e7a8e82f315ae7af25d14c6ff [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
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) {
Brett Chabot16dd82c2009-06-18 17:00:48 -0700161 throw new SecurityException(
162 String.format("Permission denial: writing to secure settings requires $1%s",
163 android.Manifest.permission.WRITE_SECURE_SETTINGS));
164
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800165 // TODO: Move gservices into its own provider so we don't need this nonsense.
166 } else if ("gservices".equals(args.table) &&
167 getContext().checkCallingOrSelfPermission(
168 android.Manifest.permission.WRITE_GSERVICES) !=
169 PackageManager.PERMISSION_GRANTED) {
Brett Chabot16dd82c2009-06-18 17:00:48 -0700170 throw new SecurityException(
171 String.format("Permission denial: writing to gservices settings requires $1%s",
172 android.Manifest.permission.WRITE_GSERVICES));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173 }
174 }
175
176 @Override
177 public boolean onCreate() {
178 mOpenHelper = new DatabaseHelper(getContext());
179 return true;
180 }
181
182 @Override
183 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
184 SqlArguments args = new SqlArguments(url, where, whereArgs);
185 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
186
187 // The favorites table was moved from this provider to a provider inside Home
188 // Home still need to query this table to upgrade from pre-cupcake builds
189 // However, a cupcake+ build with no data does not contain this table which will
190 // cause an exception in the SQL stack. The following line is a special case to
191 // let the caller of the query have a chance to recover and avoid the exception
192 if (TABLE_FAVORITES.equals(args.table)) {
193 return null;
194 } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
195 args.table = TABLE_FAVORITES;
196 Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null);
197 if (cursor != null) {
198 boolean exists = cursor.getCount() > 0;
199 cursor.close();
200 if (!exists) return null;
201 } else {
202 return null;
203 }
204 }
205
206 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
207 qb.setTables(args.table);
208
209 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
210 ret.setNotificationUri(getContext().getContentResolver(), url);
211 return ret;
212 }
213
214 @Override
215 public String getType(Uri url) {
216 // If SqlArguments supplies a where clause, then it must be an item
217 // (because we aren't supplying our own where clause).
218 SqlArguments args = new SqlArguments(url, null, null);
219 if (TextUtils.isEmpty(args.where)) {
220 return "vnd.android.cursor.dir/" + args.table;
221 } else {
222 return "vnd.android.cursor.item/" + args.table;
223 }
224 }
225
226 @Override
227 public int bulkInsert(Uri uri, ContentValues[] values) {
228 SqlArguments args = new SqlArguments(uri);
229 if (TABLE_FAVORITES.equals(args.table)) {
230 return 0;
231 }
232 checkWritePermissions(args);
233
234 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
235 db.beginTransaction();
236 try {
237 int numValues = values.length;
238 for (int i = 0; i < numValues; i++) {
239 if (db.insert(args.table, null, values[i]) < 0) return 0;
240 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
241 }
242 db.setTransactionSuccessful();
243 } finally {
244 db.endTransaction();
245 }
246
247 sendNotify(uri);
248 return values.length;
249 }
250
Mike Lockwood9637d472009-04-02 21:41:57 -0700251 /*
252 * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
253 * This setting contains a list of the currently enabled location providers.
254 * But helper functions in android.providers.Settings can enable or disable
255 * a single provider by using a "+" or "-" prefix before the provider name.
256 */
257 private boolean parseProviderList(Uri url, ContentValues initialValues) {
258 String value = initialValues.getAsString(Settings.Secure.VALUE);
259 String newProviders = null;
260 if (value != null && value.length() > 1) {
261 char prefix = value.charAt(0);
262 if (prefix == '+' || prefix == '-') {
263 // skip prefix
264 value = value.substring(1);
265
266 // read list of enabled providers into "providers"
267 String providers = "";
268 String[] columns = {Settings.Secure.VALUE};
269 String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'";
270 Cursor cursor = query(url, columns, where, null, null);
271 if (cursor != null && cursor.getCount() == 1) {
272 try {
273 cursor.moveToFirst();
274 providers = cursor.getString(0);
275 } finally {
276 cursor.close();
277 }
278 }
279
280 int index = providers.indexOf(value);
281 int end = index + value.length();
282 // check for commas to avoid matching on partial string
283 if (index > 0 && providers.charAt(index - 1) != ',') index = -1;
284 if (end < providers.length() && providers.charAt(end) != ',') index = -1;
285
286 if (prefix == '+' && index < 0) {
287 // append the provider to the list if not present
288 if (providers.length() == 0) {
289 newProviders = value;
290 } else {
291 newProviders = providers + ',' + value;
292 }
293 } else if (prefix == '-' && index >= 0) {
294 // remove the provider from the list if present
295 // remove leading and trailing commas
296 if (index > 0) index--;
297 if (end < providers.length()) end++;
298
299 newProviders = providers.substring(0, index);
300 if (end < providers.length()) {
301 newProviders += providers.substring(end);
302 }
303 } else {
304 // nothing changed, so no need to update the database
305 return false;
306 }
307
308 if (newProviders != null) {
309 initialValues.put(Settings.Secure.VALUE, newProviders);
310 }
311 }
312 }
313
314 return true;
315 }
316
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800317 @Override
318 public Uri insert(Uri url, ContentValues initialValues) {
319 SqlArguments args = new SqlArguments(url);
320 if (TABLE_FAVORITES.equals(args.table)) {
321 return null;
322 }
323 checkWritePermissions(args);
324
Mike Lockwood9637d472009-04-02 21:41:57 -0700325 // Special case LOCATION_PROVIDERS_ALLOWED.
326 // Support enabling/disabling a single provider (using "+" or "-" prefix)
327 String name = initialValues.getAsString(Settings.Secure.NAME);
328 if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
329 if (!parseProviderList(url, initialValues)) return null;
330 }
331
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800332 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
333 final long rowId = db.insert(args.table, null, initialValues);
334 if (rowId <= 0) return null;
335
336 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues);
337 url = getUriFor(url, initialValues, rowId);
338 sendNotify(url);
339 return url;
340 }
341
342 @Override
343 public int delete(Uri url, String where, String[] whereArgs) {
344 SqlArguments args = new SqlArguments(url, where, whereArgs);
345 if (TABLE_FAVORITES.equals(args.table)) {
346 return 0;
347 } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
348 args.table = TABLE_FAVORITES;
349 }
350 checkWritePermissions(args);
351
352 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
353 int count = db.delete(args.table, args.where, args.args);
354 if (count > 0) sendNotify(url);
355 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
356 return count;
357 }
358
359 @Override
360 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
361 SqlArguments args = new SqlArguments(url, where, whereArgs);
362 if (TABLE_FAVORITES.equals(args.table)) {
363 return 0;
364 }
365 checkWritePermissions(args);
366
367 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
368 int count = db.update(args.table, initialValues, args.where, args.args);
369 if (count > 0) sendNotify(url);
370 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
371 return count;
372 }
373
374 @Override
375 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
376
377 /*
378 * When a client attempts to openFile the default ringtone or
379 * notification setting Uri, we will proxy the call to the current
380 * default ringtone's Uri (if it is in the DRM or media provider).
381 */
382 int ringtoneType = RingtoneManager.getDefaultType(uri);
383 // Above call returns -1 if the Uri doesn't match a default type
384 if (ringtoneType != -1) {
385 Context context = getContext();
386
387 // Get the current value for the default sound
388 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
389 if (soundUri == null) {
390 // Fallback on any valid ringtone Uri
391 soundUri = RingtoneManager.getValidRingtoneUri(context);
392 }
393
394 if (soundUri != null) {
395 // Only proxy the openFile call to drm or media providers
396 String authority = soundUri.getAuthority();
397 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY);
398 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) {
399
400 if (isDrmAuthority) {
401 try {
402 // Check DRM access permission here, since once we
403 // do the below call the DRM will be checking our
404 // permission, not our caller's permission
405 DrmStore.enforceAccessDrmPermission(context);
406 } catch (SecurityException e) {
407 throw new FileNotFoundException(e.getMessage());
408 }
409 }
410
411 return context.getContentResolver().openFileDescriptor(soundUri, mode);
412 }
413 }
414 }
415
416 return super.openFile(uri, mode);
417 }
418}