blob: c8a3cce12003d54a026b57a70c8e3f3f7c8a0574 [file] [log] [blame]
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001/*
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 WRITE_GSERVICES_PERMISSION = "android.permission.WRITE_GSERVICES";
44
45 private DatabaseHelper mOpenHelper;
46
47 /**
48 * Decode a content URL into the table, projection, and arguments
49 * used to access the corresponding database rows.
50 */
51 private static class SqlArguments {
52 public final String table;
53 public final String where;
54 public final String[] args;
55
56 /** Operate on existing rows. */
57 SqlArguments(Uri url, String where, String[] args) {
58 if (url.getPathSegments().size() == 1) {
59 this.table = url.getPathSegments().get(0);
60 this.where = where;
61 this.args = args;
62 } else if (url.getPathSegments().size() != 2) {
63 throw new IllegalArgumentException("Invalid URI: " + url);
64 } else if (!TextUtils.isEmpty(where)) {
65 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
66 } else {
67 this.table = url.getPathSegments().get(0);
68 if ("gservices".equals(this.table) || "system".equals(this.table)) {
69 this.where = Settings.NameValueTable.NAME + "=?";
70 this.args = new String[] { url.getPathSegments().get(1) };
71 } else {
72 this.where = "_id=" + ContentUris.parseId(url);
73 this.args = null;
74 }
75 }
76 }
77
78 /** Insert new rows (no where clause allowed). */
79 SqlArguments(Uri url) {
80 if (url.getPathSegments().size() == 1) {
81 this.table = url.getPathSegments().get(0);
82 this.where = null;
83 this.args = null;
84 } else {
85 throw new IllegalArgumentException("Invalid URI: " + url);
86 }
87 }
88 }
89
90 /**
91 * Get the content URI of a row added to a table.
92 * @param tableUri of the entire table
93 * @param values found in the row
94 * @param rowId of the row
95 * @return the content URI for this particular row
96 */
97 private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) {
98 if (tableUri.getPathSegments().size() != 1) {
99 throw new IllegalArgumentException("Invalid URI: " + tableUri);
100 }
101 String table = tableUri.getPathSegments().get(0);
102 if ("gservices".equals(table) || "system".equals(table)) {
103 String name = values.getAsString(Settings.NameValueTable.NAME);
104 return Uri.withAppendedPath(tableUri, name);
105 } else {
106 return ContentUris.withAppendedId(tableUri, rowId);
107 }
108 }
109
110 /**
111 * Send a notification when a particular content URI changes.
112 * Modify the system property used to communicate the version of
113 * this table, for tables which have such a property. (The Settings
114 * contract class uses these to provide client-side caches.)
115 * @param uri to send notifications for
116 */
117 private void sendNotify(Uri uri) {
118 // Update the system property *first*, so if someone is listening for
119 // a notification and then using the contract class to get their data,
120 // the system property will be updated and they'll get the new data.
121
122 String property = null, table = uri.getPathSegments().get(0);
123 if (table.equals("system")) {
124 property = Settings.System.SYS_PROP_SETTING_VERSION;
125 } else if (table.equals("gservices")) {
126 property = Settings.Gservices.SYS_PROP_SETTING_VERSION;
127 }
128
129 if (property != null) {
130 long version = SystemProperties.getLong(property, 0) + 1;
131 if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version);
132 SystemProperties.set(property, Long.toString(version));
133 }
134
135 // Now send the notification through the content framework.
136
137 String notify = uri.getQueryParameter("notify");
138 if (notify == null || "true".equals(notify)) {
139 getContext().getContentResolver().notifyChange(uri, null);
140 if (LOCAL_LOGV) Log.v(TAG, "notifying: " + uri);
141 } else {
142 if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri);
143 }
144 }
145
146 /**
147 * Make sure the caller has permission to write this data.
148 * @param args supplied by the caller
149 * @throws SecurityException if the caller is forbidden to write.
150 */
151 private void checkWritePermissions(SqlArguments args) {
152 // TODO: Move gservices into its own provider so we don't need this nonsense.
153 if ("gservices".equals(args.table) &&
154 getContext().checkCallingOrSelfPermission(WRITE_GSERVICES_PERMISSION) !=
155 PackageManager.PERMISSION_GRANTED) {
156 throw new SecurityException("Cannot write gservices table");
157 }
158 }
159
160 @Override
161 public boolean onCreate() {
162 mOpenHelper = new DatabaseHelper(getContext());
163 return true;
164 }
165
166 @Override
167 public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
168 SqlArguments args = new SqlArguments(url, where, whereArgs);
169 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
170 qb.setTables(args.table);
171
172 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
173 Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
174 ret.setNotificationUri(getContext().getContentResolver(), url);
175 return ret;
176 }
177
178 @Override
179 public String getType(Uri url) {
180 // If SqlArguments supplies a where clause, then it must be an item
181 // (because we aren't supplying our own where clause).
182 SqlArguments args = new SqlArguments(url, null, null);
183 if (TextUtils.isEmpty(args.where)) {
184 return "vnd.android.cursor.dir/" + args.table;
185 } else {
186 return "vnd.android.cursor.item/" + args.table;
187 }
188 }
189
190 @Override
191 public int bulkInsert(Uri uri, ContentValues[] values) {
192 SqlArguments args = new SqlArguments(uri);
193 checkWritePermissions(args);
194
195 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
196 db.beginTransaction();
197 try {
198 int numValues = values.length;
199 for (int i = 0; i < numValues; i++) {
200 if (db.insert(args.table, null, values[i]) < 0) return 0;
201 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
202 }
203 db.setTransactionSuccessful();
204 } finally {
205 db.endTransaction();
206 }
207
208 sendNotify(uri);
209 return values.length;
210 }
211
212 @Override
213 public Uri insert(Uri url, ContentValues initialValues) {
214 SqlArguments args = new SqlArguments(url);
215 checkWritePermissions(args);
216
217 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
218 final long rowId = db.insert(args.table, null, initialValues);
219 if (rowId <= 0) return null;
220
221 if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues);
222 url = getUriFor(url, initialValues, rowId);
223 sendNotify(url);
224 return url;
225 }
226
227 @Override
228 public int delete(Uri url, String where, String[] whereArgs) {
229 SqlArguments args = new SqlArguments(url, where, whereArgs);
230 checkWritePermissions(args);
231
232 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
233 int count = db.delete(args.table, args.where, args.args);
234 if (count > 0) sendNotify(url);
235 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
236 return count;
237 }
238
239 @Override
240 public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
241 SqlArguments args = new SqlArguments(url, where, whereArgs);
242 checkWritePermissions(args);
243
244 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
245 int count = db.update(args.table, initialValues, args.where, args.args);
246 if (count > 0) sendNotify(url);
247 if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
248 return count;
249 }
250
251 @Override
252 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
253
254 /*
255 * When a client attempts to openFile the default ringtone or
256 * notification setting Uri, we will proxy the call to the current
257 * default ringtone's Uri (if it is in the DRM or media provider).
258 */
259 int ringtoneType = RingtoneManager.getDefaultType(uri);
260 // Above call returns -1 if the Uri doesn't match a default type
261 if (ringtoneType != -1) {
262 Context context = getContext();
263
264 // Get the current value for the default sound
265 Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
266 if (soundUri == null) {
267 // Fallback on any valid ringtone Uri
268 soundUri = RingtoneManager.getValidRingtoneUri(context);
269 }
270
271 if (soundUri != null) {
272 // Only proxy the openFile call to drm or media providers
273 String authority = soundUri.getAuthority();
274 boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY);
275 if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) {
276
277 if (isDrmAuthority) {
278 try {
279 // Check DRM access permission here, since once we
280 // do the below call the DRM will be checking our
281 // permission, not our caller's permission
282 DrmStore.enforceAccessDrmPermission(context);
283 } catch (SecurityException e) {
284 throw new FileNotFoundException(e.getMessage());
285 }
286 }
287
288 return context.getContentResolver().openFileDescriptor(soundUri, mode);
289 }
290 }
291 }
292
293 return super.openFile(uri, mode);
294 }
295}