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, R)184         addMapping(map, DownloadManager.COLUMN_LOCAL_URI,
185                 "'placeholder'");
addMapping(map, DownloadManager.COLUMN_REASON, R)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, R)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     @Override
call(String method, String arg, Bundle extras)652     public Bundle call(String method, String arg, Bundle extras) {
653         switch (method) {
654             case Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED: {
655                 getContext().enforceCallingOrSelfPermission(
656                         android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG);
657                 final long[] deletedDownloadIds = extras.getLongArray(Downloads.EXTRA_IDS);
658                 final String[] mimeTypes = extras.getStringArray(Downloads.EXTRA_MIME_TYPES);
659                 DownloadStorageProvider.onMediaProviderDownloadsDelete(getContext(),
660                         deletedDownloadIds, mimeTypes);
661                 return null;
662             }
663             case Downloads.CALL_CREATE_EXTERNAL_PUBLIC_DIR: {
664                 final String dirType = extras.getString(Downloads.DIR_TYPE);
665                 if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, dirType)) {
666                     throw new IllegalStateException("Not one of standard directories: " + dirType);
667                 }
668                 final File file = Environment.getExternalStoragePublicDirectory(dirType);
669                 if (file.exists()) {
670                     if (!file.isDirectory()) {
671                         throw new IllegalStateException(file.getAbsolutePath() +
672                                 " already exists and is not a directory");
673                     }
674                 } else if (!file.mkdirs()) {
675                     throw new IllegalStateException("Unable to create directory: " +
676                             file.getAbsolutePath());
677                 }
678                 return null;
679             }
680             case Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS : {
681                 getContext().enforceCallingOrSelfPermission(
682                         android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG);
683                 DownloadStorageProvider.revokeAllMediaStoreUriPermissions(getContext());
684                 return null;
685             }
686             default:
687                 throw new UnsupportedOperationException("Unsupported call: " + method);
688         }
689     }
690 
691     /**
692      * Inserts a row in the database
693      */
694     @Override
insert(final Uri uri, final ContentValues values)695     public Uri insert(final Uri uri, final ContentValues values) {
696         checkInsertPermissions(values);
697         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
698 
699         // note we disallow inserting into ALL_DOWNLOADS
700         int match = sURIMatcher.match(uri);
701         if (match != MY_DOWNLOADS) {
702             Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
703             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
704         }
705 
706         ContentValues filteredValues = new ContentValues();
707 
708         boolean isPublicApi =
709                 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
710 
711         // validate the destination column
712         Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
713         if (dest != null) {
714             if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
715                     != PackageManager.PERMISSION_GRANTED
716                     && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
717                             || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING)) {
718                 throw new SecurityException("setting destination to : " + dest +
719                         " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
720             }
721             // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
722             // switch to non-purgeable download
723             boolean hasNonPurgeablePermission =
724                     getContext().checkCallingOrSelfPermission(
725                             Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
726                             == PackageManager.PERMISSION_GRANTED;
727             if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
728                     && hasNonPurgeablePermission) {
729                 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
730             }
731             if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
732                 checkFileUriDestination(values);
733             } else if (dest == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
734                 checkDownloadedFilePath(values);
735             } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
736                 getContext().enforceCallingOrSelfPermission(
737                         android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
738                         "No permission to write");
739 
740                 if (mAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
741                         getCallingPackage(), Binder.getCallingUid(), getCallingAttributionTag(),
742                         null) != AppOpsManager.MODE_ALLOWED) {
743                     throw new SecurityException("No permission to write");
744                 }
745             }
746 
747             filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
748         }
749 
750         ensureDefaultColumns(values);
751 
752         // copy some of the input values as is
753         copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
754         copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
755         copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
756         copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
757         copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
758         copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
759 
760         // validate the visibility column
761         Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
762         if (vis == null) {
763             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
764                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
765                         Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
766             } else {
767                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
768                         Downloads.Impl.VISIBILITY_HIDDEN);
769             }
770         } else {
771             filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
772         }
773         // copy the control column as is
774         copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
775 
776         /*
777          * requests coming from
778          * DownloadManager.addCompletedDownload(String, String, String,
779          * boolean, String, String, long) need special treatment
780          */
781         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
782                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
783             // these requests always are marked as 'completed'
784             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
785             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
786                     values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
787             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
788             copyString(Downloads.Impl._DATA, values, filteredValues);
789             copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
790         } else {
791             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
792             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
793             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
794         }
795 
796         // set lastupdate to current time
797         long lastMod = mSystemFacade.currentTimeMillis();
798         filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
799 
800         // use packagename of the caller to set the notification columns
801         String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
802         String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
803         if (pckg != null && (clazz != null || isPublicApi)) {
804             int uid = Binder.getCallingUid();
805             try {
806                 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
807                     filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
808                     if (clazz != null) {
809                         filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
810                     }
811                 }
812             } catch (PackageManager.NameNotFoundException ex) {
813                 /* ignored for now */
814             }
815         }
816 
817         // copy some more columns as is
818         copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
819         copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
820         copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
821         copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
822 
823         // UID, PID columns
824         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
825                 == PackageManager.PERMISSION_GRANTED) {
826             copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
827         }
828         filteredValues.put(Constants.UID, Binder.getCallingUid());
829         if (Binder.getCallingUid() == 0) {
830             copyInteger(Constants.UID, values, filteredValues);
831         }
832 
833         // copy some more columns as is
834         copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
835         copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
836 
837         // is_visible_in_downloads_ui column
838         copyBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
839 
840         // public api requests and networktypes/roaming columns
841         if (isPublicApi) {
842             copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
843             copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
844             copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
845             copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues);
846         }
847 
848         final Integer mediaScanned = values.getAsInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED);
849         filteredValues.put(COLUMN_MEDIA_SCANNED,
850                 mediaScanned == null ? MEDIA_NOT_SCANNED : mediaScanned);
851 
852         final boolean shouldBeVisibleToUser
853                 = filteredValues.getAsBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)
854                         || filteredValues.getAsInteger(COLUMN_MEDIA_SCANNED) == MEDIA_NOT_SCANNED;
855         if (shouldBeVisibleToUser && filteredValues.getAsInteger(COLUMN_DESTINATION)
856                 == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
857             final CallingIdentity token = clearCallingIdentity();
858             try {
859                 final Uri mediaStoreUri = MediaStore.scanFile(getContext().getContentResolver(),
860                         new File(filteredValues.getAsString(Downloads.Impl._DATA)));
861                 if (mediaStoreUri != null) {
862                     final ContentValues mediaValues = new ContentValues();
863                     mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI,
864                             filteredValues.getAsString(Downloads.Impl.COLUMN_URI));
865                     mediaValues.put(MediaStore.Downloads.REFERER_URI,
866                             filteredValues.getAsString(Downloads.Impl.COLUMN_REFERER));
867                     mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
868                             Helpers.getPackageForUid(getContext(),
869                                     filteredValues.getAsInteger(Constants.UID)));
870                     getContext().getContentResolver().update(
871                             convertToMediaStoreDownloadsUri(mediaStoreUri),
872                             mediaValues, null, null);
873 
874                     filteredValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
875                             mediaStoreUri.toString());
876                     filteredValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
877                             mediaStoreUri.toString());
878                     filteredValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED);
879                 }
880             } finally {
881                 restoreCallingIdentity(token);
882             }
883         }
884 
885         if (Constants.LOGVV) {
886             Log.v(Constants.TAG, "initiating download with UID "
887                     + filteredValues.getAsInteger(Constants.UID));
888             if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
889                 Log.v(Constants.TAG, "other UID " +
890                         filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
891             }
892         }
893 
894         long rowID = db.insert(DB_TABLE, null, filteredValues);
895         if (rowID == -1) {
896             Log.d(Constants.TAG, "couldn't insert into downloads database");
897             return null;
898         }
899 
900         insertRequestHeaders(db, rowID, values);
901 
902         final String callingPackage = Helpers.getPackageForUid(getContext(),
903                 Binder.getCallingUid());
904         if (callingPackage == null) {
905             Log.e(Constants.TAG, "Package does not exist for calling uid");
906             return null;
907         }
908         grantAllDownloadsPermission(callingPackage, rowID);
909         notifyContentChanged(uri, match);
910 
911         final long token = Binder.clearCallingIdentity();
912         try {
913             Helpers.scheduleJob(getContext(), rowID);
914         } finally {
915             Binder.restoreCallingIdentity(token);
916         }
917 
918         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
919     }
920 
921     /**
922      * If an entry corresponding to given mediaValues doesn't already exist in MediaProvider,
923      * add it, otherwise update that entry with the given values.
924      */
updateMediaProvider(@onNull ContentProviderClient mediaProvider, @NonNull ContentValues mediaValues)925     Uri updateMediaProvider(@NonNull ContentProviderClient mediaProvider,
926             @NonNull ContentValues mediaValues) {
927         final String filePath = mediaValues.getAsString(MediaStore.DownloadColumns.DATA);
928         Uri mediaStoreUri = getMediaStoreUri(mediaProvider, filePath);
929 
930         try {
931             if (mediaStoreUri == null) {
932                 mediaStoreUri = mediaProvider.insert(
933                         Helpers.getContentUriForPath(getContext(), filePath),
934                         mediaValues);
935                 if (mediaStoreUri == null) {
936                     Log.e(Constants.TAG, "Error inserting into mediaProvider: " + mediaValues);
937                 }
938                 return mediaStoreUri;
939             } else {
940                 if (mediaProvider.update(mediaStoreUri, mediaValues, null, null) != 1) {
941                     Log.e(Constants.TAG, "Error updating MediaProvider, uri: " + mediaStoreUri
942                             + ", values: " + mediaValues);
943                 }
944                 return mediaStoreUri;
945             }
946         } catch (IllegalArgumentException ignored) {
947             // Insert or update MediaStore failed. At this point we can't do
948             // much here. If the file belongs to MediaStore collection, it will
949             // get added to MediaStore collection during next scan, and we will
950             // obtain the uri to the file in the next MediaStore#scanFile
951             // initiated by us
952             Log.w(Constants.TAG, "Couldn't update MediaStore for " + filePath, ignored);
953         } catch (RemoteException e) {
954             // Should not happen
955         }
956         return null;
957     }
958 
getMediaStoreUri(@onNull ContentProviderClient mediaProvider, @NonNull String filePath)959     private Uri getMediaStoreUri(@NonNull ContentProviderClient mediaProvider,
960             @NonNull String filePath) {
961         final Uri filesUri = MediaStore.setIncludePending(
962                 Helpers.getContentUriForPath(getContext(), filePath));
963         try (Cursor cursor = mediaProvider.query(filesUri,
964                 new String[] { MediaStore.Files.FileColumns._ID },
965                 MediaStore.Files.FileColumns.DATA + "=?", new String[] { filePath }, null, null)) {
966             if (cursor.moveToNext()) {
967                 return ContentUris.withAppendedId(filesUri, cursor.getLong(0));
968             }
969         } catch (RemoteException e) {
970             // Should not happen
971         }
972         return null;
973     }
974 
convertToMediaProviderValues(DownloadInfo info)975     ContentValues convertToMediaProviderValues(DownloadInfo info) {
976         final String filePath;
977         try {
978             filePath = new File(info.mFileName).getCanonicalPath();
979         } catch (IOException e) {
980             throw new IllegalArgumentException(e);
981         }
982         final boolean downloadCompleted = Downloads.Impl.isStatusCompleted(info.mStatus);
983         final ContentValues mediaValues = new ContentValues();
984         mediaValues.put(MediaStore.Downloads.DATA, filePath);
985         mediaValues.put(MediaStore.Downloads.VOLUME_NAME, Helpers.extractVolumeName(filePath));
986         mediaValues.put(MediaStore.Downloads.RELATIVE_PATH, Helpers.extractRelativePath(filePath));
987         mediaValues.put(MediaStore.Downloads.DISPLAY_NAME, Helpers.extractDisplayName(filePath));
988         mediaValues.put(MediaStore.Downloads.SIZE,
989                 downloadCompleted ? info.mTotalBytes : info.mCurrentBytes);
990         mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri);
991         mediaValues.put(MediaStore.Downloads.REFERER_URI, info.mReferer);
992         mediaValues.put(MediaStore.Downloads.MIME_TYPE, info.mMimeType);
993         mediaValues.put(MediaStore.Downloads.IS_PENDING, downloadCompleted ? 0 : 1);
994         mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
995                 Helpers.getPackageForUid(getContext(), info.mUid));
996         return mediaValues;
997     }
998 
getFileUri(String uriString)999     private static Uri getFileUri(String uriString) {
1000         final Uri uri = Uri.parse(uriString);
1001         return TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE) ? uri : null;
1002     }
1003 
ensureDefaultColumns(ContentValues values)1004     private void ensureDefaultColumns(ContentValues values) {
1005         final Integer dest = values.getAsInteger(COLUMN_DESTINATION);
1006         if (dest != null) {
1007             final int mediaScannable;
1008             final boolean visibleInDownloadsUi;
1009             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
1010                 mediaScannable = MEDIA_NOT_SCANNED;
1011                 visibleInDownloadsUi = true;
1012             } else if (dest != DESTINATION_FILE_URI
1013                     && dest != DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
1014                 mediaScannable = MEDIA_NOT_SCANNABLE;
1015                 visibleInDownloadsUi = false;
1016             } else {
1017                 final File file;
1018                 if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
1019                     final String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
1020                     file = new File(getFileUri(fileUri).getPath());
1021                 } else {
1022                     file = new File(values.getAsString(Downloads.Impl._DATA));
1023                 }
1024 
1025                 if (Helpers.isFileInExternalAndroidDirs(file.getAbsolutePath())) {
1026                     mediaScannable = MEDIA_NOT_SCANNABLE;
1027                     visibleInDownloadsUi = false;
1028                 } else if (Helpers.isFilenameValidInPublicDownloadsDir(file)) {
1029                     mediaScannable = MEDIA_NOT_SCANNED;
1030                     visibleInDownloadsUi = true;
1031                 } else {
1032                     mediaScannable = MEDIA_NOT_SCANNED;
1033                     visibleInDownloadsUi = false;
1034                 }
1035             }
1036             values.put(COLUMN_MEDIA_SCANNED, mediaScannable);
1037             values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, visibleInDownloadsUi);
1038         } else {
1039             if (!values.containsKey(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
1040                 values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, true);
1041             }
1042         }
1043     }
1044 
1045     /**
1046      * Check that the file URI provided for DESTINATION_FILE_URI is valid.
1047      */
checkFileUriDestination(ContentValues values)1048     private void checkFileUriDestination(ContentValues values) {
1049         String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
1050         if (fileUri == null) {
1051             throw new IllegalArgumentException(
1052                     "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
1053         }
1054         final Uri uri = getFileUri(fileUri);
1055         if (uri == null) {
1056             throw new IllegalArgumentException("Not a file URI: " + uri);
1057         }
1058         final String path = uri.getPath();
1059         if (path == null || ("/" + path + "/").contains("/../")) {
1060             throw new IllegalArgumentException("Invalid file URI: " + uri);
1061         }
1062 
1063         final File file;
1064         try {
1065             file = new File(path).getCanonicalFile();
1066             values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, Uri.fromFile(file).toString());
1067         } catch (IOException e) {
1068             throw new SecurityException(e);
1069         }
1070 
1071         final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
1072                 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
1073         Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(),
1074                 mAppOpsManager, getCallingAttributionTag(), isLegacyMode,
1075                 /* allowDownloadsDirOnly */ false);
1076     }
1077 
checkDownloadedFilePath(ContentValues values)1078     private void checkDownloadedFilePath(ContentValues values) {
1079         final String path = values.getAsString(Downloads.Impl._DATA);
1080         if (path == null || ("/" + path + "/").contains("/../")) {
1081             throw new IllegalArgumentException("Invalid file path: "
1082                     + (path == null ? "null" : path));
1083         }
1084 
1085         final File file;
1086         try {
1087             file = new File(path).getCanonicalFile();
1088             values.put(Downloads.Impl._DATA, file.getPath());
1089         } catch (IOException e) {
1090             throw new SecurityException(e);
1091         }
1092 
1093         if (!file.exists()) {
1094             throw new IllegalArgumentException("File doesn't exist: " + file);
1095         }
1096 
1097         if (Binder.getCallingPid() == Process.myPid()) {
1098             return;
1099         }
1100 
1101         final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
1102                 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
1103         Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(),
1104                 mAppOpsManager, getCallingAttributionTag(), isLegacyMode,
1105                 /* allowDownloadsDirOnly */ true);
1106     }
1107 
1108     /**
1109      * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
1110      * constraints in the rest of the code. Apps without that may still access this provider through
1111      * the public API, but additional restrictions are imposed. We check those restrictions here.
1112      *
1113      * @param values ContentValues provided to insert()
1114      * @throws SecurityException if the caller has insufficient permissions
1115      */
checkInsertPermissions(ContentValues values)1116     private void checkInsertPermissions(ContentValues values) {
1117         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS)
1118                 == PackageManager.PERMISSION_GRANTED) {
1119             return;
1120         }
1121 
1122         getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
1123                 "INTERNET permission is required to use the download manager");
1124 
1125         // ensure the request fits within the bounds of a public API request
1126         // first copy so we can remove values
1127         values = new ContentValues(values);
1128 
1129         // check columns whose values are restricted
1130         enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE);
1131 
1132         // validate the destination column
1133         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
1134                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
1135             /* this row is inserted by
1136              * DownloadManager.addCompletedDownload(String, String, String,
1137              * boolean, String, String, long)
1138              */
1139             values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
1140             values.remove(Downloads.Impl._DATA);
1141             values.remove(Downloads.Impl.COLUMN_STATUS);
1142         }
1143         enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION,
1144                 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE,
1145                 Downloads.Impl.DESTINATION_FILE_URI,
1146                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
1147 
1148         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
1149                 == PackageManager.PERMISSION_GRANTED) {
1150             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
1151                     Request.VISIBILITY_HIDDEN,
1152                     Request.VISIBILITY_VISIBLE,
1153                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
1154                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
1155         } else {
1156             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
1157                     Request.VISIBILITY_VISIBLE,
1158                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
1159                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
1160         }
1161 
1162         // remove the rest of the columns that are allowed (with any value)
1163         values.remove(Downloads.Impl.COLUMN_URI);
1164         values.remove(Downloads.Impl.COLUMN_TITLE);
1165         values.remove(Downloads.Impl.COLUMN_DESCRIPTION);
1166         values.remove(Downloads.Impl.COLUMN_MIME_TYPE);
1167         values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert()
1168         values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
1169         values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
1170         values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
1171         values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
1172         values.remove(Downloads.Impl.COLUMN_FLAGS);
1173         values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
1174         values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
1175         values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
1176         Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
1177         while (iterator.hasNext()) {
1178             String key = iterator.next().getKey();
1179             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
1180                 iterator.remove();
1181             }
1182         }
1183 
1184         // any extra columns are extraneous and disallowed
1185         if (values.size() > 0) {
1186             StringBuilder error = new StringBuilder("Invalid columns in request: ");
1187             boolean first = true;
1188             for (Map.Entry<String, Object> entry : values.valueSet()) {
1189                 if (!first) {
1190                     error.append(", ");
1191                 }
1192                 error.append(entry.getKey());
1193                 first = false;
1194             }
1195             throw new SecurityException(error.toString());
1196         }
1197     }
1198 
1199     /**
1200      * Remove column from values, and throw a SecurityException if the value isn't within the
1201      * specified allowedValues.
1202      */
enforceAllowedValues(ContentValues values, String column, Object... allowedValues)1203     private void enforceAllowedValues(ContentValues values, String column,
1204             Object... allowedValues) {
1205         Object value = values.get(column);
1206         values.remove(column);
1207         for (Object allowedValue : allowedValues) {
1208             if (value == null && allowedValue == null) {
1209                 return;
1210             }
1211             if (value != null && value.equals(allowedValue)) {
1212                 return;
1213             }
1214         }
1215         throw new SecurityException("Invalid value for " + column + ": " + value);
1216     }
1217 
queryCleared(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort)1218     private Cursor queryCleared(Uri uri, String[] projection, String selection,
1219             String[] selectionArgs, String sort) {
1220         final long token = Binder.clearCallingIdentity();
1221         try {
1222             return query(uri, projection, selection, selectionArgs, sort);
1223         } finally {
1224             Binder.restoreCallingIdentity(token);
1225         }
1226     }
1227 
1228     /**
1229      * Starts a database query
1230      */
1231     @Override
query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort)1232     public Cursor query(final Uri uri, String[] projection,
1233              final String selection, final String[] selectionArgs,
1234              final String sort) {
1235 
1236         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1237 
1238         int match = sURIMatcher.match(uri);
1239         if (match == -1) {
1240             if (Constants.LOGV) {
1241                 Log.v(Constants.TAG, "querying unknown URI: " + uri);
1242             }
1243             throw new IllegalArgumentException("Unknown URI: " + uri);
1244         }
1245 
1246         if (match == MY_DOWNLOADS_ID_HEADERS || match == ALL_DOWNLOADS_ID_HEADERS) {
1247             if (projection != null || selection != null || sort != null) {
1248                 throw new UnsupportedOperationException("Request header queries do not support "
1249                                                         + "projections, selections or sorting");
1250             }
1251 
1252             // Headers are only available to callers with full access.
1253             getContext().enforceCallingOrSelfPermission(
1254                     Downloads.Impl.PERMISSION_ACCESS_ALL, Constants.TAG);
1255 
1256             final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1257             projection = new String[] {
1258                     Downloads.Impl.RequestHeaders.COLUMN_HEADER,
1259                     Downloads.Impl.RequestHeaders.COLUMN_VALUE
1260             };
1261             return qb.query(db, projection, null, null, null, null, null);
1262         }
1263 
1264         if (Constants.LOGVV) {
1265             logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
1266         }
1267 
1268         final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1269 
1270         final Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sort);
1271 
1272         if (ret != null) {
1273             ret.setNotificationUri(getContext().getContentResolver(), uri);
1274             if (Constants.LOGVV) {
1275                 Log.v(Constants.TAG,
1276                         "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
1277             }
1278         } else {
1279             if (Constants.LOGV) {
1280                 Log.v(Constants.TAG, "query failed in downloads database");
1281             }
1282         }
1283 
1284         return ret;
1285     }
1286 
logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db)1287     private void logVerboseQueryInfo(String[] projection, final String selection,
1288             final String[] selectionArgs, final String sort, SQLiteDatabase db) {
1289         java.lang.StringBuilder sb = new java.lang.StringBuilder();
1290         sb.append("starting query, database is ");
1291         if (db != null) {
1292             sb.append("not ");
1293         }
1294         sb.append("null; ");
1295         if (projection == null) {
1296             sb.append("projection is null; ");
1297         } else if (projection.length == 0) {
1298             sb.append("projection is empty; ");
1299         } else {
1300             for (int i = 0; i < projection.length; ++i) {
1301                 sb.append("projection[");
1302                 sb.append(i);
1303                 sb.append("] is ");
1304                 sb.append(projection[i]);
1305                 sb.append("; ");
1306             }
1307         }
1308         sb.append("selection is ");
1309         sb.append(selection);
1310         sb.append("; ");
1311         if (selectionArgs == null) {
1312             sb.append("selectionArgs is null; ");
1313         } else if (selectionArgs.length == 0) {
1314             sb.append("selectionArgs is empty; ");
1315         } else {
1316             for (int i = 0; i < selectionArgs.length; ++i) {
1317                 sb.append("selectionArgs[");
1318                 sb.append(i);
1319                 sb.append("] is ");
1320                 sb.append(selectionArgs[i]);
1321                 sb.append("; ");
1322             }
1323         }
1324         sb.append("sort is ");
1325         sb.append(sort);
1326         sb.append(".");
1327         Log.v(Constants.TAG, sb.toString());
1328     }
1329 
getDownloadIdFromUri(final Uri uri)1330     private String getDownloadIdFromUri(final Uri uri) {
1331         return uri.getPathSegments().get(1);
1332     }
1333 
1334     /**
1335      * Insert request headers for a download into the DB.
1336      */
insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values)1337     private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
1338         ContentValues rowValues = new ContentValues();
1339         rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
1340         for (Map.Entry<String, Object> entry : values.valueSet()) {
1341             String key = entry.getKey();
1342             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
1343                 String headerLine = entry.getValue().toString();
1344                 if (!headerLine.contains(":")) {
1345                     throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
1346                 }
1347                 String[] parts = headerLine.split(":", 2);
1348                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
1349                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
1350                 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
1351             }
1352         }
1353     }
1354 
1355     /**
1356      * Updates a row in the database
1357      */
1358     @Override
update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)1359     public int update(final Uri uri, final ContentValues values,
1360             final String where, final String[] whereArgs) {
1361         final Context context = getContext();
1362         final ContentResolver resolver = context.getContentResolver();
1363 
1364         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1365 
1366         int count;
1367         boolean updateSchedule = false;
1368         boolean isCompleting = false;
1369 
1370         ContentValues filteredValues;
1371         if (Binder.getCallingPid() != Process.myPid()) {
1372             filteredValues = new ContentValues();
1373             copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
1374             copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues);
1375             Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
1376             if (i != null) {
1377                 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
1378                 updateSchedule = true;
1379             }
1380 
1381             copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
1382             copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
1383             copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues);
1384             copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
1385             copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues);
1386         } else {
1387             filteredValues = values;
1388             String filename = values.getAsString(Downloads.Impl._DATA);
1389             if (filename != null) {
1390                 try {
1391                     filteredValues.put(Downloads.Impl._DATA, new File(filename).getCanonicalPath());
1392                 } catch (IOException e) {
1393                     throw new IllegalStateException("Invalid path: " + filename);
1394                 }
1395 
1396                 Cursor c = null;
1397                 try {
1398                     c = query(uri, new String[]
1399                             { Downloads.Impl.COLUMN_TITLE }, null, null, null);
1400                     if (!c.moveToFirst() || c.getString(0).isEmpty()) {
1401                         values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
1402                     }
1403                 } finally {
1404                     IoUtils.closeQuietly(c);
1405                 }
1406             }
1407 
1408             Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
1409             boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING;
1410             boolean isUserBypassingSizeLimit =
1411                 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
1412             if (isRestart || isUserBypassingSizeLimit) {
1413                 updateSchedule = true;
1414             }
1415             isCompleting = status != null && Downloads.Impl.isStatusCompleted(status);
1416         }
1417 
1418         int match = sURIMatcher.match(uri);
1419         switch (match) {
1420             case MY_DOWNLOADS:
1421             case MY_DOWNLOADS_ID:
1422             case ALL_DOWNLOADS:
1423             case ALL_DOWNLOADS_ID:
1424                 if (filteredValues.size() == 0) {
1425                     count = 0;
1426                     break;
1427                 }
1428 
1429                 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1430                 count = qb.update(db, filteredValues, where, whereArgs);
1431                 final CallingIdentity token = clearCallingIdentity();
1432                 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null);
1433                         ContentProviderClient client = getContext().getContentResolver()
1434                                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
1435                     final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver,
1436                             cursor);
1437                     final DownloadInfo info = new DownloadInfo(context);
1438                     final ContentValues updateValues = new ContentValues();
1439                     while (cursor.moveToNext()) {
1440                         reader.updateFromDatabase(info);
1441                         final boolean visibleToUser = info.mIsVisibleInDownloadsUi
1442                                 || (info.mMediaScanned != MEDIA_NOT_SCANNABLE);
1443                         if (info.mFileName == null) {
1444                             if (info.mMediaStoreUri != null) {
1445                                 // If there was a mediastore entry, it would be deleted in it's
1446                                 // next idle pass.
1447                                 updateValues.clear();
1448                                 updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
1449                                 qb.update(db, updateValues, Downloads.Impl._ID + "=?",
1450                                         new String[] { Long.toString(info.mId) });
1451                             }
1452                         } else if ((info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
1453                                 || info.mDestination == Downloads.Impl.DESTINATION_FILE_URI
1454                                 || info.mDestination == Downloads.Impl
1455                                         .DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
1456                                 && visibleToUser) {
1457                             final ContentValues mediaValues = convertToMediaProviderValues(info);
1458                             final Uri mediaStoreUri;
1459                             if (Downloads.Impl.isStatusCompleted(info.mStatus)) {
1460                                 // Set size to 0 to ensure MediaScanner will scan this file.
1461                                 mediaValues.put(MediaStore.Downloads.SIZE, 0);
1462                                 updateMediaProvider(client, mediaValues);
1463                                 mediaStoreUri = triggerMediaScan(client, new File(info.mFileName));
1464                             } else {
1465                                 // Don't insert/update MediaStore db until the download is complete.
1466                                 // Incomplete files can only be inserted to MediaStore by setting
1467                                 // IS_PENDING=1 and using RELATIVE_PATH and DISPLAY_NAME in
1468                                 // MediaProvider#insert operation. We use DATA column, IS_PENDING
1469                                 // with DATA column will not be respected by MediaProvider.
1470                                 mediaStoreUri = null;
1471                             }
1472                             if (!TextUtils.equals(info.mMediaStoreUri,
1473                                     mediaStoreUri == null ? null : mediaStoreUri.toString())) {
1474                                 updateValues.clear();
1475                                 if (mediaStoreUri == null) {
1476                                     updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
1477                                     updateValues.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
1478                                     updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_NOT_SCANNED);
1479                                 } else {
1480                                     updateValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
1481                                             mediaStoreUri.toString());
1482                                     updateValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
1483                                             mediaStoreUri.toString());
1484                                     updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED);
1485                                 }
1486                                 qb.update(db, updateValues, Downloads.Impl._ID + "=?",
1487                                         new String[] { Long.toString(info.mId) });
1488                             }
1489                         }
1490                         if (updateSchedule) {
1491                             Helpers.scheduleJob(context, info);
1492                         }
1493                         if (isCompleting) {
1494                             info.sendIntentIfRequested();
1495                         }
1496                     }
1497                 } finally {
1498                     restoreCallingIdentity(token);
1499                 }
1500                 break;
1501 
1502             default:
1503                 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
1504                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
1505         }
1506 
1507         notifyContentChanged(uri, match);
1508         return count;
1509     }
1510 
1511     /**
1512      * Notify of a change through both URIs (/my_downloads and /all_downloads)
1513      * @param uri either URI for the changed download(s)
1514      * @param uriMatch the match ID from {@link #sURIMatcher}
1515      */
notifyContentChanged(final Uri uri, int uriMatch)1516     private void notifyContentChanged(final Uri uri, int uriMatch) {
1517         Long downloadId = null;
1518         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
1519             downloadId = Long.parseLong(getDownloadIdFromUri(uri));
1520         }
1521         for (Uri uriToNotify : BASE_URIS) {
1522             if (downloadId != null) {
1523                 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
1524             }
1525             getContext().getContentResolver().notifyChange(uriToNotify, null);
1526         }
1527     }
1528 
1529     /**
1530      * Create a query builder that filters access to the underlying database
1531      * based on both the requested {@link Uri} and permissions of the caller.
1532      */
getQueryBuilder(final Uri uri, int match)1533     private SQLiteQueryBuilder getQueryBuilder(final Uri uri, int match) {
1534         final String table;
1535         final Map<String, String> projectionMap;
1536 
1537         final StringBuilder where = new StringBuilder();
1538         switch (match) {
1539             // The "my_downloads" view normally limits the caller to operating
1540             // on downloads that they either directly own, or have been given
1541             // indirect ownership of via OTHER_UID.
1542             case MY_DOWNLOADS_ID:
1543                 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri));
1544                 // fall-through
1545             case MY_DOWNLOADS:
1546                 table = DB_TABLE;
1547                 projectionMap = sDownloadsMap;
1548                 if (getContext().checkCallingOrSelfPermission(
1549                         PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) {
1550                     appendWhereExpression(where, Constants.UID + "=" + Binder.getCallingUid()
1551                             + " OR " + COLUMN_OTHER_UID + "=" + Binder.getCallingUid());
1552                 }
1553                 break;
1554 
1555             // The "all_downloads" view is already limited via <path-permission>
1556             // to only callers holding the ACCESS_ALL_DOWNLOADS permission, but
1557             // access may also be delegated via Uri permission grants.
1558             case ALL_DOWNLOADS_ID:
1559                 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri));
1560                 // fall-through
1561             case ALL_DOWNLOADS:
1562                 table = DB_TABLE;
1563                 projectionMap = sDownloadsMap;
1564                 break;
1565 
1566             // Headers are limited to callers holding the ACCESS_ALL_DOWNLOADS
1567             // permission, since they're only needed for executing downloads.
1568             case MY_DOWNLOADS_ID_HEADERS:
1569             case ALL_DOWNLOADS_ID_HEADERS:
1570                 table = Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE;
1571                 projectionMap = sHeadersMap;
1572                 appendWhereExpression(where, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
1573                         + getDownloadIdFromUri(uri));
1574                 break;
1575 
1576             default:
1577                 throw new UnsupportedOperationException("Unknown URI: " + uri);
1578         }
1579 
1580         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1581         qb.setTables(table);
1582         qb.setProjectionMap(projectionMap);
1583         qb.setStrict(true);
1584         qb.setStrictColumns(true);
1585         qb.setStrictGrammar(true);
1586         qb.appendWhere(where);
1587         return qb;
1588     }
1589 
appendWhereExpression(StringBuilder sb, String expression)1590     private static void appendWhereExpression(StringBuilder sb, String expression) {
1591         if (sb.length() > 0) {
1592             sb.append(" AND ");
1593         }
1594         sb.append('(').append(expression).append(')');
1595     }
1596 
1597     /**
1598      * Deletes a row in the database
1599      */
1600     @Override
delete(final Uri uri, final String where, final String[] whereArgs)1601     public int delete(final Uri uri, final String where, final String[] whereArgs) {
1602         final Context context = getContext();
1603         final ContentResolver resolver = context.getContentResolver();
1604         final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
1605 
1606         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1607         int count;
1608         int match = sURIMatcher.match(uri);
1609         switch (match) {
1610             case MY_DOWNLOADS:
1611             case MY_DOWNLOADS_ID:
1612             case ALL_DOWNLOADS:
1613             case ALL_DOWNLOADS_ID:
1614                 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
1615                 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null)) {
1616                     final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
1617                     final DownloadInfo info = new DownloadInfo(context);
1618                     while (cursor.moveToNext()) {
1619                         reader.updateFromDatabase(info);
1620                         scheduler.cancel((int) info.mId);
1621 
1622                         revokeAllDownloadsPermission(info.mId);
1623                         DownloadStorageProvider.onDownloadProviderDelete(getContext(), info.mId);
1624 
1625                         final String path = info.mFileName;
1626                         if (!TextUtils.isEmpty(path)) {
1627                             try {
1628                                 final File file = new File(path).getCanonicalFile();
1629                                 if (Helpers.isFilenameValid(getContext(), file)) {
1630                                     Log.v(Constants.TAG,
1631                                             "Deleting " + file + " via provider delete");
1632                                     file.delete();
1633                                     MediaStore.scanFile(getContext().getContentResolver(), file);
1634                                 } else {
1635                                     Log.d(Constants.TAG, "Ignoring invalid file: " + file);
1636                                 }
1637                             } catch (IOException e) {
1638                                 Log.e(Constants.TAG, "Couldn't delete file: " + path, e);
1639                             }
1640                         }
1641 
1642                         // If the download wasn't completed yet, we're
1643                         // effectively completing it now, and we need to send
1644                         // any requested broadcasts
1645                         if (!Downloads.Impl.isStatusCompleted(info.mStatus)) {
1646                             info.sendIntentIfRequested();
1647                         }
1648 
1649                         // Delete any headers for this download
1650                         db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE,
1651                                 Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=?",
1652                                 new String[] { Long.toString(info.mId) });
1653                     }
1654                 }
1655 
1656                 count = qb.delete(db, where, whereArgs);
1657                 break;
1658 
1659             default:
1660                 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
1661                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
1662         }
1663         notifyContentChanged(uri, match);
1664         final long token = Binder.clearCallingIdentity();
1665         try {
1666             Helpers.getDownloadNotifier(getContext()).update();
1667         } finally {
1668             Binder.restoreCallingIdentity(token);
1669         }
1670         return count;
1671     }
1672 
1673     /**
1674      * Remotely opens a file
1675      */
1676     @Override
openFile(final Uri uri, String mode)1677     public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
1678         if (Constants.LOGVV) {
1679             logVerboseOpenFileInfo(uri, mode);
1680         }
1681 
1682         // Perform normal query to enforce caller identity access before
1683         // clearing it to reach internal-only columns
1684         final Cursor probeCursor = query(uri, new String[] {
1685                 Downloads.Impl._DATA }, null, null, null);
1686         try {
1687             if ((probeCursor == null) || (probeCursor.getCount() == 0)) {
1688                 throw new FileNotFoundException(
1689                         "No file found for " + uri + " as UID " + Binder.getCallingUid());
1690             }
1691         } finally {
1692             IoUtils.closeQuietly(probeCursor);
1693         }
1694 
1695         final Cursor cursor = queryCleared(uri, new String[] {
1696                 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS,
1697                 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null,
1698                 null, null);
1699         final String path;
1700         final boolean shouldScan;
1701         try {
1702             int count = (cursor != null) ? cursor.getCount() : 0;
1703             if (count != 1) {
1704                 // If there is not exactly one result, throw an appropriate exception.
1705                 if (count == 0) {
1706                     throw new FileNotFoundException("No entry for " + uri);
1707                 }
1708                 throw new FileNotFoundException("Multiple items at " + uri);
1709             }
1710 
1711             if (cursor.moveToFirst()) {
1712                 final int status = cursor.getInt(1);
1713                 final int destination = cursor.getInt(2);
1714                 final int mediaScanned = cursor.getInt(3);
1715 
1716                 path = cursor.getString(0);
1717                 shouldScan = Downloads.Impl.isStatusSuccess(status) && (
1718                         destination == Downloads.Impl.DESTINATION_EXTERNAL
1719                         || destination == Downloads.Impl.DESTINATION_FILE_URI
1720                         || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
1721                         && mediaScanned != Downloads.Impl.MEDIA_NOT_SCANNABLE;
1722             } else {
1723                 throw new FileNotFoundException("Failed moveToFirst");
1724             }
1725         } finally {
1726             IoUtils.closeQuietly(cursor);
1727         }
1728 
1729         if (path == null) {
1730             throw new FileNotFoundException("No filename found.");
1731         }
1732 
1733         final File file;
1734         try {
1735             file = new File(path).getCanonicalFile();
1736         } catch (IOException e) {
1737             throw new FileNotFoundException(e.getMessage());
1738         }
1739 
1740         if (!Helpers.isFilenameValid(getContext(), file)) {
1741             throw new FileNotFoundException("Invalid file: " + file);
1742         }
1743 
1744         final int pfdMode = ParcelFileDescriptor.parseMode(mode);
1745         if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) {
1746             return ParcelFileDescriptor.open(file, pfdMode);
1747         } else {
1748             try {
1749                 // When finished writing, update size and timestamp
1750                 return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(),
1751                         new OnCloseListener() {
1752                     @Override
1753                     public void onClose(IOException e) {
1754                         final ContentValues values = new ContentValues();
1755                         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
1756                         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
1757                                 mSystemFacade.currentTimeMillis());
1758                         update(uri, values, null, null);
1759 
1760                         if (shouldScan) {
1761                             final Intent intent = new Intent(
1762                                     Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
1763                             intent.setData(Uri.fromFile(file));
1764                             getContext().sendBroadcast(intent);
1765                         }
1766                     }
1767                 });
1768             } catch (IOException e) {
1769                 throw new FileNotFoundException("Failed to open for writing: " + e);
1770             }
1771         }
1772     }
1773 
1774     @Override
1775     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1776         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 120);
1777 
1778         pw.println("Downloads updated in last hour:");
1779         pw.increaseIndent();
1780 
1781         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1782         final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
1783         final Cursor cursor = db.query(DB_TABLE, null,
1784                 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
1785                 Downloads.Impl._ID + " ASC");
1786         try {
1787             final String[] cols = cursor.getColumnNames();
1788             final int idCol = cursor.getColumnIndex(BaseColumns._ID);
1789             while (cursor.moveToNext()) {
1790                 pw.println("Download #" + cursor.getInt(idCol) + ":");
1791                 pw.increaseIndent();
1792                 for (int i = 0; i < cols.length; i++) {
1793                     // Omit sensitive data when dumping
1794                     if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
1795                         continue;
1796                     }
1797                     pw.printPair(cols[i], cursor.getString(i));
1798                 }
1799                 pw.println();
1800                 pw.decreaseIndent();
1801             }
1802         } finally {
1803             cursor.close();
1804         }
1805 
1806         pw.decreaseIndent();
1807     }
1808 
1809     private void logVerboseOpenFileInfo(Uri uri, String mode) {
1810         Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
1811                 + ", uid: " + Binder.getCallingUid());
1812         Cursor cursor = query(Downloads.Impl.CONTENT_URI,
1813                 new String[] { "_id" }, null, null, "_id");
1814         if (cursor == null) {
1815             Log.v(Constants.TAG, "null cursor in openFile");
1816         } else {
1817             try {
1818                 if (!cursor.moveToFirst()) {
1819                     Log.v(Constants.TAG, "empty cursor in openFile");
1820                 } else {
1821                     do {
1822                         Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
1823                     } while(cursor.moveToNext());
1824                 }
1825             } finally {
1826                 cursor.close();
1827             }
1828         }
1829         cursor = query(uri, new String[] { "_data" }, null, null, null);
1830         if (cursor == null) {
1831             Log.v(Constants.TAG, "null cursor in openFile");
1832         } else {
1833             try {
1834                 if (!cursor.moveToFirst()) {
1835                     Log.v(Constants.TAG, "empty cursor in openFile");
1836                 } else {
1837                     String filename = cursor.getString(0);
1838                     Log.v(Constants.TAG, "filename in openFile: " + filename);
1839                     if (new java.io.File(filename).isFile()) {
1840                         Log.v(Constants.TAG, "file exists in openFile");
1841                     }
1842                 }
1843             } finally {
1844                 cursor.close();
1845             }
1846         }
1847     }
1848 
1849     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
1850         Integer i = from.getAsInteger(key);
1851         if (i != null) {
1852             to.put(key, i);
1853         }
1854     }
1855 
1856     private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
1857         Boolean b = from.getAsBoolean(key);
1858         if (b != null) {
1859             to.put(key, b);
1860         }
1861     }
1862 
1863     private static final void copyString(String key, ContentValues from, ContentValues to) {
1864         String s = from.getAsString(key);
1865         if (s != null) {
1866             to.put(key, s);
1867         }
1868     }
1869 
1870     private static final void copyStringWithDefault(String key, ContentValues from,
1871             ContentValues to, String defaultValue) {
1872         copyString(key, from, to);
1873         if (!to.containsKey(key)) {
1874             to.put(key, defaultValue);
1875         }
1876     }
1877 
1878     private void grantAllDownloadsPermission(String toPackage, long id) {
1879         final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1880         getContext().grantUriPermission(toPackage, uri,
1881                 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
1882     }
1883 
1884     private void revokeAllDownloadsPermission(long id) {
1885         final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1886         getContext().revokeUriPermission(uri, ~0);
1887     }
1888 }
1889