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