1 /* 2 * Copyright (C) 2008 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 17 package com.android.providers.drm; 18 19 import android.content.*; 20 import android.content.pm.PackageManager; 21 import android.database.Cursor; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.database.sqlite.SQLiteException; 24 import android.database.sqlite.SQLiteOpenHelper; 25 import android.database.sqlite.SQLiteQueryBuilder; 26 import android.database.sqlite.SQLiteStatement; 27 import android.net.Uri; 28 import android.os.ParcelFileDescriptor; 29 import android.provider.DrmStore; 30 import android.provider.OpenableColumns; 31 import android.text.TextUtils; 32 import android.util.Log; 33 34 import java.io.File; 35 import java.io.FileInputStream; 36 import java.io.FileNotFoundException; 37 import java.io.FileOutputStream; 38 import java.io.InputStream; 39 import java.io.IOException; 40 import java.util.HashMap; 41 42 /** 43 * Drm content provider. See {@link android.provider.DrmStore} for details. 44 * 45 * @hide 46 */ 47 public class DrmProvider extends ContentProvider 48 { 49 /** 50 * Creates and updated database on demand when opening it. 51 * Helper class to create database the first time the provider is 52 * initialized and upgrade it when a new version of the provider needs 53 * an updated version of the database. 54 */ 55 private final class OpenDatabaseHelper extends SQLiteOpenHelper { 56 private static final String DATABASE_NAME = "drm.db"; 57 private static final int DATABASE_VERSION = 1; 58 OpenDatabaseHelper(Context context)59 OpenDatabaseHelper(Context context) { 60 super(context, DATABASE_NAME, null, DATABASE_VERSION); 61 } 62 63 /** 64 * Creates database the first time we try to open it. 65 */ 66 @Override onCreate(final SQLiteDatabase db)67 public void onCreate(final SQLiteDatabase db) { 68 createTables(db); 69 } 70 71 /** 72 * Checks data integrity when opening the database 73 */ 74 @Override onOpen(final SQLiteDatabase db)75 public void onOpen(final SQLiteDatabase db) { 76 super.onOpen(db); 77 // TODO: validate and/or clean up database in onOpen - should that 78 // be done in the service instead? 79 } 80 81 /** 82 * Updates the database format when a new content provider is used 83 * with an older database format. 84 */ 85 // For now, just deletes the database. 86 // TODO: decide on a general policy when updates become relevant 87 // TODO: can there even be a downgrade? how can it be handled? 88 // TODO: delete temporary files 89 @Override onUpgrade(final SQLiteDatabase db, final int oldV, final int newV)90 public void onUpgrade(final SQLiteDatabase db, 91 final int oldV, final int newV) { 92 Log.i(TAG, "Upgrading downloads database from version " + 93 oldV+ " to " + newV + 94 ", which will destroy all old data"); 95 dropTables(db); 96 createTables(db); 97 } 98 } 99 100 @Override onCreate()101 public boolean onCreate() 102 { 103 mOpenHelper = new OpenDatabaseHelper(getContext()); 104 return true; 105 } 106 107 /** 108 * Creates the table that'll hold the download information. 109 */ createTables(SQLiteDatabase db)110 private void createTables(SQLiteDatabase db) { 111 db.execSQL("CREATE TABLE audio (" + 112 "_id INTEGER PRIMARY KEY," + 113 "_data TEXT," + 114 "_size INTEGER," + 115 "title TEXT," + 116 "mime_type TEXT" + 117 ");"); 118 119 db.execSQL("CREATE TABLE images (" + 120 "_id INTEGER PRIMARY KEY," + 121 "_data TEXT," + 122 "_size INTEGER," + 123 "title TEXT," + 124 "mime_type TEXT" + 125 ");"); 126 } 127 128 /** 129 * Deletes the table that holds the download information. 130 */ dropTables(SQLiteDatabase db)131 private void dropTables(SQLiteDatabase db) { 132 // TODO: error handling 133 db.execSQL("DROP TABLE IF EXISTS audio"); 134 db.execSQL("DROP TABLE IF EXISTS images"); 135 } 136 137 @Override query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sort)138 public Cursor query(Uri uri, String[] projectionIn, String selection, 139 String[] selectionArgs, String sort) { 140 String groupBy = null; 141 142 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 143 144 switch (URI_MATCHER.match(uri)) { 145 case AUDIO: 146 qb.setTables("audio"); 147 break; 148 149 case AUDIO_ID: 150 qb.setTables("audio"); 151 qb.appendWhere("_id=" + uri.getPathSegments().get(1)); 152 break; 153 154 case IMAGES: 155 qb.setTables("images"); 156 break; 157 158 case IMAGES_ID: 159 qb.setTables("images"); 160 qb.appendWhere("_id=" + uri.getPathSegments().get(1)); 161 break; 162 163 default: 164 throw new IllegalStateException("Unknown URL: " + uri.toString()); 165 } 166 167 if (projectionIn != null) { 168 for (int i = 0; i < projectionIn.length; i++) { 169 if (projectionIn[i].equals(OpenableColumns.DISPLAY_NAME)) { 170 projectionIn[i] = "title AS " + OpenableColumns.DISPLAY_NAME; 171 } 172 } 173 } 174 175 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 176 Cursor c = qb.query(db, projectionIn, selection, 177 selectionArgs, groupBy, null, sort); 178 if (c != null) { 179 c.setNotificationUri(getContext().getContentResolver(), uri); 180 } 181 return c; 182 } 183 184 @Override getType(Uri url)185 public String getType(Uri url) 186 { 187 switch (URI_MATCHER.match(url)) { 188 case AUDIO_ID: 189 case IMAGES_ID: 190 Cursor c = query(url, MIME_TYPE_PROJECTION, null, null, null); 191 if (c != null && c.getCount() == 1) { 192 c.moveToFirst(); 193 String mimeType = c.getString(1); 194 c.deactivate(); 195 return mimeType; 196 } 197 break; 198 } 199 throw new IllegalStateException("Unknown URL"); 200 } 201 202 /** 203 * Ensures there is a file in the _data column of values, if one isn't 204 * present a new file is created. 205 * 206 * @param initialValues the values passed to insert by the caller 207 * @return the new values 208 */ ensureFile(ContentValues initialValues)209 private ContentValues ensureFile(ContentValues initialValues) { 210 try { 211 File parent = getContext().getFilesDir(); 212 parent.mkdirs(); 213 File file = File.createTempFile("DRM-", ".data", parent); 214 ContentValues values = new ContentValues(initialValues); 215 values.put("_data", file.toString()); 216 return values; 217 } catch (IOException e) { 218 Log.e(TAG, "Failed to create data file in ensureFile"); 219 return null; 220 } 221 } 222 223 @Override insert(Uri uri, ContentValues initialValues)224 public Uri insert(Uri uri, ContentValues initialValues) 225 { 226 if (getContext().checkCallingOrSelfPermission(Manifest.permission.INSTALL_DRM) 227 != PackageManager.PERMISSION_GRANTED) { 228 throw new SecurityException("Requires INSTALL_DRM permission"); 229 } 230 231 long rowId; 232 int match = URI_MATCHER.match(uri); 233 Uri newUri = null; 234 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 235 236 if (initialValues == null) { 237 initialValues = new ContentValues(); 238 } 239 240 switch (match) { 241 case AUDIO: { 242 ContentValues values = ensureFile(initialValues); 243 if (values == null) return null; 244 rowId = db.insert("audio", "title", values); 245 if (rowId > 0) { 246 newUri = ContentUris.withAppendedId(DrmStore.Audio.CONTENT_URI, rowId); 247 } 248 break; 249 } 250 251 case IMAGES: { 252 ContentValues values = ensureFile(initialValues); 253 if (values == null) return null; 254 rowId = db.insert("images", "title", values); 255 if (rowId > 0) { 256 newUri = ContentUris.withAppendedId(DrmStore.Images.CONTENT_URI, rowId); 257 } 258 break; 259 } 260 261 default: 262 throw new UnsupportedOperationException("Invalid URI " + uri); 263 } 264 265 if (newUri != null) { 266 getContext().getContentResolver().notifyChange(uri, null); 267 } 268 269 return newUri; 270 } 271 272 private static final class GetTableAndWhereOutParameter { 273 public String table; 274 public String where; 275 } 276 277 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 278 new GetTableAndWhereOutParameter(); 279 getTableAndWhere(Uri uri, int match, String userWhere, GetTableAndWhereOutParameter out)280 private void getTableAndWhere(Uri uri, int match, String userWhere, 281 GetTableAndWhereOutParameter out) { 282 String where = null; 283 switch (match) { 284 case AUDIO: 285 out.table = "audio"; 286 break; 287 288 case AUDIO_ID: 289 out.table = "audio"; 290 where = "_id=" + uri.getPathSegments().get(1); 291 break; 292 293 case IMAGES: 294 out.table = "images"; 295 break; 296 297 case IMAGES_ID: 298 out.table = "images"; 299 where = "_id=" + uri.getPathSegments().get(1); 300 break; 301 302 default: 303 throw new UnsupportedOperationException( 304 "Unknown or unsupported URL: " + uri.toString()); 305 } 306 307 // Add in the user requested WHERE clause, if needed 308 if (!TextUtils.isEmpty(userWhere)) { 309 if (!TextUtils.isEmpty(where)) { 310 out.where = where + " AND (" + userWhere + ")"; 311 } else { 312 out.where = userWhere; 313 } 314 } else { 315 out.where = where; 316 } 317 } 318 deleteFiles(Uri uri, String userWhere, String[] whereArgs)319 private void deleteFiles(Uri uri, String userWhere, String[] whereArgs) { 320 Cursor c = query(uri, new String [] { "_data" }, userWhere, whereArgs, null); 321 322 try { 323 if (c != null && c.moveToFirst()) { 324 String prefix = getContext().getFilesDir().getPath(); 325 do { 326 String path = c.getString(0); 327 if (!path.startsWith(prefix)) { 328 throw new SecurityException("Attempted to delete a non-DRM file"); 329 } 330 new File(path).delete(); 331 } while (c.moveToNext()); 332 } 333 } finally { 334 if (c != null) { 335 c.close(); 336 } 337 } 338 } 339 340 @Override delete(Uri uri, String userWhere, String[] whereArgs)341 public int delete(Uri uri, String userWhere, String[] whereArgs) { 342 if (getContext().checkCallingOrSelfPermission(Manifest.permission.ACCESS_DRM) 343 != PackageManager.PERMISSION_GRANTED) { 344 throw new SecurityException("Requires DRM permission"); 345 } 346 347 int count; 348 int match = URI_MATCHER.match(uri); 349 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 350 351 synchronized (sGetTableAndWhereParam) { 352 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 353 switch (match) { 354 default: 355 deleteFiles(uri, userWhere, whereArgs); 356 count = db.delete(sGetTableAndWhereParam.table, 357 sGetTableAndWhereParam.where, whereArgs); 358 break; 359 } 360 } 361 362 return count; 363 } 364 365 @Override update(Uri uri, ContentValues initialValues, String userWhere, String[] whereArgs)366 public int update(Uri uri, ContentValues initialValues, String userWhere, 367 String[] whereArgs) { 368 if (getContext().checkCallingOrSelfPermission(Manifest.permission.ACCESS_DRM) 369 != PackageManager.PERMISSION_GRANTED) { 370 throw new SecurityException("Requires ACCESS_DRM permission"); 371 } 372 373 int count; 374 int match = URI_MATCHER.match(uri); 375 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 376 377 synchronized (sGetTableAndWhereParam) { 378 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 379 380 switch (match) { 381 default: 382 count = db.update(sGetTableAndWhereParam.table, initialValues, 383 sGetTableAndWhereParam.where, whereArgs); 384 break; 385 } 386 } 387 388 return count; 389 } 390 391 @Override openFile(Uri uri, String mode)392 public ParcelFileDescriptor openFile(Uri uri, String mode) 393 throws FileNotFoundException { 394 String requiredPermission = mode.equals("w") ? 395 Manifest.permission.INSTALL_DRM : Manifest.permission.ACCESS_DRM; 396 397 if (getContext().checkCallingOrSelfPermission(requiredPermission) 398 != PackageManager.PERMISSION_GRANTED) { 399 throw new SecurityException("Requires " + requiredPermission); 400 } 401 return openFileHelper(uri, mode); 402 } 403 404 private static String TAG = "DrmProvider"; 405 406 private static final int AUDIO = 100; 407 private static final int AUDIO_ID = 101; 408 private static final int IMAGES = 102; 409 private static final int IMAGES_ID = 103; 410 411 private static final UriMatcher URI_MATCHER = 412 new UriMatcher(UriMatcher.NO_MATCH); 413 414 private static final String[] MIME_TYPE_PROJECTION = new String[] { 415 DrmStore.Columns._ID, // 0 416 DrmStore.Columns.MIME_TYPE, // 1 417 }; 418 419 private SQLiteOpenHelper mOpenHelper; 420 421 static 422 { URI_MATCHER.addURI(DrmStore.AUTHORITY, "audio", AUDIO)423 URI_MATCHER.addURI(DrmStore.AUTHORITY, "audio", AUDIO); URI_MATCHER.addURI(DrmStore.AUTHORITY, "audio/#", AUDIO_ID)424 URI_MATCHER.addURI(DrmStore.AUTHORITY, "audio/#", AUDIO_ID); URI_MATCHER.addURI(DrmStore.AUTHORITY, "images", IMAGES)425 URI_MATCHER.addURI(DrmStore.AUTHORITY, "images", IMAGES); URI_MATCHER.addURI(DrmStore.AUTHORITY, "images/#", IMAGES_ID)426 URI_MATCHER.addURI(DrmStore.AUTHORITY, "images/#", IMAGES_ID); 427 } 428 } 429