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