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[] {
85 Downloads.DESTINATION,
88 Downloads.LAST_MODIFICATION,
89 Downloads.NOTIFICATION_PACKAGE,
90 Downloads.NOTIFICATION_CLASS,
91 Downloads.TOTAL_BYTES,
92 Downloads.CURRENT_BYTES,
97 private static HashSet<String> sAppReadableColumnsSet;
99 sAppReadableColumnsSet = new HashSet<String>();
100 for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
101 sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
105 /** The database that lies underneath this content provider */
106 private SQLiteOpenHelper mOpenHelper = null;
109 * Creates and updated database on demand when opening it.
110 * Helper class to create database the first time the provider is
111 * initialized and upgrade it when a new version of the provider needs
112 * an updated version of the database.
114 private final class DatabaseHelper extends SQLiteOpenHelper {
116 public DatabaseHelper(final Context context) {
117 super(context, DB_NAME, null, DB_VERSION);
121 * Creates database the first time we try to open it.
124 public void onCreate(final SQLiteDatabase db) {
125 if (Constants.LOGVV) {
126 Log.v(Constants.TAG, "populating new database");
131 /* (not a javadoc comment)
132 * Checks data integrity when opening the database.
136 * public void onOpen(final SQLiteDatabase db) {
142 * Updates the database format when a content provider is used
143 * with a database that was created with a different format.
145 // Note: technically, this could also be a downgrade, so if we want
146 // to gracefully handle upgrades we should be careful about
147 // what to do on downgrades.
149 public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
150 if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
151 if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op upgrade.
154 // NOP_FROM and NOP_TO are identical, just in different codelines. Upgrading
155 // from NOP_FROM is the same as upgrading from NOP_TO.
156 oldV = DB_VERSION_NOP_UPGRADE_TO;
158 Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV + " to " + newV
159 + ", which will destroy all old data");
166 * Initializes the content provider when it is created.
169 public boolean onCreate() {
170 mOpenHelper = new DatabaseHelper(getContext());
175 * Returns the content-provider-style MIME types of the various
176 * types accessible through this content provider.
179 public String getType(final Uri uri) {
180 int match = sURIMatcher.match(uri);
183 return DOWNLOAD_LIST_TYPE;
186 return DOWNLOAD_TYPE;
189 if (Constants.LOGV) {
190 Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
192 throw new IllegalArgumentException("Unknown URI: " + uri);
198 * Creates the table that'll hold the download information.
200 private void createTable(SQLiteDatabase db) {
202 db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
203 Downloads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
204 Downloads.URI + " TEXT, " +
205 Constants.RETRY_AFTER___REDIRECT_COUNT + " INTEGER, " +
206 Downloads.APP_DATA + " TEXT, " +
207 Downloads.NO_INTEGRITY + " BOOLEAN, " +
208 Downloads.FILENAME_HINT + " TEXT, " +
209 Constants.OTA_UPDATE + " BOOLEAN, " +
210 Downloads._DATA + " TEXT, " +
211 Downloads.MIMETYPE + " TEXT, " +
212 Downloads.DESTINATION + " INTEGER, " +
213 Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
214 Downloads.VISIBILITY + " INTEGER, " +
215 Downloads.CONTROL + " INTEGER, " +
216 Downloads.STATUS + " INTEGER, " +
217 Constants.FAILED_CONNECTIONS + " INTEGER, " +
218 Downloads.LAST_MODIFICATION + " BIGINT, " +
219 Downloads.NOTIFICATION_PACKAGE + " TEXT, " +
220 Downloads.NOTIFICATION_CLASS + " TEXT, " +
221 Downloads.NOTIFICATION_EXTRAS + " TEXT, " +
222 Downloads.COOKIE_DATA + " TEXT, " +
223 Downloads.USER_AGENT + " TEXT, " +
224 Downloads.REFERER + " TEXT, " +
225 Downloads.TOTAL_BYTES + " INTEGER, " +
226 Downloads.CURRENT_BYTES + " INTEGER, " +
227 Constants.ETAG + " TEXT, " +
228 Constants.UID + " INTEGER, " +
229 Downloads.OTHER_UID + " INTEGER, " +
230 Downloads.TITLE + " TEXT, " +
231 Downloads.DESCRIPTION + " TEXT, " +
232 Constants.MEDIA_SCANNED + " BOOLEAN);");
233 } catch (SQLException ex) {
234 Log.e(Constants.TAG, "couldn't create table in downloads database");
240 * Deletes the table that holds the download information.
242 private void dropTable(SQLiteDatabase db) {
244 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
245 } catch (SQLException ex) {
246 Log.e(Constants.TAG, "couldn't drop table in downloads database");
252 * Inserts a row in the database
255 public Uri insert(final Uri uri, final ContentValues values) {
256 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
258 if (sURIMatcher.match(uri) != DOWNLOADS) {
260 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
262 throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
265 ContentValues filteredValues = new ContentValues();
267 copyString(Downloads.URI, values, filteredValues);
268 copyString(Downloads.APP_DATA, values, filteredValues);
269 copyBoolean(Downloads.NO_INTEGRITY, values, filteredValues);
270 copyString(Downloads.FILENAME_HINT, values, filteredValues);
271 copyString(Downloads.MIMETYPE, values, filteredValues);
272 Integer dest = values.getAsInteger(Downloads.DESTINATION);
274 if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
275 != PackageManager.PERMISSION_GRANTED
276 && dest != Downloads.DESTINATION_EXTERNAL
277 && dest != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
278 throw new SecurityException("unauthorized destination code");
280 filteredValues.put(Downloads.DESTINATION, dest);
282 Integer vis = values.getAsInteger(Downloads.VISIBILITY);
284 if (dest == Downloads.DESTINATION_EXTERNAL) {
285 filteredValues.put(Downloads.VISIBILITY,
286 Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
288 filteredValues.put(Downloads.VISIBILITY, Downloads.VISIBILITY_HIDDEN);
291 filteredValues.put(Downloads.VISIBILITY, vis);
293 copyInteger(Downloads.CONTROL, values, filteredValues);
294 filteredValues.put(Downloads.STATUS, Downloads.STATUS_PENDING);
295 filteredValues.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
296 String pckg = values.getAsString(Downloads.NOTIFICATION_PACKAGE);
297 String clazz = values.getAsString(Downloads.NOTIFICATION_CLASS);
298 if (pckg != null && clazz != null) {
299 int uid = Binder.getCallingUid();
302 getContext().getPackageManager().getApplicationInfo(pckg, 0).uid == uid) {
303 filteredValues.put(Downloads.NOTIFICATION_PACKAGE, pckg);
304 filteredValues.put(Downloads.NOTIFICATION_CLASS, clazz);
306 } catch (PackageManager.NameNotFoundException ex) {
307 /* ignored for now */
310 copyString(Downloads.NOTIFICATION_EXTRAS, values, filteredValues);
311 copyString(Downloads.COOKIE_DATA, values, filteredValues);
312 copyString(Downloads.USER_AGENT, values, filteredValues);
313 copyString(Downloads.REFERER, values, filteredValues);
314 if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
315 == PackageManager.PERMISSION_GRANTED) {
316 copyInteger(Downloads.OTHER_UID, values, filteredValues);
318 filteredValues.put(Constants.UID, Binder.getCallingUid());
319 if (Binder.getCallingUid() == 0) {
320 copyInteger(Constants.UID, values, filteredValues);
322 copyString(Downloads.TITLE, values, filteredValues);
323 copyString(Downloads.DESCRIPTION, values, filteredValues);
325 if (Constants.LOGVV) {
326 Log.v(Constants.TAG, "initiating download with UID "
327 + filteredValues.getAsInteger(Constants.UID));
328 if (filteredValues.containsKey(Downloads.OTHER_UID)) {
329 Log.v(Constants.TAG, "other UID " +
330 filteredValues.getAsInteger(Downloads.OTHER_UID));
334 Context context = getContext();
335 context.startService(new Intent(context, DownloadService.class));
337 long rowID = db.insert(DB_TABLE, null, filteredValues);
342 context.startService(new Intent(context, DownloadService.class));
343 ret = Uri.parse(Downloads.CONTENT_URI + "/" + rowID);
344 context.getContentResolver().notifyChange(uri, null);
347 Log.d(Constants.TAG, "couldn't insert into downloads database");
355 * Starts a database query
358 public Cursor query(final Uri uri, String[] projection,
359 final String selection, final String[] selectionArgs,
362 Helpers.validateSelection(selection, sAppReadableColumnsSet);
364 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
366 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
368 int match = sURIMatcher.match(uri);
369 boolean emptyWhere = true;
372 qb.setTables(DB_TABLE);
376 qb.setTables(DB_TABLE);
377 qb.appendWhere(Downloads._ID + "=");
378 qb.appendWhere(uri.getPathSegments().get(1));
383 if (Constants.LOGV) {
384 Log.v(Constants.TAG, "querying unknown URI: " + uri);
386 throw new IllegalArgumentException("Unknown URI: " + uri);
390 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
392 qb.appendWhere(" AND ");
394 qb.appendWhere("( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
395 + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )");
398 if (projection == null) {
399 projection = sAppReadableColumnsArray;
401 for (int i = 0; i < projection.length; ++i) {
402 if (!sAppReadableColumnsSet.contains(projection[i])) {
403 throw new IllegalArgumentException(
404 "column " + projection[i] + " is not allowed in queries");
410 if (Constants.LOGVV) {
411 java.lang.StringBuilder sb = new java.lang.StringBuilder();
412 sb.append("starting query, database is ");
417 if (projection == null) {
418 sb.append("projection is null; ");
419 } else if (projection.length == 0) {
420 sb.append("projection is empty; ");
422 for (int i = 0; i < projection.length; ++i) {
423 sb.append("projection[");
426 sb.append(projection[i]);
430 sb.append("selection is ");
431 sb.append(selection);
433 if (selectionArgs == null) {
434 sb.append("selectionArgs is null; ");
435 } else if (selectionArgs.length == 0) {
436 sb.append("selectionArgs is empty; ");
438 for (int i = 0; i < selectionArgs.length; ++i) {
439 sb.append("selectionArgs[");
442 sb.append(selectionArgs[i]);
446 sb.append("sort is ");
449 Log.v(Constants.TAG, sb.toString());
452 Cursor ret = qb.query(db, projection, selection, selectionArgs,
456 ret = new ReadOnlyCursorWrapper(ret);
460 ret.setNotificationUri(getContext().getContentResolver(), uri);
461 if (Constants.LOGVV) {
463 "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
466 if (Constants.LOGV) {
467 Log.v(Constants.TAG, "query failed in downloads database");
475 * Updates a row in the database
478 public int update(final Uri uri, final ContentValues values,
479 final String where, final String[] whereArgs) {
481 Helpers.validateSelection(where, sAppReadableColumnsSet);
483 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
487 boolean startService = false;
489 ContentValues filteredValues;
490 if (Binder.getCallingPid() != Process.myPid()) {
491 filteredValues = new ContentValues();
492 copyString(Downloads.APP_DATA, values, filteredValues);
493 copyInteger(Downloads.VISIBILITY, values, filteredValues);
494 Integer i = values.getAsInteger(Downloads.CONTROL);
496 filteredValues.put(Downloads.CONTROL, i);
499 copyInteger(Downloads.CONTROL, values, filteredValues);
500 copyString(Downloads.TITLE, values, filteredValues);
501 copyString(Downloads.DESCRIPTION, values, filteredValues);
503 filteredValues = values;
505 int match = sURIMatcher.match(uri);
511 if (match == DOWNLOADS) {
512 myWhere = "( " + where + " )";
514 myWhere = "( " + where + " ) AND ";
519 if (match == DOWNLOADS_ID) {
520 String segment = uri.getPathSegments().get(1);
521 rowId = Long.parseLong(segment);
522 myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
524 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
525 myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
526 + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
528 if (filteredValues.size() > 0) {
529 count = db.update(DB_TABLE, filteredValues, myWhere, whereArgs);
537 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
539 throw new UnsupportedOperationException("Cannot update URI: " + uri);
542 getContext().getContentResolver().notifyChange(uri, null);
544 Context context = getContext();
545 context.startService(new Intent(context, DownloadService.class));
551 * Deletes a row in the database
554 public int delete(final Uri uri, final String where,
555 final String[] whereArgs) {
557 Helpers.validateSelection(where, sAppReadableColumnsSet);
559 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
561 int match = sURIMatcher.match(uri);
567 if (match == DOWNLOADS) {
568 myWhere = "( " + where + " )";
570 myWhere = "( " + where + " ) AND ";
575 if (match == DOWNLOADS_ID) {
576 String segment = uri.getPathSegments().get(1);
577 long rowId = Long.parseLong(segment);
578 myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
580 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
581 myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
582 + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
584 count = db.delete(DB_TABLE, myWhere, whereArgs);
589 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
591 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
594 getContext().getContentResolver().notifyChange(uri, null);
599 * Remotely opens a file
602 public ParcelFileDescriptor openFile(Uri uri, String mode)
603 throws FileNotFoundException {
604 if (Constants.LOGVV) {
605 Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
606 + ", uid: " + Binder.getCallingUid());
607 Cursor cursor = query(Downloads.CONTENT_URI, new String[] { "_id" }, null, null, "_id");
608 if (cursor == null) {
609 Log.v(Constants.TAG, "null cursor in openFile");
611 if (!cursor.moveToFirst()) {
612 Log.v(Constants.TAG, "empty cursor in openFile");
615 Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
616 } while(cursor.moveToNext());
620 cursor = query(uri, new String[] { "_data" }, null, null, null);
621 if (cursor == null) {
622 Log.v(Constants.TAG, "null cursor in openFile");
624 if (!cursor.moveToFirst()) {
625 Log.v(Constants.TAG, "empty cursor in openFile");
627 String filename = cursor.getString(0);
628 Log.v(Constants.TAG, "filename in openFile: " + filename);
629 if (new java.io.File(filename).isFile()) {
630 Log.v(Constants.TAG, "file exists in openFile");
637 // This logic is mostly copied form openFileHelper. If openFileHelper eventually
638 // gets split into small bits (to extract the filename and the modebits),
639 // this code could use the separate bits and be deeply simplified.
640 Cursor c = query(uri, new String[]{"_data"}, null, null, null);
641 int count = (c != null) ? c.getCount() : 0;
643 // If there is not exactly one result, throw an appropriate exception.
648 throw new FileNotFoundException("No entry for " + uri);
650 throw new FileNotFoundException("Multiple items at " + uri);
654 String path = c.getString(0);
657 throw new FileNotFoundException("No filename found.");
659 if (!Helpers.isFilenameValid(path)) {
660 throw new FileNotFoundException("Invalid filename.");
663 if (!"r".equals(mode)) {
664 throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
666 ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
667 ParcelFileDescriptor.MODE_READ_ONLY);
670 if (Constants.LOGV) {
671 Log.v(Constants.TAG, "couldn't open file");
673 throw new FileNotFoundException("couldn't open file");
675 ContentValues values = new ContentValues();
676 values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
677 update(uri, values, null, null);
682 private static final void copyInteger(String key, ContentValues from, ContentValues to) {
683 Integer i = from.getAsInteger(key);
689 private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
690 Boolean b = from.getAsBoolean(key);
696 private static final void copyString(String key, ContentValues from, ContentValues to) {
697 String s = from.getAsString(key);
703 private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
704 public ReadOnlyCursorWrapper(Cursor cursor) {
706 mCursor = (CrossProcessCursor) cursor;
709 public boolean deleteRow() {
710 throw new SecurityException("Download manager cursors are read-only");
713 public boolean commitUpdates() {
714 throw new SecurityException("Download manager cursors are read-only");
717 public void fillWindow(int pos, CursorWindow window) {
718 mCursor.fillWindow(pos, window);
721 public CursorWindow getWindow() {
722 return mCursor.getWindow();
725 public boolean onMove(int oldPosition, int newPosition) {
726 return mCursor.onMove(oldPosition, newPosition);
729 private CrossProcessCursor mCursor;