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