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.app.AppOpsManager;
20 import android.app.DownloadManager;
21 import android.app.DownloadManager.Request;
22 import android.content.ContentProvider;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.UriMatcher;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.database.Cursor;
32 import android.database.DatabaseUtils;
33 import android.database.SQLException;
34 import android.database.sqlite.SQLiteDatabase;
35 import android.database.sqlite.SQLiteOpenHelper;
36 import android.net.Uri;
37 import android.os.Binder;
38 import android.os.Handler;
39 import android.os.HandlerThread;
40 import android.os.ParcelFileDescriptor;
41 import android.os.ParcelFileDescriptor.OnCloseListener;
42 import android.os.Process;
43 import android.provider.BaseColumns;
44 import android.provider.Downloads;
45 import android.provider.OpenableColumns;
46 import android.text.TextUtils;
47 import android.text.format.DateUtils;
48 import android.util.Log;
50 import libcore.io.IoUtils;
52 import com.android.internal.util.IndentingPrintWriter;
53 import com.google.android.collect.Maps;
54 import com.google.common.annotations.VisibleForTesting;
57 import java.io.FileDescriptor;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.PrintWriter;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.HashMap;
64 import java.util.HashSet;
65 import java.util.Iterator;
66 import java.util.List;
70 * Allows application to interact with the download manager.
72 public final class DownloadProvider extends ContentProvider {
73 /** Database filename */
74 private static final String DB_NAME = "downloads.db";
75 /** Current database version */
76 private static final int DB_VERSION = 109;
77 /** Name of table in the database */
78 private static final String DB_TABLE = "downloads";
80 /** MIME type for the entire download list */
81 private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
82 /** MIME type for an individual download */
83 private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
85 /** URI matcher used to recognize URIs sent by applications */
86 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
87 /** URI matcher constant for the URI of all downloads belonging to the calling UID */
88 private static final int MY_DOWNLOADS = 1;
89 /** URI matcher constant for the URI of an individual download belonging to the calling UID */
90 private static final int MY_DOWNLOADS_ID = 2;
91 /** URI matcher constant for the URI of all downloads in the system */
92 private static final int ALL_DOWNLOADS = 3;
93 /** URI matcher constant for the URI of an individual download */
94 private static final int ALL_DOWNLOADS_ID = 4;
95 /** URI matcher constant for the URI of a download's request headers */
96 private static final int REQUEST_HEADERS_URI = 5;
97 /** URI matcher constant for the public URI returned by
98 * {@link DownloadManager#getUriForDownloadedFile(long)} if the given downloaded file
99 * is publicly accessible.
101 private static final int PUBLIC_DOWNLOAD_ID = 6;
103 sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
104 sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
105 sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
106 sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
107 sURIMatcher.addURI("downloads",
108 "my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
109 REQUEST_HEADERS_URI);
110 sURIMatcher.addURI("downloads",
111 "all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
112 REQUEST_HEADERS_URI);
113 // temporary, for backwards compatibility
114 sURIMatcher.addURI("downloads", "download", MY_DOWNLOADS);
115 sURIMatcher.addURI("downloads", "download/#", MY_DOWNLOADS_ID);
116 sURIMatcher.addURI("downloads",
117 "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
118 REQUEST_HEADERS_URI);
119 sURIMatcher.addURI("downloads",
120 Downloads.Impl.PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT + "/#",
124 /** Different base URIs that could be used to access an individual download */
125 private static final Uri[] BASE_URIS = new Uri[] {
126 Downloads.Impl.CONTENT_URI,
127 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
130 private static final String[] sAppReadableColumnsArray = new String[] {
132 Downloads.Impl.COLUMN_APP_DATA,
133 Downloads.Impl._DATA,
134 Downloads.Impl.COLUMN_MIME_TYPE,
135 Downloads.Impl.COLUMN_VISIBILITY,
136 Downloads.Impl.COLUMN_DESTINATION,
137 Downloads.Impl.COLUMN_CONTROL,
138 Downloads.Impl.COLUMN_STATUS,
139 Downloads.Impl.COLUMN_LAST_MODIFICATION,
140 Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
141 Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
142 Downloads.Impl.COLUMN_TOTAL_BYTES,
143 Downloads.Impl.COLUMN_CURRENT_BYTES,
144 Downloads.Impl.COLUMN_TITLE,
145 Downloads.Impl.COLUMN_DESCRIPTION,
146 Downloads.Impl.COLUMN_URI,
147 Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
148 Downloads.Impl.COLUMN_FILE_NAME_HINT,
149 Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
150 Downloads.Impl.COLUMN_DELETED,
151 OpenableColumns.DISPLAY_NAME,
152 OpenableColumns.SIZE,
155 private static final HashSet<String> sAppReadableColumnsSet;
156 private static final HashMap<String, String> sColumnsMap;
159 sAppReadableColumnsSet = new HashSet<String>();
160 for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
161 sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
164 sColumnsMap = Maps.newHashMap();
165 sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
166 Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
167 sColumnsMap.put(OpenableColumns.SIZE,
168 Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
170 private static final List<String> downloadManagerColumnsList =
171 Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
173 private Handler mHandler;
175 /** The database that lies underneath this content provider */
176 private SQLiteOpenHelper mOpenHelper = null;
178 /** List of uids that can access the downloads */
179 private int mSystemUid = -1;
180 private int mDefContainerUid = -1;
183 SystemFacade mSystemFacade;
186 * This class encapsulates a SQL where clause and its parameters. It makes it possible for
187 * shared methods (like {@link DownloadProvider#getWhereClause(Uri, String, String[], int)})
188 * to return both pieces of information, and provides some utility logic to ease piece-by-piece
189 * construction of selections.
191 private static class SqlSelection {
192 public StringBuilder mWhereClause = new StringBuilder();
193 public List<String> mParameters = new ArrayList<String>();
195 public <T> void appendClause(String newClause, final T... parameters) {
196 if (newClause == null || newClause.isEmpty()) {
199 if (mWhereClause.length() != 0) {
200 mWhereClause.append(" AND ");
202 mWhereClause.append("(");
203 mWhereClause.append(newClause);
204 mWhereClause.append(")");
205 if (parameters != null) {
206 for (Object parameter : parameters) {
207 mParameters.add(parameter.toString());
212 public String getSelection() {
213 return mWhereClause.toString();
216 public String[] getParameters() {
217 String[] array = new String[mParameters.size()];
218 return mParameters.toArray(array);
223 * Creates and updated database on demand when opening it.
224 * Helper class to create database the first time the provider is
225 * initialized and upgrade it when a new version of the provider needs
226 * an updated version of the database.
228 private final class DatabaseHelper extends SQLiteOpenHelper {
229 public DatabaseHelper(final Context context) {
230 super(context, DB_NAME, null, DB_VERSION);
234 * Creates database the first time we try to open it.
237 public void onCreate(final SQLiteDatabase db) {
238 if (Constants.LOGVV) {
239 Log.v(Constants.TAG, "populating new database");
241 onUpgrade(db, 0, DB_VERSION);
245 * Updates the database format when a content provider is used
246 * with a database that was created with a different format.
248 * Note: to support downgrades, creating a table should always drop it first if it already
252 public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
254 // 31 and 100 are identical, just in different codelines. Upgrading from 31 is the
255 // same as upgrading from 100.
257 } else if (oldV < 100) {
258 // no logic to upgrade from these older version, just recreate the DB
259 Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV
260 + " to version " + newV + ", which will destroy all old data");
262 } else if (oldV > newV) {
263 // user must have downgraded software; we have no way to know how to downgrade the
264 // DB, so just recreate it
265 Log.i(Constants.TAG, "Downgrading downloads database from version " + oldV
266 + " (current version is " + newV + "), destroying all old data");
270 for (int version = oldV + 1; version <= newV; version++) {
271 upgradeTo(db, version);
276 * Upgrade database from (version - 1) to version.
278 private void upgradeTo(SQLiteDatabase db, int version) {
281 createDownloadsTable(db);
285 createHeadersTable(db);
289 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_PUBLIC_API,
290 "INTEGER NOT NULL DEFAULT 0");
291 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_ROAMING,
292 "INTEGER NOT NULL DEFAULT 0");
293 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES,
294 "INTEGER NOT NULL DEFAULT 0");
298 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
299 "INTEGER NOT NULL DEFAULT 1");
300 makeCacheDownloadsInvisible(db);
304 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT,
305 "INTEGER NOT NULL DEFAULT 0");
313 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, "TEXT");
314 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_DELETED,
315 "BOOLEAN NOT NULL DEFAULT 0");
319 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT");
323 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_METERED,
324 "INTEGER NOT NULL DEFAULT 1");
328 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_WRITE,
329 "BOOLEAN NOT NULL DEFAULT 0");
333 throw new IllegalStateException("Don't know how to upgrade to " + version);
338 * insert() now ensures these four columns are never null for new downloads, so this method
339 * makes that true for existing columns, so that code can rely on this assumption.
341 private void fillNullValues(SQLiteDatabase db) {
342 ContentValues values = new ContentValues();
343 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
344 fillNullValuesForColumn(db, values);
345 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
346 fillNullValuesForColumn(db, values);
347 values.put(Downloads.Impl.COLUMN_TITLE, "");
348 fillNullValuesForColumn(db, values);
349 values.put(Downloads.Impl.COLUMN_DESCRIPTION, "");
350 fillNullValuesForColumn(db, values);
353 private void fillNullValuesForColumn(SQLiteDatabase db, ContentValues values) {
354 String column = values.valueSet().iterator().next().getKey();
355 db.update(DB_TABLE, values, column + " is null", null);
360 * Set all existing downloads to the cache partition to be invisible in the downloads UI.
362 private void makeCacheDownloadsInvisible(SQLiteDatabase db) {
363 ContentValues values = new ContentValues();
364 values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, false);
365 String cacheSelection = Downloads.Impl.COLUMN_DESTINATION
366 + " != " + Downloads.Impl.DESTINATION_EXTERNAL;
367 db.update(DB_TABLE, values, cacheSelection, null);
371 * Add a column to a table using ALTER TABLE.
372 * @param dbTable name of the table
373 * @param columnName name of the column to add
374 * @param columnDefinition SQL for the column definition
376 private void addColumn(SQLiteDatabase db, String dbTable, String columnName,
377 String columnDefinition) {
378 db.execSQL("ALTER TABLE " + dbTable + " ADD COLUMN " + columnName + " "
383 * Creates the table that'll hold the download information.
385 private void createDownloadsTable(SQLiteDatabase db) {
387 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
388 db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
389 Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
390 Downloads.Impl.COLUMN_URI + " TEXT, " +
391 Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
392 Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
393 Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
394 Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
395 Constants.OTA_UPDATE + " BOOLEAN, " +
396 Downloads.Impl._DATA + " TEXT, " +
397 Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
398 Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
399 Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
400 Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
401 Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
402 Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
403 Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " +
404 Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
405 Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
406 Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
407 Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
408 Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
409 Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
410 Downloads.Impl.COLUMN_REFERER + " TEXT, " +
411 Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
412 Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
413 Constants.ETAG + " TEXT, " +
414 Constants.UID + " INTEGER, " +
415 Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
416 Downloads.Impl.COLUMN_TITLE + " TEXT, " +
417 Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
418 Downloads.Impl.COLUMN_MEDIA_SCANNED + " BOOLEAN);");
419 } catch (SQLException ex) {
420 Log.e(Constants.TAG, "couldn't create table in downloads database");
425 private void createHeadersTable(SQLiteDatabase db) {
426 db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE);
427 db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" +
428 "id INTEGER PRIMARY KEY AUTOINCREMENT," +
429 Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," +
430 Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," +
431 Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" +
437 * Initializes the content provider when it is created.
440 public boolean onCreate() {
441 if (mSystemFacade == null) {
442 mSystemFacade = new RealSystemFacade(getContext());
445 HandlerThread handlerThread =
446 new HandlerThread("DownloadProvider handler", Process.THREAD_PRIORITY_BACKGROUND);
447 handlerThread.start();
448 mHandler = new Handler(handlerThread.getLooper());
450 mOpenHelper = new DatabaseHelper(getContext());
451 // Initialize the system uid
452 mSystemUid = Process.SYSTEM_UID;
453 // Initialize the default container uid. Package name hardcoded
455 ApplicationInfo appInfo = null;
457 appInfo = getContext().getPackageManager().
458 getApplicationInfo("com.android.defcontainer", 0);
459 } catch (NameNotFoundException e) {
460 Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
462 if (appInfo != null) {
463 mDefContainerUid = appInfo.uid;
465 // start the DownloadService class. don't wait for the 1st download to be issued.
466 // saves us by getting some initialization code in DownloadService out of the way.
467 Context context = getContext();
468 context.startService(new Intent(context, DownloadService.class));
473 * Returns the content-provider-style MIME types of the various
474 * types accessible through this content provider.
477 public String getType(final Uri uri) {
478 int match = sURIMatcher.match(uri);
481 case ALL_DOWNLOADS: {
482 return DOWNLOAD_LIST_TYPE;
484 case MY_DOWNLOADS_ID:
485 case ALL_DOWNLOADS_ID:
486 case PUBLIC_DOWNLOAD_ID: {
487 // return the mimetype of this id from the database
488 final String id = getDownloadIdFromUri(uri);
489 final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
490 final String mimeType = DatabaseUtils.stringForQuery(db,
491 "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
492 " WHERE " + Downloads.Impl._ID + " = ?",
494 if (TextUtils.isEmpty(mimeType)) {
495 return DOWNLOAD_TYPE;
501 if (Constants.LOGV) {
502 Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
504 throw new IllegalArgumentException("Unknown URI: " + uri);
510 * Inserts a row in the database
513 public Uri insert(final Uri uri, final ContentValues values) {
514 checkInsertPermissions(values);
515 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
517 // note we disallow inserting into ALL_DOWNLOADS
518 int match = sURIMatcher.match(uri);
519 if (match != MY_DOWNLOADS) {
520 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
521 throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
524 // copy some of the input values as it
525 ContentValues filteredValues = new ContentValues();
526 copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
527 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
528 copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
529 copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
530 copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
531 copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
533 boolean isPublicApi =
534 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
536 // validate the destination column
537 Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
539 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
540 != PackageManager.PERMISSION_GRANTED
541 && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
542 || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
543 || dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION)) {
544 throw new SecurityException("setting destination to : " + dest +
545 " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
547 // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
548 // switch to non-purgeable download
549 boolean hasNonPurgeablePermission =
550 getContext().checkCallingOrSelfPermission(
551 Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
552 == PackageManager.PERMISSION_GRANTED;
553 if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
554 && hasNonPurgeablePermission) {
555 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
557 if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
558 checkFileUriDestination(values);
560 } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
561 getContext().enforceCallingOrSelfPermission(
562 android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
563 "No permission to write");
565 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
566 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
567 getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
568 throw new SecurityException("No permission to write");
571 } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
572 getContext().enforcePermission(
573 android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
574 Binder.getCallingPid(), Binder.getCallingUid(),
575 "need ACCESS_CACHE_FILESYSTEM permission to use system cache");
577 filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
580 // validate the visibility column
581 Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
583 if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
584 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
585 Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
587 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
588 Downloads.Impl.VISIBILITY_HIDDEN);
591 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
593 // copy the control column as is
594 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
597 * requests coming from
598 * DownloadManager.addCompletedDownload(String, String, String,
599 * boolean, String, String, long) need special treatment
601 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
602 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
603 // these requests always are marked as 'completed'
604 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
605 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
606 values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
607 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
608 copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
609 copyString(Downloads.Impl._DATA, values, filteredValues);
610 copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
612 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
613 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
614 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
617 // set lastupdate to current time
618 long lastMod = mSystemFacade.currentTimeMillis();
619 filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
621 // use packagename of the caller to set the notification columns
622 String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
623 String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
624 if (pckg != null && (clazz != null || isPublicApi)) {
625 int uid = Binder.getCallingUid();
627 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
628 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
630 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
633 } catch (PackageManager.NameNotFoundException ex) {
634 /* ignored for now */
638 // copy some more columns as is
639 copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
640 copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
641 copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
642 copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
645 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
646 == PackageManager.PERMISSION_GRANTED) {
647 copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
649 filteredValues.put(Constants.UID, Binder.getCallingUid());
650 if (Binder.getCallingUid() == 0) {
651 copyInteger(Constants.UID, values, filteredValues);
654 // copy some more columns as is
655 copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
656 copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
658 // is_visible_in_downloads_ui column
659 if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
660 copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
662 // by default, make external downloads visible in the UI
663 boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL);
664 filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal);
667 // public api requests and networktypes/roaming columns
669 copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
670 copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
671 copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
674 if (Constants.LOGVV) {
675 Log.v(Constants.TAG, "initiating download with UID "
676 + filteredValues.getAsInteger(Constants.UID));
677 if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
678 Log.v(Constants.TAG, "other UID " +
679 filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
683 long rowID = db.insert(DB_TABLE, null, filteredValues);
685 Log.d(Constants.TAG, "couldn't insert into downloads database");
689 insertRequestHeaders(db, rowID, values);
690 notifyContentChanged(uri, match);
692 // Always start service to handle notifications and/or scanning
693 final Context context = getContext();
694 context.startService(new Intent(context, DownloadService.class));
696 return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
700 * Check that the file URI provided for DESTINATION_FILE_URI is valid.
702 private void checkFileUriDestination(ContentValues values) {
703 String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
704 if (fileUri == null) {
705 throw new IllegalArgumentException(
706 "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
708 Uri uri = Uri.parse(fileUri);
709 String scheme = uri.getScheme();
710 if (scheme == null || !scheme.equals("file")) {
711 throw new IllegalArgumentException("Not a file URI: " + uri);
713 final String path = uri.getPath();
715 throw new IllegalArgumentException("Invalid file URI: " + uri);
720 file = new File(path).getCanonicalFile();
721 } catch (IOException e) {
722 throw new SecurityException(e);
725 if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) {
726 // No permissions required for paths belonging to calling package
728 } else if (Helpers.isFilenameValidInExternal(getContext(), file)) {
729 // Otherwise we require write permission
730 getContext().enforceCallingOrSelfPermission(
731 android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
732 "No permission to write to " + file);
734 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
735 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
736 getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
737 throw new SecurityException("No permission to write to " + file);
741 throw new SecurityException("Unsupported path " + file);
746 * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
747 * constraints in the rest of the code. Apps without that may still access this provider through
748 * the public API, but additional restrictions are imposed. We check those restrictions here.
750 * @param values ContentValues provided to insert()
751 * @throws SecurityException if the caller has insufficient permissions
753 private void checkInsertPermissions(ContentValues values) {
754 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS)
755 == PackageManager.PERMISSION_GRANTED) {
759 getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
760 "INTERNET permission is required to use the download manager");
762 // ensure the request fits within the bounds of a public API request
763 // first copy so we can remove values
764 values = new ContentValues(values);
766 // check columns whose values are restricted
767 enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE);
769 // validate the destination column
770 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
771 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
772 /* this row is inserted by
773 * DownloadManager.addCompletedDownload(String, String, String,
774 * boolean, String, String, long)
776 values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
777 values.remove(Downloads.Impl._DATA);
778 values.remove(Downloads.Impl.COLUMN_STATUS);
780 enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION,
781 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE,
782 Downloads.Impl.DESTINATION_FILE_URI,
783 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
785 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
786 == PackageManager.PERMISSION_GRANTED) {
787 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
788 Request.VISIBILITY_HIDDEN,
789 Request.VISIBILITY_VISIBLE,
790 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
791 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
793 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
794 Request.VISIBILITY_VISIBLE,
795 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
796 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
799 // remove the rest of the columns that are allowed (with any value)
800 values.remove(Downloads.Impl.COLUMN_URI);
801 values.remove(Downloads.Impl.COLUMN_TITLE);
802 values.remove(Downloads.Impl.COLUMN_DESCRIPTION);
803 values.remove(Downloads.Impl.COLUMN_MIME_TYPE);
804 values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert()
805 values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
806 values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
807 values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
808 values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
809 values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
810 values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
811 values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
812 Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
813 while (iterator.hasNext()) {
814 String key = iterator.next().getKey();
815 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
820 // any extra columns are extraneous and disallowed
821 if (values.size() > 0) {
822 StringBuilder error = new StringBuilder("Invalid columns in request: ");
823 boolean first = true;
824 for (Map.Entry<String, Object> entry : values.valueSet()) {
828 error.append(entry.getKey());
830 throw new SecurityException(error.toString());
835 * Remove column from values, and throw a SecurityException if the value isn't within the
836 * specified allowedValues.
838 private void enforceAllowedValues(ContentValues values, String column,
839 Object... allowedValues) {
840 Object value = values.get(column);
841 values.remove(column);
842 for (Object allowedValue : allowedValues) {
843 if (value == null && allowedValue == null) {
846 if (value != null && value.equals(allowedValue)) {
850 throw new SecurityException("Invalid value for " + column + ": " + value);
853 private Cursor queryCleared(Uri uri, String[] projection, String selection,
854 String[] selectionArgs, String sort) {
855 final long token = Binder.clearCallingIdentity();
857 return query(uri, projection, selection, selectionArgs, sort);
859 Binder.restoreCallingIdentity(token);
864 * Starts a database query
867 public Cursor query(final Uri uri, String[] projection,
868 final String selection, final String[] selectionArgs,
871 Helpers.validateSelection(selection, sAppReadableColumnsSet);
873 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
875 int match = sURIMatcher.match(uri);
877 if (Constants.LOGV) {
878 Log.v(Constants.TAG, "querying unknown URI: " + uri);
880 throw new IllegalArgumentException("Unknown URI: " + uri);
883 if (match == REQUEST_HEADERS_URI) {
884 if (projection != null || selection != null || sort != null) {
885 throw new UnsupportedOperationException("Request header queries do not support "
886 + "projections, selections or sorting");
888 return queryRequestHeaders(db, uri);
891 SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match);
893 if (shouldRestrictVisibility()) {
894 if (projection == null) {
895 projection = sAppReadableColumnsArray.clone();
897 // check the validity of the columns in projection
898 for (int i = 0; i < projection.length; ++i) {
899 if (!sAppReadableColumnsSet.contains(projection[i]) &&
900 !downloadManagerColumnsList.contains(projection[i])) {
901 throw new IllegalArgumentException(
902 "column " + projection[i] + " is not allowed in queries");
907 for (int i = 0; i < projection.length; i++) {
908 final String newColumn = sColumnsMap.get(projection[i]);
909 if (newColumn != null) {
910 projection[i] = newColumn;
915 if (Constants.LOGVV) {
916 logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
919 Cursor ret = db.query(DB_TABLE, projection, fullSelection.getSelection(),
920 fullSelection.getParameters(), null, null, sort);
923 ret.setNotificationUri(getContext().getContentResolver(), uri);
924 if (Constants.LOGVV) {
926 "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
929 if (Constants.LOGV) {
930 Log.v(Constants.TAG, "query failed in downloads database");
937 private void logVerboseQueryInfo(String[] projection, final String selection,
938 final String[] selectionArgs, final String sort, SQLiteDatabase db) {
939 java.lang.StringBuilder sb = new java.lang.StringBuilder();
940 sb.append("starting query, database is ");
945 if (projection == null) {
946 sb.append("projection is null; ");
947 } else if (projection.length == 0) {
948 sb.append("projection is empty; ");
950 for (int i = 0; i < projection.length; ++i) {
951 sb.append("projection[");
954 sb.append(projection[i]);
958 sb.append("selection is ");
959 sb.append(selection);
961 if (selectionArgs == null) {
962 sb.append("selectionArgs is null; ");
963 } else if (selectionArgs.length == 0) {
964 sb.append("selectionArgs is empty; ");
966 for (int i = 0; i < selectionArgs.length; ++i) {
967 sb.append("selectionArgs[");
970 sb.append(selectionArgs[i]);
974 sb.append("sort is ");
977 Log.v(Constants.TAG, sb.toString());
980 private String getDownloadIdFromUri(final Uri uri) {
981 return uri.getPathSegments().get(1);
985 * Insert request headers for a download into the DB.
987 private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
988 ContentValues rowValues = new ContentValues();
989 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
990 for (Map.Entry<String, Object> entry : values.valueSet()) {
991 String key = entry.getKey();
992 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
993 String headerLine = entry.getValue().toString();
994 if (!headerLine.contains(":")) {
995 throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
997 String[] parts = headerLine.split(":", 2);
998 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
999 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
1000 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
1006 * Handle a query for the custom request headers registered for a download.
1008 private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
1009 String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
1010 + getDownloadIdFromUri(uri);
1011 String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER,
1012 Downloads.Impl.RequestHeaders.COLUMN_VALUE};
1013 return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
1014 null, null, null, null);
1018 * Delete request headers for downloads matching the given query.
1020 private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
1021 String[] projection = new String[] {Downloads.Impl._ID};
1022 Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null);
1024 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1025 long id = cursor.getLong(0);
1026 String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
1027 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
1035 * @return true if we should restrict the columns readable by this caller
1037 private boolean shouldRestrictVisibility() {
1038 int callingUid = Binder.getCallingUid();
1039 return Binder.getCallingPid() != Process.myPid() &&
1040 callingUid != mSystemUid &&
1041 callingUid != mDefContainerUid;
1045 * Updates a row in the database
1048 public int update(final Uri uri, final ContentValues values,
1049 final String where, final String[] whereArgs) {
1051 Helpers.validateSelection(where, sAppReadableColumnsSet);
1053 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1056 boolean startService = false;
1058 if (values.containsKey(Downloads.Impl.COLUMN_DELETED)) {
1059 if (values.getAsInteger(Downloads.Impl.COLUMN_DELETED) == 1) {
1060 // some rows are to be 'deleted'. need to start DownloadService.
1061 startService = true;
1065 ContentValues filteredValues;
1066 if (Binder.getCallingPid() != Process.myPid()) {
1067 filteredValues = new ContentValues();
1068 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
1069 copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues);
1070 Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
1072 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
1073 startService = true;
1076 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
1077 copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
1078 copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues);
1079 copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
1080 copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues);
1082 filteredValues = values;
1083 String filename = values.getAsString(Downloads.Impl._DATA);
1084 if (filename != null) {
1087 c = query(uri, new String[]
1088 { Downloads.Impl.COLUMN_TITLE }, null, null, null);
1089 if (!c.moveToFirst() || c.getString(0).isEmpty()) {
1090 values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
1093 IoUtils.closeQuietly(c);
1097 Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
1098 boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING;
1099 boolean isUserBypassingSizeLimit =
1100 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
1101 if (isRestart || isUserBypassingSizeLimit) {
1102 startService = true;
1106 int match = sURIMatcher.match(uri);
1109 case MY_DOWNLOADS_ID:
1111 case ALL_DOWNLOADS_ID:
1112 SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1113 if (filteredValues.size() > 0) {
1114 count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
1115 selection.getParameters());
1122 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
1123 throw new UnsupportedOperationException("Cannot update URI: " + uri);
1126 notifyContentChanged(uri, match);
1128 Context context = getContext();
1129 context.startService(new Intent(context, DownloadService.class));
1135 * Notify of a change through both URIs (/my_downloads and /all_downloads)
1136 * @param uri either URI for the changed download(s)
1137 * @param uriMatch the match ID from {@link #sURIMatcher}
1139 private void notifyContentChanged(final Uri uri, int uriMatch) {
1140 Long downloadId = null;
1141 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
1142 downloadId = Long.parseLong(getDownloadIdFromUri(uri));
1144 for (Uri uriToNotify : BASE_URIS) {
1145 if (downloadId != null) {
1146 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
1148 getContext().getContentResolver().notifyChange(uriToNotify, null);
1152 private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs,
1154 SqlSelection selection = new SqlSelection();
1155 selection.appendClause(where, whereArgs);
1156 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID ||
1157 uriMatch == PUBLIC_DOWNLOAD_ID) {
1158 selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri));
1160 if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
1161 && getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
1162 != PackageManager.PERMISSION_GRANTED) {
1163 selection.appendClause(
1164 Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?",
1165 Binder.getCallingUid(), Binder.getCallingUid());
1171 * Deletes a row in the database
1174 public int delete(final Uri uri, final String where, final String[] whereArgs) {
1175 if (shouldRestrictVisibility()) {
1176 Helpers.validateSelection(where, sAppReadableColumnsSet);
1179 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1181 int match = sURIMatcher.match(uri);
1184 case MY_DOWNLOADS_ID:
1186 case ALL_DOWNLOADS_ID:
1187 SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1188 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
1190 final Cursor cursor = db.query(DB_TABLE, new String[] {
1191 Downloads.Impl._ID, Downloads.Impl._DATA
1192 }, selection.getSelection(), selection.getParameters(), null, null, null);
1194 while (cursor.moveToNext()) {
1195 final long id = cursor.getLong(0);
1196 DownloadStorageProvider.onDownloadProviderDelete(getContext(), id);
1198 final String path = cursor.getString(1);
1199 if (!TextUtils.isEmpty(path)) {
1201 final File file = new File(path).getCanonicalFile();
1202 if (Helpers.isFilenameValid(getContext(), file)) {
1203 Log.v(Constants.TAG,
1204 "Deleting " + file + " via provider delete");
1207 } catch (IOException ignored) {
1212 IoUtils.closeQuietly(cursor);
1215 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
1219 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
1220 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
1222 notifyContentChanged(uri, match);
1227 * Remotely opens a file
1230 public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
1231 if (Constants.LOGVV) {
1232 logVerboseOpenFileInfo(uri, mode);
1235 final Cursor cursor = queryCleared(uri, new String[] {
1236 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS,
1237 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null,
1240 final boolean shouldScan;
1242 int count = (cursor != null) ? cursor.getCount() : 0;
1244 // If there is not exactly one result, throw an appropriate exception.
1246 throw new FileNotFoundException("No entry for " + uri);
1248 throw new FileNotFoundException("Multiple items at " + uri);
1251 if (cursor.moveToFirst()) {
1252 final int status = cursor.getInt(1);
1253 final int destination = cursor.getInt(2);
1254 final int mediaScanned = cursor.getInt(3);
1256 path = cursor.getString(0);
1257 shouldScan = Downloads.Impl.isStatusSuccess(status) && (
1258 destination == Downloads.Impl.DESTINATION_EXTERNAL
1259 || destination == Downloads.Impl.DESTINATION_FILE_URI
1260 || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
1261 && mediaScanned != 2;
1263 throw new FileNotFoundException("Failed moveToFirst");
1266 IoUtils.closeQuietly(cursor);
1270 throw new FileNotFoundException("No filename found.");
1273 final File file = new File(path);
1274 if (!Helpers.isFilenameValid(getContext(), file)) {
1275 throw new FileNotFoundException("Invalid file: " + file);
1278 final int pfdMode = ParcelFileDescriptor.parseMode(mode);
1279 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) {
1280 return ParcelFileDescriptor.open(file, pfdMode);
1283 // When finished writing, update size and timestamp
1284 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() {
1286 public void onClose(IOException e) {
1287 final ContentValues values = new ContentValues();
1288 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
1289 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
1290 System.currentTimeMillis());
1291 update(uri, values, null, null);
1294 final Intent intent = new Intent(
1295 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
1296 intent.setData(Uri.fromFile(file));
1297 getContext().sendBroadcast(intent);
1301 } catch (IOException e) {
1302 throw new FileNotFoundException("Failed to open for writing: " + e);
1308 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1309 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120);
1311 pw.println("Downloads updated in last hour:");
1312 pw.increaseIndent();
1314 final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1315 final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
1316 final Cursor cursor = db.query(DB_TABLE, null,
1317 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
1318 Downloads.Impl._ID + " ASC");
1320 final String[] cols = cursor.getColumnNames();
1321 final int idCol = cursor.getColumnIndex(BaseColumns._ID);
1322 while (cursor.moveToNext()) {
1323 pw.println("Download #" + cursor.getInt(idCol) + ":");
1324 pw.increaseIndent();
1325 for (int i = 0; i < cols.length; i++) {
1326 // Omit sensitive data when dumping
1327 if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
1330 pw.printPair(cols[i], cursor.getString(i));
1333 pw.decreaseIndent();
1339 pw.decreaseIndent();
1342 private void logVerboseOpenFileInfo(Uri uri, String mode) {
1343 Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
1344 + ", uid: " + Binder.getCallingUid());
1345 Cursor cursor = query(Downloads.Impl.CONTENT_URI,
1346 new String[] { "_id" }, null, null, "_id");
1347 if (cursor == null) {
1348 Log.v(Constants.TAG, "null cursor in openFile");
1351 if (!cursor.moveToFirst()) {
1352 Log.v(Constants.TAG, "empty cursor in openFile");
1355 Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
1356 } while(cursor.moveToNext());
1362 cursor = query(uri, new String[] { "_data" }, null, null, null);
1363 if (cursor == null) {
1364 Log.v(Constants.TAG, "null cursor in openFile");
1367 if (!cursor.moveToFirst()) {
1368 Log.v(Constants.TAG, "empty cursor in openFile");
1370 String filename = cursor.getString(0);
1371 Log.v(Constants.TAG, "filename in openFile: " + filename);
1372 if (new java.io.File(filename).isFile()) {
1373 Log.v(Constants.TAG, "file exists in openFile");
1382 private static final void copyInteger(String key, ContentValues from, ContentValues to) {
1383 Integer i = from.getAsInteger(key);
1389 private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
1390 Boolean b = from.getAsBoolean(key);
1396 private static final void copyString(String key, ContentValues from, ContentValues to) {
1397 String s = from.getAsString(key);
1403 private static final void copyStringWithDefault(String key, ContentValues from,
1404 ContentValues to, String defaultValue) {
1405 copyString(key, from, to);
1406 if (!to.containsKey(key)) {
1407 to.put(key, defaultValue);