2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.providers.downloads;
19 import android.content.ContentProvider;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.UriMatcher;
24 import android.content.pm.PackageManager;
25 import android.database.CrossProcessCursor;
26 import android.database.Cursor;
27 import android.database.CursorWindow;
28 import android.database.CursorWrapper;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.database.sqlite.SQLiteOpenHelper;
31 import android.database.sqlite.SQLiteQueryBuilder;
32 import android.database.SQLException;
33 import android.net.Uri;
34 import android.os.Binder;
35 import android.os.ParcelFileDescriptor;
36 import android.os.Process;
37 import android.provider.Downloads;
38 import android.util.Config;
39 import android.util.Log;
42 import java.io.FileNotFoundException;
43 import java.io.IOException;
44 import java.util.HashSet;
48 * Allows application to interact with the download manager.
50 public final class DownloadProvider extends ContentProvider {
52 /** Database filename */
53 private static final String DB_NAME = "downloads.db";
54 /** Current database version */
55 private static final int DB_VERSION = 100;
56 /** Database version from which upgrading is a nop */
57 private static final int DB_VERSION_NOP_UPGRADE_FROM = 31;
58 /** Database version to which upgrading is a nop */
59 private static final int DB_VERSION_NOP_UPGRADE_TO = 100;
60 /** Name of table in the database */
61 private static final String DB_TABLE = "downloads";
63 /** MIME type for the entire download list */
64 private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
65 /** MIME type for an individual download */
66 private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
68 /** URI matcher used to recognize URIs sent by applications */
69 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
70 /** URI matcher constant for the URI of the entire download list */
71 private static final int DOWNLOADS = 1;
72 /** URI matcher constant for the URI of an individual download */
73 private static final int DOWNLOADS_ID = 2;
75 sURIMatcher.addURI("downloads", "download", DOWNLOADS);
76 sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
79 private static final String[] sAppReadableColumnsArray = new String[] {
87 Downloads.LAST_MODIFICATION,
88 Downloads.NOTIFICATION_PACKAGE,
89 Downloads.NOTIFICATION_CLASS,
90 Downloads.TOTAL_BYTES,
91 Downloads.CURRENT_BYTES,
96 private static HashSet<String> sAppReadableColumnsSet;
98 sAppReadableColumnsSet = new HashSet<String>();
99 for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
100 sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
104 /** The database that lies underneath this content provider */
105 private SQLiteOpenHelper mOpenHelper = null;
108 * Creates and updated database on demand when opening it.
109 * Helper class to create database the first time the provider is
110 * initialized and upgrade it when a new version of the provider needs
111 * an updated version of the database.
113 private final class DatabaseHelper extends SQLiteOpenHelper {
115 public DatabaseHelper(final Context context) {
116 super(context, DB_NAME, null, DB_VERSION);
120 * Creates database the first time we try to open it.
123 public void onCreate(final SQLiteDatabase db) {
124 if (Constants.LOGVV) {
125 Log.v(Constants.TAG, "populating new database");
130 /* (not a javadoc comment)
131 * Checks data integrity when opening the database.
135 * public void onOpen(final SQLiteDatabase db) {
141 * Updates the database format when a content provider is used
142 * with a database that was created with a different format.
144 // Note: technically, this could also be a downgrade, so if we want
145 // to gracefully handle upgrades we should be careful about
146 // what to do on downgrades.
148 public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
149 if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
150 if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op upgrade.
153 // NOP_FROM and NOP_TO are identical, just in different codelines. Upgrading
154 // from NOP_FROM is the same as upgrading from NOP_TO.
155 oldV = DB_VERSION_NOP_UPGRADE_TO;
157 Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV + " to " + newV
158 + ", which will destroy all old data");
165 * Initializes the content provider when it is created.
168 public boolean onCreate() {
169 mOpenHelper = new DatabaseHelper(getContext());
174 * Returns the content-provider-style MIME types of the various
175 * types accessible through this content provider.
178 public String getType(final Uri uri) {
179 int match = sURIMatcher.match(uri);
182 return DOWNLOAD_LIST_TYPE;
185 return DOWNLOAD_TYPE;
188 if (Constants.LOGV) {
189 Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
191 throw new IllegalArgumentException("Unknown URI: " + uri);
197 * Creates the table that'll hold the download information.
199 private void createTable(SQLiteDatabase db) {
201 db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
202 Downloads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
203 Downloads.URI + " TEXT, " +
204 Constants.RETRY_AFTER___REDIRECT_COUNT + " INTEGER, " +
205 Downloads.APP_DATA + " TEXT, " +
206 Downloads.NO_INTEGRITY + " BOOLEAN, " +
207 Downloads.FILENAME_HINT + " TEXT, " +
208 Constants.OTA_UPDATE + " BOOLEAN, " +
209 Downloads._DATA + " TEXT, " +
210 Downloads.MIMETYPE + " TEXT, " +
211 Downloads.DESTINATION + " INTEGER, " +
212 Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
213 Downloads.VISIBILITY + " INTEGER, " +
214 Downloads.CONTROL + " INTEGER, " +
215 Downloads.STATUS + " INTEGER, " +
216 Constants.FAILED_CONNECTIONS + " INTEGER, " +
217 Downloads.LAST_MODIFICATION + " BIGINT, " +
218 Downloads.NOTIFICATION_PACKAGE + " TEXT, " +
219 Downloads.NOTIFICATION_CLASS + " TEXT, " +
220 Downloads.NOTIFICATION_EXTRAS + " TEXT, " +
221 Downloads.COOKIE_DATA + " TEXT, " +
222 Downloads.USER_AGENT + " TEXT, " +
223 Downloads.REFERER + " TEXT, " +
224 Downloads.TOTAL_BYTES + " INTEGER, " +
225 Downloads.CURRENT_BYTES + " INTEGER, " +
226 Constants.ETAG + " TEXT, " +
227 Constants.UID + " INTEGER, " +
228 Downloads.OTHER_UID + " INTEGER, " +
229 Downloads.TITLE + " TEXT, " +
230 Downloads.DESCRIPTION + " TEXT, " +
231 Constants.MEDIA_SCANNED + " BOOLEAN);");
232 } catch (SQLException ex) {
233 Log.e(Constants.TAG, "couldn't create table in downloads database");
239 * Deletes the table that holds the download information.
241 private void dropTable(SQLiteDatabase db) {
243 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
244 } catch (SQLException ex) {
245 Log.e(Constants.TAG, "couldn't drop table in downloads database");
251 * Inserts a row in the database
254 public Uri insert(final Uri uri, final ContentValues values) {
255 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
257 if (sURIMatcher.match(uri) != DOWNLOADS) {
259 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
261 throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
264 ContentValues filteredValues = new ContentValues();
266 copyString(Downloads.URI, values, filteredValues);
267 copyString(Downloads.APP_DATA, values, filteredValues);
268 copyBoolean(Downloads.NO_INTEGRITY, values, filteredValues);
269 copyString(Downloads.FILENAME_HINT, values, filteredValues);
270 copyString(Downloads.MIMETYPE, values, filteredValues);
271 Integer dest = values.getAsInteger(Downloads.DESTINATION);
273 if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
274 != PackageManager.PERMISSION_GRANTED
275 && dest != Downloads.DESTINATION_EXTERNAL
276 && dest != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
277 throw new SecurityException("unauthorized destination code");
279 filteredValues.put(Downloads.DESTINATION, dest);
281 Integer vis = values.getAsInteger(Downloads.VISIBILITY);
283 if (dest == Downloads.DESTINATION_EXTERNAL) {
284 filteredValues.put(Downloads.VISIBILITY,
285 Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
287 filteredValues.put(Downloads.VISIBILITY, Downloads.VISIBILITY_HIDDEN);
290 filteredValues.put(Downloads.VISIBILITY, vis);
292 copyInteger(Downloads.CONTROL, values, filteredValues);
293 filteredValues.put(Downloads.STATUS, Downloads.STATUS_PENDING);
294 filteredValues.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
295 String pckg = values.getAsString(Downloads.NOTIFICATION_PACKAGE);
296 String clazz = values.getAsString(Downloads.NOTIFICATION_CLASS);
297 if (pckg != null && clazz != null) {
298 int uid = Binder.getCallingUid();
301 getContext().getPackageManager().getApplicationInfo(pckg, 0).uid == uid) {
302 filteredValues.put(Downloads.NOTIFICATION_PACKAGE, pckg);
303 filteredValues.put(Downloads.NOTIFICATION_CLASS, clazz);
305 } catch (PackageManager.NameNotFoundException ex) {
306 /* ignored for now */
309 copyString(Downloads.NOTIFICATION_EXTRAS, values, filteredValues);
310 copyString(Downloads.COOKIE_DATA, values, filteredValues);
311 copyString(Downloads.USER_AGENT, values, filteredValues);
312 copyString(Downloads.REFERER, values, filteredValues);
313 if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
314 == PackageManager.PERMISSION_GRANTED) {
315 copyInteger(Downloads.OTHER_UID, values, filteredValues);
317 filteredValues.put(Constants.UID, Binder.getCallingUid());
318 if (Binder.getCallingUid() == 0) {
319 copyInteger(Constants.UID, values, filteredValues);
321 copyString(Downloads.TITLE, values, filteredValues);
322 copyString(Downloads.DESCRIPTION, values, filteredValues);
324 if (Constants.LOGVV) {
325 Log.v(Constants.TAG, "initiating download with UID "
326 + filteredValues.getAsInteger(Constants.UID));
327 if (filteredValues.containsKey(Downloads.OTHER_UID)) {
328 Log.v(Constants.TAG, "other UID " +
329 filteredValues.getAsInteger(Downloads.OTHER_UID));
333 Context context = getContext();
334 context.startService(new Intent(context, DownloadService.class));
336 long rowID = db.insert(DB_TABLE, null, filteredValues);
341 context.startService(new Intent(context, DownloadService.class));
342 ret = Uri.parse(Downloads.CONTENT_URI + "/" + rowID);
343 context.getContentResolver().notifyChange(uri, null);
346 Log.d(Constants.TAG, "couldn't insert into downloads database");
354 * Starts a database query
357 public Cursor query(final Uri uri, String[] projection,
358 final String selection, final String[] selectionArgs,
361 Helpers.validateSelection(selection, sAppReadableColumnsSet);
363 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
365 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
367 int match = sURIMatcher.match(uri);
368 boolean emptyWhere = true;
371 qb.setTables(DB_TABLE);
375 qb.setTables(DB_TABLE);
376 qb.appendWhere(Downloads._ID + "=");
377 qb.appendWhere(uri.getPathSegments().get(1));
382 if (Constants.LOGV) {
383 Log.v(Constants.TAG, "querying unknown URI: " + uri);
385 throw new IllegalArgumentException("Unknown URI: " + uri);
389 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
391 qb.appendWhere(" AND ");
393 qb.appendWhere("( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
394 + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )");
397 if (projection == null) {
398 projection = sAppReadableColumnsArray;
400 for (int i = 0; i < projection.length; ++i) {
401 if (!sAppReadableColumnsSet.contains(projection[i])) {
402 throw new IllegalArgumentException(
403 "column " + projection[i] + " is not allowed in queries");
409 if (Constants.LOGVV) {
410 java.lang.StringBuilder sb = new java.lang.StringBuilder();
411 sb.append("starting query, database is ");
416 if (projection == null) {
417 sb.append("projection is null; ");
418 } else if (projection.length == 0) {
419 sb.append("projection is empty; ");
421 for (int i = 0; i < projection.length; ++i) {
422 sb.append("projection[");
425 sb.append(projection[i]);
429 sb.append("selection is ");
430 sb.append(selection);
432 if (selectionArgs == null) {
433 sb.append("selectionArgs is null; ");
434 } else if (selectionArgs.length == 0) {
435 sb.append("selectionArgs is empty; ");
437 for (int i = 0; i < selectionArgs.length; ++i) {
438 sb.append("selectionArgs[");
441 sb.append(selectionArgs[i]);
445 sb.append("sort is ");
448 Log.v(Constants.TAG, sb.toString());
451 Cursor ret = qb.query(db, projection, selection, selectionArgs,
455 ret = new ReadOnlyCursorWrapper(ret);
459 ret.setNotificationUri(getContext().getContentResolver(), uri);
460 if (Constants.LOGVV) {
462 "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
465 if (Constants.LOGV) {
466 Log.v(Constants.TAG, "query failed in downloads database");
474 * Updates a row in the database
477 public int update(final Uri uri, final ContentValues values,
478 final String where, final String[] whereArgs) {
480 Helpers.validateSelection(where, sAppReadableColumnsSet);
482 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
486 boolean startService = false;
488 ContentValues filteredValues;
489 if (Binder.getCallingPid() != Process.myPid()) {
490 filteredValues = new ContentValues();
491 copyString(Downloads.APP_DATA, values, filteredValues);
492 copyInteger(Downloads.VISIBILITY, values, filteredValues);
493 Integer i = values.getAsInteger(Downloads.CONTROL);
495 filteredValues.put(Downloads.CONTROL, i);
498 copyInteger(Downloads.CONTROL, values, filteredValues);
499 copyString(Downloads.TITLE, values, filteredValues);
500 copyString(Downloads.DESCRIPTION, values, filteredValues);
502 filteredValues = values;
504 int match = sURIMatcher.match(uri);
510 if (match == DOWNLOADS) {
511 myWhere = "( " + where + " )";
513 myWhere = "( " + where + " ) AND ";
518 if (match == DOWNLOADS_ID) {
519 String segment = uri.getPathSegments().get(1);
520 rowId = Long.parseLong(segment);
521 myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
523 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
524 myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
525 + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
527 if (filteredValues.size() > 0) {
528 count = db.update(DB_TABLE, filteredValues, myWhere, whereArgs);
536 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
538 throw new UnsupportedOperationException("Cannot update URI: " + uri);
541 getContext().getContentResolver().notifyChange(uri, null);
543 Context context = getContext();
544 context.startService(new Intent(context, DownloadService.class));
550 * Deletes a row in the database
553 public int delete(final Uri uri, final String where,
554 final String[] whereArgs) {
556 Helpers.validateSelection(where, sAppReadableColumnsSet);
558 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
560 int match = sURIMatcher.match(uri);
566 if (match == DOWNLOADS) {
567 myWhere = "( " + where + " )";
569 myWhere = "( " + where + " ) AND ";
574 if (match == DOWNLOADS_ID) {
575 String segment = uri.getPathSegments().get(1);
576 long rowId = Long.parseLong(segment);
577 myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
579 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
580 myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
581 + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
583 count = db.delete(DB_TABLE, myWhere, whereArgs);
588 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
590 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
593 getContext().getContentResolver().notifyChange(uri, null);
598 * Remotely opens a file
601 public ParcelFileDescriptor openFile(Uri uri, String mode)
602 throws FileNotFoundException {
603 if (Constants.LOGVV) {
604 Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
605 + ", uid: " + Binder.getCallingUid());
606 Cursor cursor = query(Downloads.CONTENT_URI, new String[] { "_id" }, null, null, "_id");
607 if (cursor == null) {
608 Log.v(Constants.TAG, "null cursor in openFile");
610 if (!cursor.moveToFirst()) {
611 Log.v(Constants.TAG, "empty cursor in openFile");
614 Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
615 } while(cursor.moveToNext());
619 cursor = query(uri, new String[] { "_data" }, null, null, null);
620 if (cursor == null) {
621 Log.v(Constants.TAG, "null cursor in openFile");
623 if (!cursor.moveToFirst()) {
624 Log.v(Constants.TAG, "empty cursor in openFile");
626 String filename = cursor.getString(0);
627 Log.v(Constants.TAG, "filename in openFile: " + filename);
628 if (new java.io.File(filename).isFile()) {
629 Log.v(Constants.TAG, "file exists in openFile");
636 // This logic is mostly copied form openFileHelper. If openFileHelper eventually
637 // gets split into small bits (to extract the filename and the modebits),
638 // this code could use the separate bits and be deeply simplified.
639 Cursor c = query(uri, new String[]{"_data"}, null, null, null);
640 int count = (c != null) ? c.getCount() : 0;
642 // If there is not exactly one result, throw an appropriate exception.
647 throw new FileNotFoundException("No entry for " + uri);
649 throw new FileNotFoundException("Multiple items at " + uri);
653 String path = c.getString(0);
656 throw new FileNotFoundException("No filename found.");
658 if (!Helpers.isFilenameValid(path)) {
659 throw new FileNotFoundException("Invalid filename.");
662 if (!"r".equals(mode)) {
663 throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
665 ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
666 ParcelFileDescriptor.MODE_READ_ONLY);
669 if (Constants.LOGV) {
670 Log.v(Constants.TAG, "couldn't open file");
672 throw new FileNotFoundException("couldn't open file");
674 ContentValues values = new ContentValues();
675 values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
676 update(uri, values, null, null);
681 private static final void copyInteger(String key, ContentValues from, ContentValues to) {
682 Integer i = from.getAsInteger(key);
688 private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
689 Boolean b = from.getAsBoolean(key);
695 private static final void copyString(String key, ContentValues from, ContentValues to) {
696 String s = from.getAsString(key);
702 private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
703 public ReadOnlyCursorWrapper(Cursor cursor) {
705 mCursor = (CrossProcessCursor) cursor;
708 public boolean deleteRow() {
709 throw new SecurityException("Download manager cursors are read-only");
712 public boolean commitUpdates() {
713 throw new SecurityException("Download manager cursors are read-only");
716 public void fillWindow(int pos, CursorWindow window) {
717 mCursor.fillWindow(pos, window);
720 public CursorWindow getWindow() {
721 return mCursor.getWindow();
724 public boolean onMove(int oldPosition, int newPosition) {
725 return mCursor.onMove(oldPosition, newPosition);
728 private CrossProcessCursor mCursor;