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