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