• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.downloads;
18 
19 import static android.provider.BaseColumns._ID;
20 import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
21 import static android.provider.Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI;
22 import static android.provider.Downloads.Impl.COLUMN_MEDIASTORE_URI;
23 import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
24 import static android.provider.Downloads.Impl.COLUMN_OTHER_UID;
25 import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
26 import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
27 import static android.provider.Downloads.Impl.MEDIA_NOT_SCANNABLE;
28 import static android.provider.Downloads.Impl.MEDIA_NOT_SCANNED;
29 import static android.provider.Downloads.Impl.MEDIA_SCANNED;
30 import static android.provider.Downloads.Impl.PERMISSION_ACCESS_ALL;
31 
32 import static com.android.providers.downloads.Helpers.convertToMediaStoreDownloadsUri;
33 import static com.android.providers.downloads.Helpers.triggerMediaScan;
34 
35 import android.annotation.NonNull;
36 import android.app.AppOpsManager;
37 import android.app.DownloadManager;
38 import android.app.DownloadManager.Request;
39 import android.app.job.JobScheduler;
40 import android.content.ContentProvider;
41 import android.content.ContentProviderClient;
42 import android.content.ContentResolver;
43 import android.content.ContentUris;
44 import android.content.ContentValues;
45 import android.content.Context;
46 import android.content.Intent;
47 import android.content.UriMatcher;
48 import android.content.pm.ApplicationInfo;
49 import android.content.pm.PackageManager;
50 import android.database.Cursor;
51 import android.database.DatabaseUtils;
52 import android.database.SQLException;
53 import android.database.sqlite.SQLiteDatabase;
54 import android.database.sqlite.SQLiteOpenHelper;
55 import android.database.sqlite.SQLiteQueryBuilder;
56 import android.net.Uri;
57 import android.os.Binder;
58 import android.os.Build;
59 import android.os.Bundle;
60 import android.os.Environment;
61 import android.os.ParcelFileDescriptor;
62 import android.os.ParcelFileDescriptor.OnCloseListener;
63 import android.os.Process;
64 import android.os.RemoteException;
65 import android.os.storage.StorageManager;
66 import android.provider.BaseColumns;
67 import android.provider.Downloads;
68 import android.provider.MediaStore;
69 import android.provider.OpenableColumns;
70 import android.text.TextUtils;
71 import android.text.format.DateUtils;
72 import android.util.ArrayMap;
73 import android.util.Log;
74 
75 import com.android.internal.util.ArrayUtils;
76 import com.android.internal.util.IndentingPrintWriter;
77 
78 import libcore.io.IoUtils;
79 
80 import com.google.common.annotations.VisibleForTesting;
81 
82 import java.io.File;
83 import java.io.FileDescriptor;
84 import java.io.FileNotFoundException;
85 import java.io.IOException;
86 import java.io.PrintWriter;
87 import java.util.Iterator;
88 import java.util.Map;
89 
90 /**
91  * Allows application to interact with the download manager.
92  */
93 public final class DownloadProvider extends ContentProvider {
94     /** Database filename */
95     private static final String DB_NAME = "downloads.db";
96     /** Current database version */
97     private static final int DB_VERSION = 114;
98     /** Name of table in the database */
99     private static final String DB_TABLE = "downloads";
100     /** Memory optimization - close idle connections after 30s of inactivity */
101     private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
102 
103     /** MIME type for the entire download list */
104     private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
105     /** MIME type for an individual download */
106     private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
107 
108     /** URI matcher used to recognize URIs sent by applications */
109     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
110     /** URI matcher constant for the URI of all downloads belonging to the calling UID */
111     private static final int MY_DOWNLOADS = 1;
112     /** URI matcher constant for the URI of an individual download belonging to the calling UID */
113     private static final int MY_DOWNLOADS_ID = 2;
114     /** URI matcher constant for the URI of a download's request headers */
115     private static final int MY_DOWNLOADS_ID_HEADERS = 3;
116     /** URI matcher constant for the URI of all downloads in the system */
117     private static final int ALL_DOWNLOADS = 4;
118     /** URI matcher constant for the URI of an individual download */
119     private static final int ALL_DOWNLOADS_ID = 5;
120     /** URI matcher constant for the URI of a download's request headers */
121     private static final int ALL_DOWNLOADS_ID_HEADERS = 6;
122     static {
123         sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
124         sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
125         sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
126         sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
127         sURIMatcher.addURI("downloads",
128                 "my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
129                 MY_DOWNLOADS_ID_HEADERS);
130         sURIMatcher.addURI("downloads",
131                 "all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
132                 ALL_DOWNLOADS_ID_HEADERS);
133         // temporary, for backwards compatibility
134         sURIMatcher.addURI("downloads", "download", MY_DOWNLOADS);
135         sURIMatcher.addURI("downloads", "download/#", MY_DOWNLOADS_ID);
136         sURIMatcher.addURI("downloads",
137                 "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
138                 MY_DOWNLOADS_ID_HEADERS);
139     }
140 
141     /** Different base URIs that could be used to access an individual download */
142     private static final Uri[] BASE_URIS = new Uri[] {
143             Downloads.Impl.CONTENT_URI,
144             Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
145     };
146 
addMapping(Map<String, String> map, String column)147     private static void addMapping(Map<String, String> map, String column) {
148         if (!map.containsKey(column)) {
149             map.put(column, column);
150         }
151     }
152 
addMapping(Map<String, String> map, String column, String rawColumn)153     private static void addMapping(Map<String, String> map, String column, String rawColumn) {
154         if (!map.containsKey(column)) {
155             map.put(column, rawColumn + " AS " + column);
156         }
157     }
158 
159     private static final Map<String, String> sDownloadsMap = new ArrayMap<>();
160     static {
161         final Map<String, String> map = sDownloadsMap;
162 
163         // Columns defined by public API
addMapping(map, DownloadManager.COLUMN_ID, Downloads.Impl._ID)164         addMapping(map, DownloadManager.COLUMN_ID,
165                 Downloads.Impl._ID);
addMapping(map, DownloadManager.COLUMN_LOCAL_FILENAME, Downloads.Impl._DATA)166         addMapping(map, DownloadManager.COLUMN_LOCAL_FILENAME,
167                 Downloads.Impl._DATA);
addMapping(map, DownloadManager.COLUMN_MEDIAPROVIDER_URI)168         addMapping(map, DownloadManager.COLUMN_MEDIAPROVIDER_URI);
addMapping(map, DownloadManager.COLUMN_DESTINATION)169         addMapping(map, DownloadManager.COLUMN_DESTINATION);
addMapping(map, DownloadManager.COLUMN_TITLE)170         addMapping(map, DownloadManager.COLUMN_TITLE);
addMapping(map, DownloadManager.COLUMN_DESCRIPTION)171         addMapping(map, DownloadManager.COLUMN_DESCRIPTION);
addMapping(map, DownloadManager.COLUMN_URI)172         addMapping(map, DownloadManager.COLUMN_URI);
addMapping(map, DownloadManager.COLUMN_STATUS)173         addMapping(map, DownloadManager.COLUMN_STATUS);
addMapping(map, DownloadManager.COLUMN_FILE_NAME_HINT)174         addMapping(map, DownloadManager.COLUMN_FILE_NAME_HINT);
addMapping(map, DownloadManager.COLUMN_MEDIA_TYPE, Downloads.Impl.COLUMN_MIME_TYPE)175         addMapping(map, DownloadManager.COLUMN_MEDIA_TYPE,
176                 Downloads.Impl.COLUMN_MIME_TYPE);
addMapping(map, DownloadManager.COLUMN_TOTAL_SIZE_BYTES, Downloads.Impl.COLUMN_TOTAL_BYTES)177         addMapping(map, DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
178                 Downloads.Impl.COLUMN_TOTAL_BYTES);
addMapping(map, DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP, Downloads.Impl.COLUMN_LAST_MODIFICATION)179         addMapping(map, DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP,
180                 Downloads.Impl.COLUMN_LAST_MODIFICATION);
addMapping(map, DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR, Downloads.Impl.COLUMN_CURRENT_BYTES)181         addMapping(map, DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR,
182                 Downloads.Impl.COLUMN_CURRENT_BYTES);
addMapping(map, DownloadManager.COLUMN_ALLOW_WRITE)183         addMapping(map, DownloadManager.COLUMN_ALLOW_WRITE);
addMapping(map, DownloadManager.COLUMN_LOCAL_URI, "'placeholder'")184         addMapping(map, DownloadManager.COLUMN_LOCAL_URI,
185                 "'placeholder'");
addMapping(map, DownloadManager.COLUMN_REASON, "'placeholder'")186         addMapping(map, DownloadManager.COLUMN_REASON,
187                 "'placeholder'");
188 
189         // Columns defined by OpenableColumns
addMapping(map, OpenableColumns.DISPLAY_NAME, Downloads.Impl.COLUMN_TITLE)190         addMapping(map, OpenableColumns.DISPLAY_NAME,
191                 Downloads.Impl.COLUMN_TITLE);
addMapping(map, OpenableColumns.SIZE, Downloads.Impl.COLUMN_TOTAL_BYTES)192         addMapping(map, OpenableColumns.SIZE,
193                 Downloads.Impl.COLUMN_TOTAL_BYTES);
194 
195         // Allow references to all other columns to support DownloadInfo.Reader;
196         // we're already using SQLiteQueryBuilder to block access to other rows
197         // that don't belong to the calling UID.
addMapping(map, Downloads.Impl._ID)198         addMapping(map, Downloads.Impl._ID);
addMapping(map, Downloads.Impl._DATA)199         addMapping(map, Downloads.Impl._DATA);
addMapping(map, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES)200         addMapping(map, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
addMapping(map, Downloads.Impl.COLUMN_ALLOW_METERED)201         addMapping(map, Downloads.Impl.COLUMN_ALLOW_METERED);
addMapping(map, Downloads.Impl.COLUMN_ALLOW_ROAMING)202         addMapping(map, Downloads.Impl.COLUMN_ALLOW_ROAMING);
addMapping(map, Downloads.Impl.COLUMN_ALLOW_WRITE)203         addMapping(map, Downloads.Impl.COLUMN_ALLOW_WRITE);
addMapping(map, Downloads.Impl.COLUMN_APP_DATA)204         addMapping(map, Downloads.Impl.COLUMN_APP_DATA);
addMapping(map, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT)205         addMapping(map, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
addMapping(map, Downloads.Impl.COLUMN_CONTROL)206         addMapping(map, Downloads.Impl.COLUMN_CONTROL);
addMapping(map, Downloads.Impl.COLUMN_COOKIE_DATA)207         addMapping(map, Downloads.Impl.COLUMN_COOKIE_DATA);
addMapping(map, Downloads.Impl.COLUMN_CURRENT_BYTES)208         addMapping(map, Downloads.Impl.COLUMN_CURRENT_BYTES);
addMapping(map, Downloads.Impl.COLUMN_DELETED)209         addMapping(map, Downloads.Impl.COLUMN_DELETED);
addMapping(map, Downloads.Impl.COLUMN_DESCRIPTION)210         addMapping(map, Downloads.Impl.COLUMN_DESCRIPTION);
addMapping(map, Downloads.Impl.COLUMN_DESTINATION)211         addMapping(map, Downloads.Impl.COLUMN_DESTINATION);
addMapping(map, Downloads.Impl.COLUMN_ERROR_MSG)212         addMapping(map, Downloads.Impl.COLUMN_ERROR_MSG);
addMapping(map, Downloads.Impl.COLUMN_FAILED_CONNECTIONS)213         addMapping(map, Downloads.Impl.COLUMN_FAILED_CONNECTIONS);
addMapping(map, Downloads.Impl.COLUMN_FILE_NAME_HINT)214         addMapping(map, Downloads.Impl.COLUMN_FILE_NAME_HINT);
addMapping(map, Downloads.Impl.COLUMN_FLAGS)215         addMapping(map, Downloads.Impl.COLUMN_FLAGS);
addMapping(map, Downloads.Impl.COLUMN_IS_PUBLIC_API)216         addMapping(map, Downloads.Impl.COLUMN_IS_PUBLIC_API);
addMapping(map, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)217         addMapping(map, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
addMapping(map, Downloads.Impl.COLUMN_LAST_MODIFICATION)218         addMapping(map, Downloads.Impl.COLUMN_LAST_MODIFICATION);
addMapping(map, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI)219         addMapping(map, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
addMapping(map, Downloads.Impl.COLUMN_MEDIA_SCANNED)220         addMapping(map, Downloads.Impl.COLUMN_MEDIA_SCANNED);
addMapping(map, Downloads.Impl.COLUMN_MEDIASTORE_URI)221         addMapping(map, Downloads.Impl.COLUMN_MEDIASTORE_URI);
addMapping(map, Downloads.Impl.COLUMN_MIME_TYPE)222         addMapping(map, Downloads.Impl.COLUMN_MIME_TYPE);
addMapping(map, Downloads.Impl.COLUMN_NO_INTEGRITY)223         addMapping(map, Downloads.Impl.COLUMN_NO_INTEGRITY);
addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_CLASS)224         addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS)225         addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE)226         addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
addMapping(map, Downloads.Impl.COLUMN_OTHER_UID)227         addMapping(map, Downloads.Impl.COLUMN_OTHER_UID);
addMapping(map, Downloads.Impl.COLUMN_REFERER)228         addMapping(map, Downloads.Impl.COLUMN_REFERER);
addMapping(map, Downloads.Impl.COLUMN_STATUS)229         addMapping(map, Downloads.Impl.COLUMN_STATUS);
addMapping(map, Downloads.Impl.COLUMN_TITLE)230         addMapping(map, Downloads.Impl.COLUMN_TITLE);
addMapping(map, Downloads.Impl.COLUMN_TOTAL_BYTES)231         addMapping(map, Downloads.Impl.COLUMN_TOTAL_BYTES);
addMapping(map, Downloads.Impl.COLUMN_URI)232         addMapping(map, Downloads.Impl.COLUMN_URI);
addMapping(map, Downloads.Impl.COLUMN_USER_AGENT)233         addMapping(map, Downloads.Impl.COLUMN_USER_AGENT);
addMapping(map, Downloads.Impl.COLUMN_VISIBILITY)234         addMapping(map, Downloads.Impl.COLUMN_VISIBILITY);
235 
addMapping(map, Constants.ETAG)236         addMapping(map, Constants.ETAG);
addMapping(map, Constants.RETRY_AFTER_X_REDIRECT_COUNT)237         addMapping(map, Constants.RETRY_AFTER_X_REDIRECT_COUNT);
addMapping(map, Constants.UID)238         addMapping(map, Constants.UID);
239     }
240 
241     private static final Map<String, String> sHeadersMap = new ArrayMap<>();
242     static {
243         final Map<String, String> map = sHeadersMap;
addMapping(map, "id")244         addMapping(map, "id");
addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID)245         addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID);
addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_HEADER)246         addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_HEADER);
addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_VALUE)247         addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_VALUE);
248     }
249 
250     @VisibleForTesting
251     SystemFacade mSystemFacade;
252 
253     /** The database that lies underneath this content provider */
254     private SQLiteOpenHelper mOpenHelper = null;
255 
256     /** List of uids that can access the downloads */
257     private int mSystemUid = -1;
258 
259     private StorageManager mStorageManager;
260 
261     /**
262      * Creates and updated database on demand when opening it.
263      * Helper class to create database the first time the provider is
264      * initialized and upgrade it when a new version of the provider needs
265      * an updated version of the database.
266      */
267     private final class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(final Context context)268         public DatabaseHelper(final Context context) {
269             super(context, DB_NAME, null, DB_VERSION);
270             setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
271         }
272 
273         /**
274          * Creates database the first time we try to open it.
275          */
276         @Override
onCreate(final SQLiteDatabase db)277         public void onCreate(final SQLiteDatabase db) {
278             if (Constants.LOGVV) {
279                 Log.v(Constants.TAG, "populating new database");
280             }
281             onUpgrade(db, 0, DB_VERSION);
282         }
283 
284         /**
285          * Updates the database format when a content provider is used
286          * with a database that was created with a different format.
287          *
288          * Note: to support downgrades, creating a table should always drop it first if it already
289          * exists.
290          */
291         @Override
onUpgrade(final SQLiteDatabase db, int oldV, final int newV)292         public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
293             if (oldV == 31) {
294                 // 31 and 100 are identical, just in different codelines. Upgrading from 31 is the
295                 // same as upgrading from 100.
296                 oldV = 100;
297             } else if (oldV < 100) {
298                 // no logic to upgrade from these older version, just recreate the DB
299                 Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV
300                       + " to version " + newV + ", which will destroy all old data");
301                 oldV = 99;
302             } else if (oldV > newV) {
303                 // user must have downgraded software; we have no way to know how to downgrade the
304                 // DB, so just recreate it
305                 Log.i(Constants.TAG, "Downgrading downloads database from version " + oldV
306                       + " (current version is " + newV + "), destroying all old data");
307                 oldV = 99;
308             }
309 
310             for (int version = oldV + 1; version <= newV; version++) {
311                 upgradeTo(db, version);
312             }
313         }
314 
315         /**
316          * Upgrade database from (version - 1) to version.
317          */
upgradeTo(SQLiteDatabase db, int version)318         private void upgradeTo(SQLiteDatabase db, int version) {
319             boolean scheduleMediaScanTriggerJob = false;
320             switch (version) {
321                 case 100:
322                     createDownloadsTable(db);
323                     break;
324 
325                 case 101:
326                     createHeadersTable(db);
327                     break;
328 
329                 case 102:
330                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_PUBLIC_API,
331                               "INTEGER NOT NULL DEFAULT 0");
332                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_ROAMING,
333                               "INTEGER NOT NULL DEFAULT 0");
334                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES,
335                               "INTEGER NOT NULL DEFAULT 0");
336                     break;
337 
338                 case 103:
339                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
340                               "INTEGER NOT NULL DEFAULT 1");
341                     makeCacheDownloadsInvisible(db);
342                     break;
343 
344                 case 104:
345                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT,
346                             "INTEGER NOT NULL DEFAULT 0");
347                     break;
348 
349                 case 105:
350                     fillNullValues(db);
351                     break;
352 
353                 case 106:
354                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, "TEXT");
355                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_DELETED,
356                             "BOOLEAN NOT NULL DEFAULT 0");
357                     break;
358 
359                 case 107:
360                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT");
361                     break;
362 
363                 case 108:
364                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_METERED,
365                             "INTEGER NOT NULL DEFAULT 1");
366                     break;
367 
368                 case 109:
369                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_WRITE,
370                             "BOOLEAN NOT NULL DEFAULT 0");
371                     break;
372 
373                 case 110:
374                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_FLAGS,
375                             "INTEGER NOT NULL DEFAULT 0");
376                     break;
377 
378                 case 111:
379                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIASTORE_URI,
380                             "TEXT DEFAULT NULL");
381                     scheduleMediaScanTriggerJob = true;
382                     break;
383 
384                 case 112:
385                     updateMediaStoreUrisFromFilesToDownloads(db);
386                     break;
387 
388                 case 113:
389                     canonicalizeDataPaths(db);
390                     break;
391 
392                 case 114:
393                     nullifyMediaStoreUris(db);
394                     scheduleMediaScanTriggerJob = true;
395                     break;
396 
397                 default:
398                     throw new IllegalStateException("Don't know how to upgrade to " + version);
399             }
400             if (scheduleMediaScanTriggerJob) {
401                 MediaScanTriggerJob.schedule(getContext());
402             }
403         }
404 
405         /**
406          * insert() now ensures these four columns are never null for new downloads, so this method
407          * makes that true for existing columns, so that code can rely on this assumption.
408          */
fillNullValues(SQLiteDatabase db)409         private void fillNullValues(SQLiteDatabase db) {
410             ContentValues values = new ContentValues();
411             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
412             fillNullValuesForColumn(db, values);
413             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
414             fillNullValuesForColumn(db, values);
415             values.put(Downloads.Impl.COLUMN_TITLE, "");
416             fillNullValuesForColumn(db, values);
417             values.put(Downloads.Impl.COLUMN_DESCRIPTION, "");
418             fillNullValuesForColumn(db, values);
419         }
420 
fillNullValuesForColumn(SQLiteDatabase db, ContentValues values)421         private void fillNullValuesForColumn(SQLiteDatabase db, ContentValues values) {
422             String column = values.valueSet().iterator().next().getKey();
423             db.update(DB_TABLE, values, column + " is null", null);
424             values.clear();
425         }
426 
427         /**
428          * Set all existing downloads to the cache partition to be invisible in the downloads UI.
429          */
makeCacheDownloadsInvisible(SQLiteDatabase db)430         private void makeCacheDownloadsInvisible(SQLiteDatabase db) {
431             ContentValues values = new ContentValues();
432             values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, false);
433             String cacheSelection = Downloads.Impl.COLUMN_DESTINATION
434                     + " != " + Downloads.Impl.DESTINATION_EXTERNAL;
435             db.update(DB_TABLE, values, cacheSelection, null);
436         }
437 
438         /**
439          * DownloadProvider has been updated to use MediaStore.Downloads based uris
440          * for COLUMN_MEDIASTORE_URI but the existing entries would still have MediaStore.Files
441          * based uris. It's possible that in the future we might incorrectly assume that all the
442          * uris are MediaStore.DownloadColumns based and end up querying some
443          * MediaStore.Downloads specific columns. To avoid this, update the existing entries to
444          * use MediaStore.Downloads based uris only.
445          */
updateMediaStoreUrisFromFilesToDownloads(SQLiteDatabase db)446         private void updateMediaStoreUrisFromFilesToDownloads(SQLiteDatabase db) {
447             try (Cursor cursor = db.query(DB_TABLE,
448                     new String[] { Downloads.Impl._ID, COLUMN_MEDIASTORE_URI },
449                     COLUMN_MEDIASTORE_URI + " IS NOT NULL", null, null, null, null)) {
450                 final ContentValues updateValues = new ContentValues();
451                 while (cursor.moveToNext()) {
452                     final long id = cursor.getLong(0);
453                     final Uri mediaStoreFilesUri = Uri.parse(cursor.getString(1));
454 
455                     final long mediaStoreId = ContentUris.parseId(mediaStoreFilesUri);
456                     final String volumeName = MediaStore.getVolumeName(mediaStoreFilesUri);
457                     final Uri mediaStoreDownloadsUri
458                             = MediaStore.Downloads.getContentUri(volumeName, mediaStoreId);
459 
460                     updateValues.clear();
461                     updateValues.put(COLUMN_MEDIASTORE_URI, mediaStoreDownloadsUri.toString());
462                     db.update(DB_TABLE, updateValues, Downloads.Impl._ID + "=?",
463                             new String[] { Long.toString(id) });
464                 }
465             }
466         }
467 
canonicalizeDataPaths(SQLiteDatabase db)468         private void canonicalizeDataPaths(SQLiteDatabase db) {
469             try (Cursor cursor = db.query(DB_TABLE,
470                     new String[] { Downloads.Impl._ID, Downloads.Impl._DATA},
471                     Downloads.Impl._DATA + " IS NOT NULL", null, null, null, null)) {
472                 final ContentValues updateValues = new ContentValues();
473                 while (cursor.moveToNext()) {
474                     final long id = cursor.getLong(0);
475                     final String filePath = cursor.getString(1);
476                     final String canonicalPath;
477                     try {
478                         canonicalPath = new File(filePath).getCanonicalPath();
479                     } catch (IOException e) {
480                         Log.e(Constants.TAG, "Found invalid path='" + filePath + "' for id=" + id);
481                         continue;
482                     }
483 
484                     updateValues.clear();
485                     updateValues.put(Downloads.Impl._DATA, canonicalPath);
486                     db.update(DB_TABLE, updateValues, Downloads.Impl._ID + "=?",
487                             new String[] { Long.toString(id) });
488                 }
489             }
490         }
491 
492         /**
493          * Set mediastore uri column to null before the clean-up job and fill it again while
494          * running the job so that if the clean-up job gets preempted, we could use it
495          * as a way to know the entries which are already handled when the job gets restarted.
496          */
nullifyMediaStoreUris(SQLiteDatabase db)497         private void nullifyMediaStoreUris(SQLiteDatabase db) {
498             final String whereClause = Downloads.Impl._DATA + " IS NOT NULL"
499                     + " AND (" + COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + "=1"
500                     + " OR " + COLUMN_MEDIA_SCANNED + "=" + MEDIA_SCANNED + ")"
501                     + " AND (" + COLUMN_DESTINATION + "=" + Downloads.Impl.DESTINATION_EXTERNAL
502                     + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_FILE_URI
503                     + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
504                     + ")";
505             final ContentValues values = new ContentValues();
506             values.putNull(COLUMN_MEDIASTORE_URI);
507             db.update(DB_TABLE, values, whereClause, null);
508         }
509 
510         /**
511          * Add a column to a table using ALTER TABLE.
512          * @param dbTable name of the table
513          * @param columnName name of the column to add
514          * @param columnDefinition SQL for the column definition
515          */
addColumn(SQLiteDatabase db, String dbTable, String columnName, String columnDefinition)516         private void addColumn(SQLiteDatabase db, String dbTable, String columnName,
517                                String columnDefinition) {
518             db.execSQL("ALTER TABLE " + dbTable + " ADD COLUMN " + columnName + " "
519                        + columnDefinition);
520         }
521 
522         /**
523          * Creates the table that'll hold the download information.
524          */
createDownloadsTable(SQLiteDatabase db)525         private void createDownloadsTable(SQLiteDatabase db) {
526             try {
527                 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
528                 db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
529                         Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
530                         Downloads.Impl.COLUMN_URI + " TEXT, " +
531                         Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
532                         Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
533                         Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
534                         Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
535                         Constants.OTA_UPDATE + " BOOLEAN, " +
536                         Downloads.Impl._DATA + " TEXT, " +
537                         Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
538                         Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
539                         Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
540                         Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
541                         Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
542                         Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
543                         Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " +
544                         Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
545                         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
546                         Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
547                         Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
548                         Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
549                         Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
550                         Downloads.Impl.COLUMN_REFERER + " TEXT, " +
551                         Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
552                         Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
553                         Constants.ETAG + " TEXT, " +
554                         Constants.UID + " INTEGER, " +
555                         Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
556                         Downloads.Impl.COLUMN_TITLE + " TEXT, " +
557                         Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
558                         Downloads.Impl.COLUMN_MEDIA_SCANNED + " BOOLEAN);");
559             } catch (SQLException ex) {
560                 Log.e(Constants.TAG, "couldn't create table in downloads database");
561                 throw ex;
562             }
563         }
564 
createHeadersTable(SQLiteDatabase db)565         private void createHeadersTable(SQLiteDatabase db) {
566             db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE);
567             db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" +
568                        "id INTEGER PRIMARY KEY AUTOINCREMENT," +
569                        Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," +
570                        Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," +
571                        Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" +
572                        ");");
573         }
574     }
575 
576     /**
577      * Initializes the content provider when it is created.
578      */
579     @Override
onCreate()580     public boolean onCreate() {
581         if (mSystemFacade == null) {
582             mSystemFacade = new RealSystemFacade(getContext());
583         }
584 
585         mOpenHelper = new DatabaseHelper(getContext());
586         // Initialize the system uid
587         mSystemUid = Process.SYSTEM_UID;
588 
589         mStorageManager = getContext().getSystemService(StorageManager.class);
590 
591         // Grant access permissions for all known downloads to the owning apps.
592         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
593         try (Cursor cursor = db.query(DB_TABLE,
594                 new String[] { _ID, Constants.UID }, null, null, null, null, null)) {
595             while (cursor.moveToNext()) {
596                 final long id = cursor.getLong(0);
597                 final int uid = cursor.getInt(1);
598                 final String[] packageNames = getContext().getPackageManager()
599                         .getPackagesForUid(uid);
600                 // Potentially stale download, will be deleted after MEDIA_MOUNTED broadcast
601                 // is received.
602                 if (ArrayUtils.isEmpty(packageNames)) {
603                     continue;
604                 }
605                 // We only need to grant to the first package, since the
606                 // platform internally tracks based on UIDs.
607                 grantAllDownloadsPermission(packageNames[0], id);
608             }
609         }
610         return true;
611     }
612 
613     /**
614      * Returns the content-provider-style MIME types of the various
615      * types accessible through this content provider.
616      */
617     @Override
getType(final Uri uri)618     public String getType(final Uri uri) {
619         int match = sURIMatcher.match(uri);
620         switch (match) {
621             case MY_DOWNLOADS:
622             case ALL_DOWNLOADS: {
623                 return DOWNLOAD_LIST_TYPE;
624             }
625             case MY_DOWNLOADS_ID:
626             case ALL_DOWNLOADS_ID: {
627                 // return the mimetype of this id from the database
628                 final String id = getDownloadIdFromUri(uri);
629                 final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
630                 final String mimeType = DatabaseUtils.stringForQuery(db,
631                         "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
632                         " WHERE " + Downloads.Impl._ID + " = ?",
633                         new String[]{id});
634                 if (TextUtils.isEmpty(mimeType)) {
635                     return DOWNLOAD_TYPE;
636                 } else {
637                     return mimeType;
638                 }
639             }
640             default: {
641                 if (Constants.LOGV) {
642                     Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
643                 }
644                 throw new IllegalArgumentException("Unknown URI: " + uri);
645             }
646         }
647     }
648 
649     @Override
call(String method, String arg, Bundle extras)650     public Bundle call(String method, String arg, Bundle extras) {
651         switch (method) {
652             case Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED: {
653                 getContext().enforceCallingOrSelfPermission(
654                         android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG);
655                 final long[] deletedDownloadIds = extras.getLongArray(Downloads.EXTRA_IDS);
656                 final String[] mimeTypes = extras.getStringArray(Downloads.EXTRA_MIME_TYPES);
657                 DownloadStorageProvider.onMediaProviderDownloadsDelete(getContext(),
658                         deletedDownloadIds, mimeTypes);
659                 return null;
660             }
661             case Downloads.CALL_CREATE_EXTERNAL_PUBLIC_DIR: {
662                 final String dirType = extras.getString(Downloads.DIR_TYPE);
663                 if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, dirType)) {
664                     throw new IllegalStateException("Not one of standard directories: " + dirType);
665                 }
666                 final File file = Environment.getExternalStoragePublicDirectory(dirType);
667                 if (file.exists()) {
668                     if (!file.isDirectory()) {
669                         throw new IllegalStateException(file.getAbsolutePath() +
670                                 " already exists and is not a directory");
671                     }
672                 } else if (!file.mkdirs()) {
673                     throw new IllegalStateException("Unable to create directory: " +
674                             file.getAbsolutePath());
675                 }
676                 return null;
677             }
678             case Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS : {
679                 getContext().enforceCallingOrSelfPermission(
680                         android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG);
681                 DownloadStorageProvider.revokeAllMediaStoreUriPermissions(getContext());
682                 return null;
683             }
684             default:
685                 throw new UnsupportedOperationException("Unsupported call: " + method);
686         }
687     }
688 
689     /**
690      * Inserts a row in the database
691      */
692     @Override
insert(final Uri uri, final ContentValues values)693     public Uri insert(final Uri uri, final ContentValues values) {
694         checkInsertPermissions(values);
695         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
696 
697         // note we disallow inserting into ALL_DOWNLOADS
698         int match = sURIMatcher.match(uri);
699         if (match != MY_DOWNLOADS) {
700             Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
701             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
702         }
703 
704         ContentValues filteredValues = new ContentValues();
705 
706         boolean isPublicApi =
707                 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
708 
709         // validate the destination column
710         Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
711         if (dest != null) {
712             if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
713                     != PackageManager.PERMISSION_GRANTED
714                     && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
715                             || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING)) {
716                 throw new SecurityException("setting destination to : " + dest +
717                         " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
718             }
719             // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
720             // switch to non-purgeable download
721             boolean hasNonPurgeablePermission =
722                     getContext().checkCallingOrSelfPermission(
723                             Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
724                             == PackageManager.PERMISSION_GRANTED;
725             if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
726                     && hasNonPurgeablePermission) {
727                 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
728             }
729             if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
730                 checkFileUriDestination(values);
731             } else if (dest == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
732                 checkDownloadedFilePath(values);
733             } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
734                 getContext().enforceCallingOrSelfPermission(
735                         android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
736                         "No permission to write");
737 
738                 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
739                 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
740                         Binder.getCallingUid(), getCallingAttributionTag(), null)
741                         != AppOpsManager.MODE_ALLOWED) {
742                     throw new SecurityException("No permission to write");
743                 }
744             }
745 
746             filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
747         }
748 
749         ensureDefaultColumns(values);
750 
751         // copy some of the input values as is
752         copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
753         copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
754         copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
755         copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
756         copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
757         copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
758 
759         // validate the visibility column
760         Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
761         if (vis == null) {
762             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
763                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
764                         Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
765             } else {
766                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
767                         Downloads.Impl.VISIBILITY_HIDDEN);
768             }
769         } else {
770             filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
771         }
772         // copy the control column as is
773         copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
774 
775         /*
776          * requests coming from
777          * DownloadManager.addCompletedDownload(String, String, String,
778          * boolean, String, String, long) need special treatment
779          */
780         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
781                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
782             // these requests always are marked as 'completed'
783             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
784             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
785                     values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
786             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
787             copyString(Downloads.Impl._DATA, values, filteredValues);
788             copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
789         } else {
790             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
791             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
792             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
793         }
794 
795         // set lastupdate to current time
796         long lastMod = mSystemFacade.currentTimeMillis();
797         filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
798 
799         // use packagename of the caller to set the notification columns
800         String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
801         String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
802         if (pckg != null && (clazz != null || isPublicApi)) {
803             int uid = Binder.getCallingUid();
804             try {
805                 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
806                     filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
807                     if (clazz != null) {
808                         filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
809                     }
810                 }
811             } catch (PackageManager.NameNotFoundException ex) {
812                 /* ignored for now */
813             }
814         }
815 
816         // copy some more columns as is
817         copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
818         copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
819         copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
820         copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
821 
822         // UID, PID columns
823         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
824                 == PackageManager.PERMISSION_GRANTED) {
825             copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
826         }
827         filteredValues.put(Constants.UID, Binder.getCallingUid());
828         if (Binder.getCallingUid() == 0) {
829             copyInteger(Constants.UID, values, filteredValues);
830         }
831 
832         // copy some more columns as is
833         copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
834         copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
835 
836         // is_visible_in_downloads_ui column
837         copyBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
838 
839         // public api requests and networktypes/roaming columns
840         if (isPublicApi) {
841             copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
842             copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
843             copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
844             copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues);
845         }
846 
847         final Integer mediaScanned = values.getAsInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED);
848         filteredValues.put(COLUMN_MEDIA_SCANNED,
849                 mediaScanned == null ? MEDIA_NOT_SCANNED : mediaScanned);
850 
851         final boolean shouldBeVisibleToUser
852                 = filteredValues.getAsBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)
853                         || filteredValues.getAsInteger(COLUMN_MEDIA_SCANNED) == MEDIA_NOT_SCANNED;
854         if (shouldBeVisibleToUser && filteredValues.getAsInteger(COLUMN_DESTINATION)
855                 == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
856             final CallingIdentity token = clearCallingIdentity();
857             try {
858                 final Uri mediaStoreUri = MediaStore.scanFile(getContext().getContentResolver(),
859                         new File(filteredValues.getAsString(Downloads.Impl._DATA)));
860                 if (mediaStoreUri != null) {
861                     final ContentValues mediaValues = new ContentValues();
862                     mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI,
863                             filteredValues.getAsString(Downloads.Impl.COLUMN_URI));
864                     mediaValues.put(MediaStore.Downloads.REFERER_URI,
865                             filteredValues.getAsString(Downloads.Impl.COLUMN_REFERER));
866                     mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
867                             Helpers.getPackageForUid(getContext(),
868                                     filteredValues.getAsInteger(Constants.UID)));
869                     getContext().getContentResolver().update(
870                             convertToMediaStoreDownloadsUri(mediaStoreUri),
871                             mediaValues, null, null);
872 
873                     filteredValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
874                             mediaStoreUri.toString());
875                     filteredValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
876                             mediaStoreUri.toString());
877                     filteredValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED);
878                 }
879             } finally {
880                 restoreCallingIdentity(token);
881             }
882         }
883 
884         if (Constants.LOGVV) {
885             Log.v(Constants.TAG, "initiating download with UID "
886                     + filteredValues.getAsInteger(Constants.UID));
887             if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
888                 Log.v(Constants.TAG, "other UID " +
889                         filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
890             }
891         }
892 
893         long rowID = db.insert(DB_TABLE, null, filteredValues);
894         if (rowID == -1) {
895             Log.d(Constants.TAG, "couldn't insert into downloads database");
896             return null;
897         }
898 
899         insertRequestHeaders(db, rowID, values);
900 
901         final String callingPackage = Helpers.getPackageForUid(getContext(),
902                 Binder.getCallingUid());
903         if (callingPackage == null) {
904             Log.e(Constants.TAG, "Package does not exist for calling uid");
905             return null;
906         }
907         grantAllDownloadsPermission(callingPackage, rowID);
908         notifyContentChanged(uri, match);
909 
910         final long token = Binder.clearCallingIdentity();
911         try {
912             Helpers.scheduleJob(getContext(), rowID);
913         } finally {
914             Binder.restoreCallingIdentity(token);
915         }
916 
917         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
918     }
919 
920     /**
921      * If an entry corresponding to given mediaValues doesn't already exist in MediaProvider,
922      * add it, otherwise update that entry with the given values.
923      */
updateMediaProvider(@onNull ContentProviderClient mediaProvider, @NonNull ContentValues mediaValues)924     Uri updateMediaProvider(@NonNull ContentProviderClient mediaProvider,
925             @NonNull ContentValues mediaValues) {
926         final String filePath = mediaValues.getAsString(MediaStore.DownloadColumns.DATA);
927         Uri mediaStoreUri = getMediaStoreUri(mediaProvider, filePath);
928 
929         try {
930             if (mediaStoreUri == null) {
931                 mediaStoreUri = mediaProvider.insert(
932                         Helpers.getContentUriForPath(getContext(), filePath),
933                         mediaValues);
934                 if (mediaStoreUri == null) {
935                     Log.e(Constants.TAG, "Error inserting into mediaProvider: " + mediaValues);
936                 }
937                 return mediaStoreUri;
938             } else {
939                 if (mediaProvider.update(mediaStoreUri, mediaValues, null, null) != 1) {
940                     Log.e(Constants.TAG, "Error updating MediaProvider, uri: " + mediaStoreUri
941                             + ", values: " + mediaValues);
942                 }
943                 return mediaStoreUri;
944             }
945         } catch (IllegalArgumentException ignored) {
946             // Insert or update MediaStore failed. At this point we can't do
947             // much here. If the file belongs to MediaStore collection, it will
948             // get added to MediaStore collection during next scan, and we will
949             // obtain the uri to the file in the next MediaStore#scanFile
950             // initiated by us
951             Log.w(Constants.TAG, "Couldn't update MediaStore for " + filePath, ignored);
952         } catch (RemoteException e) {
953             // Should not happen
954         }
955         return null;
956     }
957 
getMediaStoreUri(@onNull ContentProviderClient mediaProvider, @NonNull String filePath)958     private Uri getMediaStoreUri(@NonNull ContentProviderClient mediaProvider,
959             @NonNull String filePath) {
960         final Uri filesUri = MediaStore.setIncludePending(
961                 Helpers.getContentUriForPath(getContext(), filePath));
962         try (Cursor cursor = mediaProvider.query(filesUri,
963                 new String[] { MediaStore.Files.FileColumns._ID },
964                 MediaStore.Files.FileColumns.DATA + "=?", new String[] { filePath }, null, null)) {
965             if (cursor.moveToNext()) {
966                 return ContentUris.withAppendedId(filesUri, cursor.getLong(0));
967             }
968         } catch (RemoteException e) {
969             // Should not happen
970         }
971         return null;
972     }
973 
convertToMediaProviderValues(DownloadInfo info)974     ContentValues convertToMediaProviderValues(DownloadInfo info) {
975         final String filePath;
976         try {
977             filePath = new File(info.mFileName).getCanonicalPath();
978         } catch (IOException e) {
979             throw new IllegalArgumentException(e);
980         }
981         final boolean downloadCompleted = Downloads.Impl.isStatusCompleted(info.mStatus);
982         final ContentValues mediaValues = new ContentValues();
983         mediaValues.put(MediaStore.Downloads.DATA, filePath);
984         mediaValues.put(MediaStore.Downloads.VOLUME_NAME, Helpers.extractVolumeName(filePath));
985         mediaValues.put(MediaStore.Downloads.RELATIVE_PATH, Helpers.extractRelativePath(filePath));
986         mediaValues.put(MediaStore.Downloads.DISPLAY_NAME, Helpers.extractDisplayName(filePath));
987         mediaValues.put(MediaStore.Downloads.SIZE,
988                 downloadCompleted ? info.mTotalBytes : info.mCurrentBytes);
989         mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri);
990         mediaValues.put(MediaStore.Downloads.REFERER_URI, info.mReferer);
991         mediaValues.put(MediaStore.Downloads.MIME_TYPE, info.mMimeType);
992         mediaValues.put(MediaStore.Downloads.IS_PENDING, downloadCompleted ? 0 : 1);
993         mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
994                 Helpers.getPackageForUid(getContext(), info.mUid));
995         return mediaValues;
996     }
997 
getFileUri(String uriString)998     private static Uri getFileUri(String uriString) {
999         final Uri uri = Uri.parse(uriString);
1000         return TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE) ? uri : null;
1001     }
1002 
ensureDefaultColumns(ContentValues values)1003     private void ensureDefaultColumns(ContentValues values) {
1004         final Integer dest = values.getAsInteger(COLUMN_DESTINATION);
1005         if (dest != null) {
1006             final int mediaScannable;
1007             final boolean visibleInDownloadsUi;
1008             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
1009                 mediaScannable = MEDIA_NOT_SCANNED;
1010                 visibleInDownloadsUi = true;
1011             } else if (dest != DESTINATION_FILE_URI
1012                     && dest != DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
1013                 mediaScannable = MEDIA_NOT_SCANNABLE;
1014                 visibleInDownloadsUi = false;
1015             } else {
1016                 final File file;
1017                 if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
1018                     final String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
1019                     file = new File(getFileUri(fileUri).getPath());
1020                 } else {
1021                     file = new File(values.getAsString(Downloads.Impl._DATA));
1022                 }
1023 
1024                 if (Helpers.isFileInExternalAndroidDirs(file.getAbsolutePath())) {
1025                     mediaScannable = MEDIA_NOT_SCANNABLE;
1026                     visibleInDownloadsUi = false;
1027                 } else if (Helpers.isFilenameValidInPublicDownloadsDir(file)) {
1028                     mediaScannable = MEDIA_NOT_SCANNED;
1029                     visibleInDownloadsUi = true;
1030                 } else {
1031                     mediaScannable = MEDIA_NOT_SCANNED;
1032                     visibleInDownloadsUi = false;
1033                 }
1034             }
1035             values.put(COLUMN_MEDIA_SCANNED, mediaScannable);
1036             values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, visibleInDownloadsUi);
1037         } else {
1038             if (!values.containsKey(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
1039                 values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, true);
1040             }
1041         }
1042     }
1043 
1044     /**
1045      * Check that the file URI provided for DESTINATION_FILE_URI is valid.
1046      */
checkFileUriDestination(ContentValues values)1047     private void checkFileUriDestination(ContentValues values) {
1048         String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
1049         if (fileUri == null) {
1050             throw new IllegalArgumentException(
1051                     "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
1052         }
1053         final Uri uri = getFileUri(fileUri);
1054         if (uri == null) {
1055             throw new IllegalArgumentException("Not a file URI: " + uri);
1056         }
1057         final String path = uri.getPath();
1058         if (path == null || ("/" + path + "/").contains("/../")) {
1059             throw new IllegalArgumentException("Invalid file URI: " + uri);
1060         }
1061 
1062         final File file;
1063         try {
1064             file = new File(path).getCanonicalFile();
1065             values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, Uri.fromFile(file).toString());
1066         } catch (IOException e) {
1067             throw new SecurityException(e);
1068         }
1069 
1070         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
1071         final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
1072         final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
1073                 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
1074 
1075         if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
1076                 || Helpers.isFilenameValidInKnownPublicDir(file.getAbsolutePath())) {
1077             // No permissions required for paths belonging to calling package or
1078             // public downloads dir.
1079             return;
1080         } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) {
1081             // Otherwise we require write permission
1082             getContext().enforceCallingOrSelfPermission(
1083                     android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
1084                     "No permission to write to " + file);
1085 
1086             final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
1087             if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
1088                     Binder.getCallingUid(), getCallingAttributionTag(), null)
1089                     != AppOpsManager.MODE_ALLOWED) {
1090                 throw new SecurityException("No permission to write to " + file);
1091             }
1092         } else if (Helpers.isFilenameValidInExternalObbDir(file) &&
1093                 ((appOpsManager.noteOp(
1094                     AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
1095                     Binder.getCallingUid(), getCallingPackage(), null, "obb_download")
1096                         == AppOpsManager.MODE_ALLOWED)
1097                 || (getContext().checkCallingOrSelfPermission(
1098                     android.Manifest.permission.REQUEST_INSTALL_PACKAGES)
1099                     == PackageManager.PERMISSION_GRANTED))) {
1100             // Installers are allowed to download in OBB dirs, even outside their own package
1101             return;
1102         } else {
1103             throw new SecurityException("Unsupported path " + file);
1104         }
1105     }
1106 
checkDownloadedFilePath(ContentValues values)1107     private void checkDownloadedFilePath(ContentValues values) {
1108         final String path = values.getAsString(Downloads.Impl._DATA);
1109         if (path == null || ("/" + path + "/").contains("/../")) {
1110             throw new IllegalArgumentException("Invalid file path: "
1111                     + (path == null ? "null" : path));
1112         }
1113 
1114         final File file;
1115         try {
1116             file = new File(path).getCanonicalFile();
1117             values.put(Downloads.Impl._DATA, file.getPath());
1118         } catch (IOException e) {
1119             throw new SecurityException(e);
1120         }
1121 
1122         if (!file.exists()) {
1123             throw new IllegalArgumentException("File doesn't exist: " + file);
1124         }
1125 
1126         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
1127         final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
1128         final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
1129                 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
1130 
1131         if (Binder.getCallingPid() == Process.myPid()) {
1132             return;
1133         } else if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
1134                 || Helpers.isFilenameValidInPublicDownloadsDir(file)) {
1135             // No permissions required for paths belonging to calling package or
1136             // public downloads dir.
1137             return;
1138         } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) {
1139             // Otherwise we require write permission
1140             getContext().enforceCallingOrSelfPermission(
1141                     android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
1142                     "No permission to write to " + file);
1143 
1144             final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
1145             if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
1146                     Binder.getCallingUid(), getCallingAttributionTag(), null)
1147                     != AppOpsManager.MODE_ALLOWED) {
1148                 throw new SecurityException("No permission to write to " + file);
1149             }
1150         } else {
1151             throw new SecurityException("Unsupported path " + file);
1152         }
1153     }
1154 
getCallingPackageTargetSdkVersion()1155     private int getCallingPackageTargetSdkVersion() {
1156         final String callingPackage = getCallingPackage();
1157         if (callingPackage != null) {
1158             ApplicationInfo ai = null;
1159             try {
1160                 ai = getContext().getPackageManager()
1161                         .getApplicationInfo(callingPackage, 0);
1162             } catch (PackageManager.NameNotFoundException ignored) {
1163             }
1164             if (ai != null) {
1165                 return ai.targetSdkVersion;
1166             }
1167         }
1168         return Build.VERSION_CODES.CUR_DEVELOPMENT;
1169     }
1170 
1171     /**
1172      * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
1173      * constraints in the rest of the code. Apps without that may still access this provider through
1174      * the public API, but additional restrictions are imposed. We check those restrictions here.
1175      *
1176      * @param values ContentValues provided to insert()
1177      * @throws SecurityException if the caller has insufficient permissions
1178      */
checkInsertPermissions(ContentValues values)1179     private void checkInsertPermissions(ContentValues values) {
1180         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS)
1181                 == PackageManager.PERMISSION_GRANTED) {
1182             return;
1183         }
1184 
1185         getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
1186                 "INTERNET permission is required to use the download manager");
1187 
1188         // ensure the request fits within the bounds of a public API request
1189         // first copy so we can remove values
1190         values = new ContentValues(values);
1191 
1192         // check columns whose values are restricted
1193         enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE);
1194 
1195         // validate the destination column
1196         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
1197                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
1198             /* this row is inserted by
1199              * DownloadManager.addCompletedDownload(String, String, String,
1200              * boolean, String, String, long)
1201              */
1202             values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
1203             values.remove(Downloads.Impl._DATA);
1204             values.remove(Downloads.Impl.COLUMN_STATUS);
1205         }
1206         enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION,
1207                 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE,
1208                 Downloads.Impl.DESTINATION_FILE_URI,
1209                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
1210 
1211         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
1212                 == PackageManager.PERMISSION_GRANTED) {
1213             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
1214                     Request.VISIBILITY_HIDDEN,
1215                     Request.VISIBILITY_VISIBLE,
1216                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
1217                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
1218         } else {
1219             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
1220                     Request.VISIBILITY_VISIBLE,
1221                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
1222                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
1223         }
1224 
1225         // remove the rest of the columns that are allowed (with any value)
1226         values.remove(Downloads.Impl.COLUMN_URI);
1227         values.remove(Downloads.Impl.COLUMN_TITLE);
1228         values.remove(Downloads.Impl.COLUMN_DESCRIPTION);
1229         values.remove(Downloads.Impl.COLUMN_MIME_TYPE);
1230         values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert()
1231         values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
1232         values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
1233         values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
1234         values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
1235         values.remove(Downloads.Impl.COLUMN_FLAGS);
1236         values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
1237         values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
1238         values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
1239         Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
1240         while (iterator.hasNext()) {
1241             String key = iterator.next().getKey();
1242             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
1243                 iterator.remove();
1244             }
1245         }
1246 
1247         // any extra columns are extraneous and disallowed
1248         if (values.size() > 0) {
1249             StringBuilder error = new StringBuilder("Invalid columns in request: ");
1250             boolean first = true;
1251             for (Map.Entry<String, Object> entry : values.valueSet()) {
1252                 if (!first) {
1253                     error.append(", ");
1254                 }
1255                 error.append(entry.getKey());
1256                 first = false;
1257             }
1258             throw new SecurityException(error.toString());
1259         }
1260     }
1261 
1262     /**
1263      * Remove column from values, and throw a SecurityException if the value isn't within the
1264      * specified allowedValues.
1265      */
enforceAllowedValues(ContentValues values, String column, Object... allowedValues)1266     private void enforceAllowedValues(ContentValues values, String column,
1267             Object... allowedValues) {
1268         Object value = values.get(column);
1269         values.remove(column);
1270         for (Object allowedValue : allowedValues) {
1271             if (value == null && allowedValue == null) {
1272                 return;
1273             }
1274             if (value != null && value.equals(allowedValue)) {
1275                 return;
1276             }
1277         }
1278         throw new SecurityException("Invalid value for " + column + ": " + value);
1279     }
1280 
queryCleared(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort)1281     private Cursor queryCleared(Uri uri, String[] projection, String selection,
1282             String[] selectionArgs, String sort) {
1283         final long token = Binder.clearCallingIdentity();
1284         try {
1285             return query(uri, projection, selection, selectionArgs, sort);
1286         } finally {
1287             Binder.restoreCallingIdentity(token);
1288         }
1289     }
1290 
1291     /**
1292      * Starts a database query
1293      */
1294     @Override
query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort)1295     public Cursor query(final Uri uri, String[] projection,
1296              final String selection, final String[] selectionArgs,
1297              final String sort) {
1298 
1299         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1300 
1301         int match = sURIMatcher.match(uri);
1302         if (match == -1) {
1303             if (Constants.LOGV) {
1304                 Log.v(Constants.TAG, "querying unknown URI: " + uri);
1305             }
1306             throw new IllegalArgumentException("Unknown URI: " + uri);
1307         }
1308 
1309         if (match == MY_DOWNLOADS_ID_HEADERS || match == ALL_DOWNLOADS_ID_HEADERS) {
1310             if (projection != null || selection != null || sort != null) {
1311                 throw new UnsupportedOperationException("Request header queries do not support "
1312                                                         + "projections, selections or sorting");
1313             }
1314 
1315             // Headers are only available to callers with full access.
1316             getContext().enforceCallingOrSelfPermission(
1317                     Downloads.Impl.PERMISSION_ACCESS_ALL, Constants.TAG);
1318 
1319             final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1320             projection = new String[] {
1321                     Downloads.Impl.RequestHeaders.COLUMN_HEADER,
1322                     Downloads.Impl.RequestHeaders.COLUMN_VALUE
1323             };
1324             return qb.query(db, projection, null, null, null, null, null);
1325         }
1326 
1327         if (Constants.LOGVV) {
1328             logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
1329         }
1330 
1331         final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1332 
1333         final Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sort);
1334 
1335         if (ret != null) {
1336             ret.setNotificationUri(getContext().getContentResolver(), uri);
1337             if (Constants.LOGVV) {
1338                 Log.v(Constants.TAG,
1339                         "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
1340             }
1341         } else {
1342             if (Constants.LOGV) {
1343                 Log.v(Constants.TAG, "query failed in downloads database");
1344             }
1345         }
1346 
1347         return ret;
1348     }
1349 
logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db)1350     private void logVerboseQueryInfo(String[] projection, final String selection,
1351             final String[] selectionArgs, final String sort, SQLiteDatabase db) {
1352         java.lang.StringBuilder sb = new java.lang.StringBuilder();
1353         sb.append("starting query, database is ");
1354         if (db != null) {
1355             sb.append("not ");
1356         }
1357         sb.append("null; ");
1358         if (projection == null) {
1359             sb.append("projection is null; ");
1360         } else if (projection.length == 0) {
1361             sb.append("projection is empty; ");
1362         } else {
1363             for (int i = 0; i < projection.length; ++i) {
1364                 sb.append("projection[");
1365                 sb.append(i);
1366                 sb.append("] is ");
1367                 sb.append(projection[i]);
1368                 sb.append("; ");
1369             }
1370         }
1371         sb.append("selection is ");
1372         sb.append(selection);
1373         sb.append("; ");
1374         if (selectionArgs == null) {
1375             sb.append("selectionArgs is null; ");
1376         } else if (selectionArgs.length == 0) {
1377             sb.append("selectionArgs is empty; ");
1378         } else {
1379             for (int i = 0; i < selectionArgs.length; ++i) {
1380                 sb.append("selectionArgs[");
1381                 sb.append(i);
1382                 sb.append("] is ");
1383                 sb.append(selectionArgs[i]);
1384                 sb.append("; ");
1385             }
1386         }
1387         sb.append("sort is ");
1388         sb.append(sort);
1389         sb.append(".");
1390         Log.v(Constants.TAG, sb.toString());
1391     }
1392 
getDownloadIdFromUri(final Uri uri)1393     private String getDownloadIdFromUri(final Uri uri) {
1394         return uri.getPathSegments().get(1);
1395     }
1396 
1397     /**
1398      * Insert request headers for a download into the DB.
1399      */
insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values)1400     private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
1401         ContentValues rowValues = new ContentValues();
1402         rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
1403         for (Map.Entry<String, Object> entry : values.valueSet()) {
1404             String key = entry.getKey();
1405             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
1406                 String headerLine = entry.getValue().toString();
1407                 if (!headerLine.contains(":")) {
1408                     throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
1409                 }
1410                 String[] parts = headerLine.split(":", 2);
1411                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
1412                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
1413                 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
1414             }
1415         }
1416     }
1417 
1418     /**
1419      * Updates a row in the database
1420      */
1421     @Override
update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)1422     public int update(final Uri uri, final ContentValues values,
1423             final String where, final String[] whereArgs) {
1424         final Context context = getContext();
1425         final ContentResolver resolver = context.getContentResolver();
1426 
1427         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1428 
1429         int count;
1430         boolean updateSchedule = false;
1431         boolean isCompleting = false;
1432 
1433         ContentValues filteredValues;
1434         if (Binder.getCallingPid() != Process.myPid()) {
1435             filteredValues = new ContentValues();
1436             copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
1437             copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues);
1438             Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
1439             if (i != null) {
1440                 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
1441                 updateSchedule = true;
1442             }
1443 
1444             copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
1445             copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
1446             copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues);
1447             copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
1448             copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues);
1449         } else {
1450             filteredValues = values;
1451             String filename = values.getAsString(Downloads.Impl._DATA);
1452             if (filename != null) {
1453                 try {
1454                     filteredValues.put(Downloads.Impl._DATA, new File(filename).getCanonicalPath());
1455                 } catch (IOException e) {
1456                     throw new IllegalStateException("Invalid path: " + filename);
1457                 }
1458 
1459                 Cursor c = null;
1460                 try {
1461                     c = query(uri, new String[]
1462                             { Downloads.Impl.COLUMN_TITLE }, null, null, null);
1463                     if (!c.moveToFirst() || c.getString(0).isEmpty()) {
1464                         values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
1465                     }
1466                 } finally {
1467                     IoUtils.closeQuietly(c);
1468                 }
1469             }
1470 
1471             Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
1472             boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING;
1473             boolean isUserBypassingSizeLimit =
1474                 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
1475             if (isRestart || isUserBypassingSizeLimit) {
1476                 updateSchedule = true;
1477             }
1478             isCompleting = status != null && Downloads.Impl.isStatusCompleted(status);
1479         }
1480 
1481         int match = sURIMatcher.match(uri);
1482         switch (match) {
1483             case MY_DOWNLOADS:
1484             case MY_DOWNLOADS_ID:
1485             case ALL_DOWNLOADS:
1486             case ALL_DOWNLOADS_ID:
1487                 if (filteredValues.size() == 0) {
1488                     count = 0;
1489                     break;
1490                 }
1491 
1492                 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1493                 count = qb.update(db, filteredValues, where, whereArgs);
1494                 final CallingIdentity token = clearCallingIdentity();
1495                 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null);
1496                         ContentProviderClient client = getContext().getContentResolver()
1497                                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
1498                     final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver,
1499                             cursor);
1500                     final DownloadInfo info = new DownloadInfo(context);
1501                     final ContentValues updateValues = new ContentValues();
1502                     while (cursor.moveToNext()) {
1503                         reader.updateFromDatabase(info);
1504                         final boolean visibleToUser = info.mIsVisibleInDownloadsUi
1505                                 || (info.mMediaScanned != MEDIA_NOT_SCANNABLE);
1506                         if (info.mFileName == null) {
1507                             if (info.mMediaStoreUri != null) {
1508                                 // If there was a mediastore entry, it would be deleted in it's
1509                                 // next idle pass.
1510                                 updateValues.clear();
1511                                 updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
1512                                 qb.update(db, updateValues, Downloads.Impl._ID + "=?",
1513                                         new String[] { Long.toString(info.mId) });
1514                             }
1515                         } else if ((info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
1516                                 || info.mDestination == Downloads.Impl.DESTINATION_FILE_URI
1517                                 || info.mDestination == Downloads.Impl
1518                                         .DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
1519                                 && visibleToUser) {
1520                             final ContentValues mediaValues = convertToMediaProviderValues(info);
1521                             final Uri mediaStoreUri;
1522                             if (Downloads.Impl.isStatusCompleted(info.mStatus)) {
1523                                 // Set size to 0 to ensure MediaScanner will scan this file.
1524                                 mediaValues.put(MediaStore.Downloads.SIZE, 0);
1525                                 updateMediaProvider(client, mediaValues);
1526                                 mediaStoreUri = triggerMediaScan(client, new File(info.mFileName));
1527                             } else {
1528                                 // Don't insert/update MediaStore db until the download is complete.
1529                                 // Incomplete files can only be inserted to MediaStore by setting
1530                                 // IS_PENDING=1 and using RELATIVE_PATH and DISPLAY_NAME in
1531                                 // MediaProvider#insert operation. We use DATA column, IS_PENDING
1532                                 // with DATA column will not be respected by MediaProvider.
1533                                 mediaStoreUri = null;
1534                             }
1535                             if (!TextUtils.equals(info.mMediaStoreUri,
1536                                     mediaStoreUri == null ? null : mediaStoreUri.toString())) {
1537                                 updateValues.clear();
1538                                 if (mediaStoreUri == null) {
1539                                     updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
1540                                     updateValues.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
1541                                     updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_NOT_SCANNED);
1542                                 } else {
1543                                     updateValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
1544                                             mediaStoreUri.toString());
1545                                     updateValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
1546                                             mediaStoreUri.toString());
1547                                     updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED);
1548                                 }
1549                                 qb.update(db, updateValues, Downloads.Impl._ID + "=?",
1550                                         new String[] { Long.toString(info.mId) });
1551                             }
1552                         }
1553                         if (updateSchedule) {
1554                             Helpers.scheduleJob(context, info);
1555                         }
1556                         if (isCompleting) {
1557                             info.sendIntentIfRequested();
1558                         }
1559                     }
1560                 } finally {
1561                     restoreCallingIdentity(token);
1562                 }
1563                 break;
1564 
1565             default:
1566                 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
1567                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
1568         }
1569 
1570         notifyContentChanged(uri, match);
1571         return count;
1572     }
1573 
1574     /**
1575      * Notify of a change through both URIs (/my_downloads and /all_downloads)
1576      * @param uri either URI for the changed download(s)
1577      * @param uriMatch the match ID from {@link #sURIMatcher}
1578      */
notifyContentChanged(final Uri uri, int uriMatch)1579     private void notifyContentChanged(final Uri uri, int uriMatch) {
1580         Long downloadId = null;
1581         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
1582             downloadId = Long.parseLong(getDownloadIdFromUri(uri));
1583         }
1584         for (Uri uriToNotify : BASE_URIS) {
1585             if (downloadId != null) {
1586                 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
1587             }
1588             getContext().getContentResolver().notifyChange(uriToNotify, null);
1589         }
1590     }
1591 
1592     /**
1593      * Create a query builder that filters access to the underlying database
1594      * based on both the requested {@link Uri} and permissions of the caller.
1595      */
getQueryBuilder(final Uri uri, int match)1596     private SQLiteQueryBuilder getQueryBuilder(final Uri uri, int match) {
1597         final String table;
1598         final Map<String, String> projectionMap;
1599 
1600         final StringBuilder where = new StringBuilder();
1601         switch (match) {
1602             // The "my_downloads" view normally limits the caller to operating
1603             // on downloads that they either directly own, or have been given
1604             // indirect ownership of via OTHER_UID.
1605             case MY_DOWNLOADS_ID:
1606                 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri));
1607                 // fall-through
1608             case MY_DOWNLOADS:
1609                 table = DB_TABLE;
1610                 projectionMap = sDownloadsMap;
1611                 if (getContext().checkCallingOrSelfPermission(
1612                         PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) {
1613                     appendWhereExpression(where, Constants.UID + "=" + Binder.getCallingUid()
1614                             + " OR " + COLUMN_OTHER_UID + "=" + Binder.getCallingUid());
1615                 }
1616                 break;
1617 
1618             // The "all_downloads" view is already limited via <path-permission>
1619             // to only callers holding the ACCESS_ALL_DOWNLOADS permission, but
1620             // access may also be delegated via Uri permission grants.
1621             case ALL_DOWNLOADS_ID:
1622                 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri));
1623                 // fall-through
1624             case ALL_DOWNLOADS:
1625                 table = DB_TABLE;
1626                 projectionMap = sDownloadsMap;
1627                 break;
1628 
1629             // Headers are limited to callers holding the ACCESS_ALL_DOWNLOADS
1630             // permission, since they're only needed for executing downloads.
1631             case MY_DOWNLOADS_ID_HEADERS:
1632             case ALL_DOWNLOADS_ID_HEADERS:
1633                 table = Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE;
1634                 projectionMap = sHeadersMap;
1635                 appendWhereExpression(where, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
1636                         + getDownloadIdFromUri(uri));
1637                 break;
1638 
1639             default:
1640                 throw new UnsupportedOperationException("Unknown URI: " + uri);
1641         }
1642 
1643         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1644         qb.setTables(table);
1645         qb.setProjectionMap(projectionMap);
1646         qb.setStrict(true);
1647         qb.setStrictColumns(true);
1648         qb.setStrictGrammar(true);
1649         qb.appendWhere(where);
1650         return qb;
1651     }
1652 
appendWhereExpression(StringBuilder sb, String expression)1653     private static void appendWhereExpression(StringBuilder sb, String expression) {
1654         if (sb.length() > 0) {
1655             sb.append(" AND ");
1656         }
1657         sb.append('(').append(expression).append(')');
1658     }
1659 
1660     /**
1661      * Deletes a row in the database
1662      */
1663     @Override
delete(final Uri uri, final String where, final String[] whereArgs)1664     public int delete(final Uri uri, final String where, final String[] whereArgs) {
1665         final Context context = getContext();
1666         final ContentResolver resolver = context.getContentResolver();
1667         final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
1668 
1669         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1670         int count;
1671         int match = sURIMatcher.match(uri);
1672         switch (match) {
1673             case MY_DOWNLOADS:
1674             case MY_DOWNLOADS_ID:
1675             case ALL_DOWNLOADS:
1676             case ALL_DOWNLOADS_ID:
1677                 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1678                 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null)) {
1679                     final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
1680                     final DownloadInfo info = new DownloadInfo(context);
1681                     while (cursor.moveToNext()) {
1682                         reader.updateFromDatabase(info);
1683                         scheduler.cancel((int) info.mId);
1684 
1685                         revokeAllDownloadsPermission(info.mId);
1686                         DownloadStorageProvider.onDownloadProviderDelete(getContext(), info.mId);
1687 
1688                         final String path = info.mFileName;
1689                         if (!TextUtils.isEmpty(path)) {
1690                             try {
1691                                 final File file = new File(path).getCanonicalFile();
1692                                 if (Helpers.isFilenameValid(getContext(), file)) {
1693                                     Log.v(Constants.TAG,
1694                                             "Deleting " + file + " via provider delete");
1695                                     file.delete();
1696                                     MediaStore.scanFile(getContext().getContentResolver(), file);
1697                                 } else {
1698                                     Log.d(Constants.TAG, "Ignoring invalid file: " + file);
1699                                 }
1700                             } catch (IOException e) {
1701                                 Log.e(Constants.TAG, "Couldn't delete file: " + path, e);
1702                             }
1703                         }
1704 
1705                         // If the download wasn't completed yet, we're
1706                         // effectively completing it now, and we need to send
1707                         // any requested broadcasts
1708                         if (!Downloads.Impl.isStatusCompleted(info.mStatus)) {
1709                             info.sendIntentIfRequested();
1710                         }
1711 
1712                         // Delete any headers for this download
1713                         db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE,
1714                                 Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=?",
1715                                 new String[] { Long.toString(info.mId) });
1716                     }
1717                 }
1718 
1719                 count = qb.delete(db, where, whereArgs);
1720                 break;
1721 
1722             default:
1723                 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
1724                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
1725         }
1726         notifyContentChanged(uri, match);
1727         final long token = Binder.clearCallingIdentity();
1728         try {
1729             Helpers.getDownloadNotifier(getContext()).update();
1730         } finally {
1731             Binder.restoreCallingIdentity(token);
1732         }
1733         return count;
1734     }
1735 
1736     /**
1737      * Remotely opens a file
1738      */
1739     @Override
openFile(final Uri uri, String mode)1740     public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
1741         if (Constants.LOGVV) {
1742             logVerboseOpenFileInfo(uri, mode);
1743         }
1744 
1745         // Perform normal query to enforce caller identity access before
1746         // clearing it to reach internal-only columns
1747         final Cursor probeCursor = query(uri, new String[] {
1748                 Downloads.Impl._DATA }, null, null, null);
1749         try {
1750             if ((probeCursor == null) || (probeCursor.getCount() == 0)) {
1751                 throw new FileNotFoundException(
1752                         "No file found for " + uri + " as UID " + Binder.getCallingUid());
1753             }
1754         } finally {
1755             IoUtils.closeQuietly(probeCursor);
1756         }
1757 
1758         final Cursor cursor = queryCleared(uri, new String[] {
1759                 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS,
1760                 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null,
1761                 null, null);
1762         final String path;
1763         final boolean shouldScan;
1764         try {
1765             int count = (cursor != null) ? cursor.getCount() : 0;
1766             if (count != 1) {
1767                 // If there is not exactly one result, throw an appropriate exception.
1768                 if (count == 0) {
1769                     throw new FileNotFoundException("No entry for " + uri);
1770                 }
1771                 throw new FileNotFoundException("Multiple items at " + uri);
1772             }
1773 
1774             if (cursor.moveToFirst()) {
1775                 final int status = cursor.getInt(1);
1776                 final int destination = cursor.getInt(2);
1777                 final int mediaScanned = cursor.getInt(3);
1778 
1779                 path = cursor.getString(0);
1780                 shouldScan = Downloads.Impl.isStatusSuccess(status) && (
1781                         destination == Downloads.Impl.DESTINATION_EXTERNAL
1782                         || destination == Downloads.Impl.DESTINATION_FILE_URI
1783                         || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
1784                         && mediaScanned != Downloads.Impl.MEDIA_NOT_SCANNABLE;
1785             } else {
1786                 throw new FileNotFoundException("Failed moveToFirst");
1787             }
1788         } finally {
1789             IoUtils.closeQuietly(cursor);
1790         }
1791 
1792         if (path == null) {
1793             throw new FileNotFoundException("No filename found.");
1794         }
1795 
1796         final File file;
1797         try {
1798             file = new File(path).getCanonicalFile();
1799         } catch (IOException e) {
1800             throw new FileNotFoundException(e.getMessage());
1801         }
1802 
1803         if (!Helpers.isFilenameValid(getContext(), file)) {
1804             throw new FileNotFoundException("Invalid file: " + file);
1805         }
1806 
1807         final int pfdMode = ParcelFileDescriptor.parseMode(mode);
1808         if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) {
1809             return ParcelFileDescriptor.open(file, pfdMode);
1810         } else {
1811             try {
1812                 // When finished writing, update size and timestamp
1813                 return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(),
1814                         new OnCloseListener() {
1815                     @Override
1816                     public void onClose(IOException e) {
1817                         final ContentValues values = new ContentValues();
1818                         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
1819                         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
1820                                 mSystemFacade.currentTimeMillis());
1821                         update(uri, values, null, null);
1822 
1823                         if (shouldScan) {
1824                             final Intent intent = new Intent(
1825                                     Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
1826                             intent.setData(Uri.fromFile(file));
1827                             getContext().sendBroadcast(intent);
1828                         }
1829                     }
1830                 });
1831             } catch (IOException e) {
1832                 throw new FileNotFoundException("Failed to open for writing: " + e);
1833             }
1834         }
1835     }
1836 
1837     @Override
1838     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1839         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 120);
1840 
1841         pw.println("Downloads updated in last hour:");
1842         pw.increaseIndent();
1843 
1844         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1845         final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
1846         final Cursor cursor = db.query(DB_TABLE, null,
1847                 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
1848                 Downloads.Impl._ID + " ASC");
1849         try {
1850             final String[] cols = cursor.getColumnNames();
1851             final int idCol = cursor.getColumnIndex(BaseColumns._ID);
1852             while (cursor.moveToNext()) {
1853                 pw.println("Download #" + cursor.getInt(idCol) + ":");
1854                 pw.increaseIndent();
1855                 for (int i = 0; i < cols.length; i++) {
1856                     // Omit sensitive data when dumping
1857                     if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
1858                         continue;
1859                     }
1860                     pw.printPair(cols[i], cursor.getString(i));
1861                 }
1862                 pw.println();
1863                 pw.decreaseIndent();
1864             }
1865         } finally {
1866             cursor.close();
1867         }
1868 
1869         pw.decreaseIndent();
1870     }
1871 
1872     private void logVerboseOpenFileInfo(Uri uri, String mode) {
1873         Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
1874                 + ", uid: " + Binder.getCallingUid());
1875         Cursor cursor = query(Downloads.Impl.CONTENT_URI,
1876                 new String[] { "_id" }, null, null, "_id");
1877         if (cursor == null) {
1878             Log.v(Constants.TAG, "null cursor in openFile");
1879         } else {
1880             try {
1881                 if (!cursor.moveToFirst()) {
1882                     Log.v(Constants.TAG, "empty cursor in openFile");
1883                 } else {
1884                     do {
1885                         Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
1886                     } while(cursor.moveToNext());
1887                 }
1888             } finally {
1889                 cursor.close();
1890             }
1891         }
1892         cursor = query(uri, new String[] { "_data" }, null, null, null);
1893         if (cursor == null) {
1894             Log.v(Constants.TAG, "null cursor in openFile");
1895         } else {
1896             try {
1897                 if (!cursor.moveToFirst()) {
1898                     Log.v(Constants.TAG, "empty cursor in openFile");
1899                 } else {
1900                     String filename = cursor.getString(0);
1901                     Log.v(Constants.TAG, "filename in openFile: " + filename);
1902                     if (new java.io.File(filename).isFile()) {
1903                         Log.v(Constants.TAG, "file exists in openFile");
1904                     }
1905                 }
1906             } finally {
1907                 cursor.close();
1908             }
1909         }
1910     }
1911 
1912     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
1913         Integer i = from.getAsInteger(key);
1914         if (i != null) {
1915             to.put(key, i);
1916         }
1917     }
1918 
1919     private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
1920         Boolean b = from.getAsBoolean(key);
1921         if (b != null) {
1922             to.put(key, b);
1923         }
1924     }
1925 
1926     private static final void copyString(String key, ContentValues from, ContentValues to) {
1927         String s = from.getAsString(key);
1928         if (s != null) {
1929             to.put(key, s);
1930         }
1931     }
1932 
1933     private static final void copyStringWithDefault(String key, ContentValues from,
1934             ContentValues to, String defaultValue) {
1935         copyString(key, from, to);
1936         if (!to.containsKey(key)) {
1937             to.put(key, defaultValue);
1938         }
1939     }
1940 
1941     private void grantAllDownloadsPermission(String toPackage, long id) {
1942         final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1943         getContext().grantUriPermission(toPackage, uri,
1944                 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
1945     }
1946 
1947     private void revokeAllDownloadsPermission(long id) {
1948         final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1949         getContext().revokeUriPermission(uri, ~0);
1950     }
1951 }
1952