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 (IllegalArgumentException ignored) { 946 // Insert or update MediaStore failed. At this point we can't do 947 // much here. If the file belongs to MediaStore collection, it will 948 // get added to MediaStore collection during next scan, and we will 949 // obtain the uri to the file in the next MediaStore#scanFile 950 // initiated by us 951 Log.w(Constants.TAG, "Couldn't update MediaStore for " + filePath, ignored); 952 } catch (RemoteException e) { 953 // Should not happen 954 } 955 return null; 956 } 957 getMediaStoreUri(@onNull ContentProviderClient mediaProvider, @NonNull String filePath)958 private Uri getMediaStoreUri(@NonNull ContentProviderClient mediaProvider, 959 @NonNull String filePath) { 960 final Uri filesUri = MediaStore.setIncludePending( 961 Helpers.getContentUriForPath(getContext(), filePath)); 962 try (Cursor cursor = mediaProvider.query(filesUri, 963 new String[] { MediaStore.Files.FileColumns._ID }, 964 MediaStore.Files.FileColumns.DATA + "=?", new String[] { filePath }, null, null)) { 965 if (cursor.moveToNext()) { 966 return ContentUris.withAppendedId(filesUri, cursor.getLong(0)); 967 } 968 } catch (RemoteException e) { 969 // Should not happen 970 } 971 return null; 972 } 973 convertToMediaProviderValues(DownloadInfo info)974 ContentValues convertToMediaProviderValues(DownloadInfo info) { 975 final String filePath; 976 try { 977 filePath = new File(info.mFileName).getCanonicalPath(); 978 } catch (IOException e) { 979 throw new IllegalArgumentException(e); 980 } 981 final boolean downloadCompleted = Downloads.Impl.isStatusCompleted(info.mStatus); 982 final ContentValues mediaValues = new ContentValues(); 983 mediaValues.put(MediaStore.Downloads.DATA, filePath); 984 mediaValues.put(MediaStore.Downloads.VOLUME_NAME, Helpers.extractVolumeName(filePath)); 985 mediaValues.put(MediaStore.Downloads.RELATIVE_PATH, Helpers.extractRelativePath(filePath)); 986 mediaValues.put(MediaStore.Downloads.DISPLAY_NAME, Helpers.extractDisplayName(filePath)); 987 mediaValues.put(MediaStore.Downloads.SIZE, 988 downloadCompleted ? info.mTotalBytes : info.mCurrentBytes); 989 mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri); 990 mediaValues.put(MediaStore.Downloads.REFERER_URI, info.mReferer); 991 mediaValues.put(MediaStore.Downloads.MIME_TYPE, info.mMimeType); 992 mediaValues.put(MediaStore.Downloads.IS_PENDING, downloadCompleted ? 0 : 1); 993 mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME, 994 Helpers.getPackageForUid(getContext(), info.mUid)); 995 return mediaValues; 996 } 997 getFileUri(String uriString)998 private static Uri getFileUri(String uriString) { 999 final Uri uri = Uri.parse(uriString); 1000 return TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE) ? uri : null; 1001 } 1002 ensureDefaultColumns(ContentValues values)1003 private void ensureDefaultColumns(ContentValues values) { 1004 final Integer dest = values.getAsInteger(COLUMN_DESTINATION); 1005 if (dest != null) { 1006 final int mediaScannable; 1007 final boolean visibleInDownloadsUi; 1008 if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 1009 mediaScannable = MEDIA_NOT_SCANNED; 1010 visibleInDownloadsUi = true; 1011 } else if (dest != DESTINATION_FILE_URI 1012 && dest != DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 1013 mediaScannable = MEDIA_NOT_SCANNABLE; 1014 visibleInDownloadsUi = false; 1015 } else { 1016 final File file; 1017 if (dest == Downloads.Impl.DESTINATION_FILE_URI) { 1018 final String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 1019 file = new File(getFileUri(fileUri).getPath()); 1020 } else { 1021 file = new File(values.getAsString(Downloads.Impl._DATA)); 1022 } 1023 1024 if (Helpers.isFileInExternalAndroidDirs(file.getAbsolutePath())) { 1025 mediaScannable = MEDIA_NOT_SCANNABLE; 1026 visibleInDownloadsUi = false; 1027 } else if (Helpers.isFilenameValidInPublicDownloadsDir(file)) { 1028 mediaScannable = MEDIA_NOT_SCANNED; 1029 visibleInDownloadsUi = true; 1030 } else { 1031 mediaScannable = MEDIA_NOT_SCANNED; 1032 visibleInDownloadsUi = false; 1033 } 1034 } 1035 values.put(COLUMN_MEDIA_SCANNED, mediaScannable); 1036 values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, visibleInDownloadsUi); 1037 } else { 1038 if (!values.containsKey(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) { 1039 values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, true); 1040 } 1041 } 1042 } 1043 1044 /** 1045 * Check that the file URI provided for DESTINATION_FILE_URI is valid. 1046 */ checkFileUriDestination(ContentValues values)1047 private void checkFileUriDestination(ContentValues values) { 1048 String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 1049 if (fileUri == null) { 1050 throw new IllegalArgumentException( 1051 "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT"); 1052 } 1053 final Uri uri = getFileUri(fileUri); 1054 if (uri == null) { 1055 throw new IllegalArgumentException("Not a file URI: " + uri); 1056 } 1057 final String path = uri.getPath(); 1058 if (path == null || ("/" + path + "/").contains("/../")) { 1059 throw new IllegalArgumentException("Invalid file URI: " + uri); 1060 } 1061 1062 final File file; 1063 try { 1064 file = new File(path).getCanonicalFile(); 1065 values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, Uri.fromFile(file).toString()); 1066 } catch (IOException e) { 1067 throw new SecurityException(e); 1068 } 1069 1070 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 1071 final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class); 1072 final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE, 1073 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED; 1074 1075 if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage()) 1076 || Helpers.isFilenameValidInKnownPublicDir(file.getAbsolutePath())) { 1077 // No permissions required for paths belonging to calling package or 1078 // public downloads dir. 1079 return; 1080 } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) { 1081 // Otherwise we require write permission 1082 getContext().enforceCallingOrSelfPermission( 1083 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 1084 "No permission to write to " + file); 1085 1086 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 1087 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(), 1088 Binder.getCallingUid(), getCallingAttributionTag(), null) 1089 != AppOpsManager.MODE_ALLOWED) { 1090 throw new SecurityException("No permission to write to " + file); 1091 } 1092 } else if (Helpers.isFilenameValidInExternalObbDir(file) && 1093 ((appOpsManager.noteOp( 1094 AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, 1095 Binder.getCallingUid(), getCallingPackage(), null, "obb_download") 1096 == AppOpsManager.MODE_ALLOWED) 1097 || (getContext().checkCallingOrSelfPermission( 1098 android.Manifest.permission.REQUEST_INSTALL_PACKAGES) 1099 == PackageManager.PERMISSION_GRANTED))) { 1100 // Installers are allowed to download in OBB dirs, even outside their own package 1101 return; 1102 } else { 1103 throw new SecurityException("Unsupported path " + file); 1104 } 1105 } 1106 checkDownloadedFilePath(ContentValues values)1107 private void checkDownloadedFilePath(ContentValues values) { 1108 final String path = values.getAsString(Downloads.Impl._DATA); 1109 if (path == null || ("/" + path + "/").contains("/../")) { 1110 throw new IllegalArgumentException("Invalid file path: " 1111 + (path == null ? "null" : path)); 1112 } 1113 1114 final File file; 1115 try { 1116 file = new File(path).getCanonicalFile(); 1117 values.put(Downloads.Impl._DATA, file.getPath()); 1118 } catch (IOException e) { 1119 throw new SecurityException(e); 1120 } 1121 1122 if (!file.exists()) { 1123 throw new IllegalArgumentException("File doesn't exist: " + file); 1124 } 1125 1126 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 1127 final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class); 1128 final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE, 1129 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED; 1130 1131 if (Binder.getCallingPid() == Process.myPid()) { 1132 return; 1133 } else if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage()) 1134 || Helpers.isFilenameValidInPublicDownloadsDir(file)) { 1135 // No permissions required for paths belonging to calling package or 1136 // public downloads dir. 1137 return; 1138 } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) { 1139 // Otherwise we require write permission 1140 getContext().enforceCallingOrSelfPermission( 1141 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 1142 "No permission to write to " + file); 1143 1144 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 1145 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(), 1146 Binder.getCallingUid(), getCallingAttributionTag(), null) 1147 != AppOpsManager.MODE_ALLOWED) { 1148 throw new SecurityException("No permission to write to " + file); 1149 } 1150 } else { 1151 throw new SecurityException("Unsupported path " + file); 1152 } 1153 } 1154 getCallingPackageTargetSdkVersion()1155 private int getCallingPackageTargetSdkVersion() { 1156 final String callingPackage = getCallingPackage(); 1157 if (callingPackage != null) { 1158 ApplicationInfo ai = null; 1159 try { 1160 ai = getContext().getPackageManager() 1161 .getApplicationInfo(callingPackage, 0); 1162 } catch (PackageManager.NameNotFoundException ignored) { 1163 } 1164 if (ai != null) { 1165 return ai.targetSdkVersion; 1166 } 1167 } 1168 return Build.VERSION_CODES.CUR_DEVELOPMENT; 1169 } 1170 1171 /** 1172 * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to 1173 * constraints in the rest of the code. Apps without that may still access this provider through 1174 * the public API, but additional restrictions are imposed. We check those restrictions here. 1175 * 1176 * @param values ContentValues provided to insert() 1177 * @throws SecurityException if the caller has insufficient permissions 1178 */ checkInsertPermissions(ContentValues values)1179 private void checkInsertPermissions(ContentValues values) { 1180 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS) 1181 == PackageManager.PERMISSION_GRANTED) { 1182 return; 1183 } 1184 1185 getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, 1186 "INTERNET permission is required to use the download manager"); 1187 1188 // ensure the request fits within the bounds of a public API request 1189 // first copy so we can remove values 1190 values = new ContentValues(values); 1191 1192 // check columns whose values are restricted 1193 enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE); 1194 1195 // validate the destination column 1196 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == 1197 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 1198 /* this row is inserted by 1199 * DownloadManager.addCompletedDownload(String, String, String, 1200 * boolean, String, String, long) 1201 */ 1202 values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES); 1203 values.remove(Downloads.Impl._DATA); 1204 values.remove(Downloads.Impl.COLUMN_STATUS); 1205 } 1206 enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION, 1207 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE, 1208 Downloads.Impl.DESTINATION_FILE_URI, 1209 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD); 1210 1211 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION) 1212 == PackageManager.PERMISSION_GRANTED) { 1213 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 1214 Request.VISIBILITY_HIDDEN, 1215 Request.VISIBILITY_VISIBLE, 1216 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 1217 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 1218 } else { 1219 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 1220 Request.VISIBILITY_VISIBLE, 1221 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 1222 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 1223 } 1224 1225 // remove the rest of the columns that are allowed (with any value) 1226 values.remove(Downloads.Impl.COLUMN_URI); 1227 values.remove(Downloads.Impl.COLUMN_TITLE); 1228 values.remove(Downloads.Impl.COLUMN_DESCRIPTION); 1229 values.remove(Downloads.Impl.COLUMN_MIME_TYPE); 1230 values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert() 1231 values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert() 1232 values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); 1233 values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING); 1234 values.remove(Downloads.Impl.COLUMN_ALLOW_METERED); 1235 values.remove(Downloads.Impl.COLUMN_FLAGS); 1236 values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI); 1237 values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED); 1238 values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE); 1239 Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator(); 1240 while (iterator.hasNext()) { 1241 String key = iterator.next().getKey(); 1242 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 1243 iterator.remove(); 1244 } 1245 } 1246 1247 // any extra columns are extraneous and disallowed 1248 if (values.size() > 0) { 1249 StringBuilder error = new StringBuilder("Invalid columns in request: "); 1250 boolean first = true; 1251 for (Map.Entry<String, Object> entry : values.valueSet()) { 1252 if (!first) { 1253 error.append(", "); 1254 } 1255 error.append(entry.getKey()); 1256 first = false; 1257 } 1258 throw new SecurityException(error.toString()); 1259 } 1260 } 1261 1262 /** 1263 * Remove column from values, and throw a SecurityException if the value isn't within the 1264 * specified allowedValues. 1265 */ enforceAllowedValues(ContentValues values, String column, Object... allowedValues)1266 private void enforceAllowedValues(ContentValues values, String column, 1267 Object... allowedValues) { 1268 Object value = values.get(column); 1269 values.remove(column); 1270 for (Object allowedValue : allowedValues) { 1271 if (value == null && allowedValue == null) { 1272 return; 1273 } 1274 if (value != null && value.equals(allowedValue)) { 1275 return; 1276 } 1277 } 1278 throw new SecurityException("Invalid value for " + column + ": " + value); 1279 } 1280 queryCleared(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort)1281 private Cursor queryCleared(Uri uri, String[] projection, String selection, 1282 String[] selectionArgs, String sort) { 1283 final long token = Binder.clearCallingIdentity(); 1284 try { 1285 return query(uri, projection, selection, selectionArgs, sort); 1286 } finally { 1287 Binder.restoreCallingIdentity(token); 1288 } 1289 } 1290 1291 /** 1292 * Starts a database query 1293 */ 1294 @Override query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort)1295 public Cursor query(final Uri uri, String[] projection, 1296 final String selection, final String[] selectionArgs, 1297 final String sort) { 1298 1299 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1300 1301 int match = sURIMatcher.match(uri); 1302 if (match == -1) { 1303 if (Constants.LOGV) { 1304 Log.v(Constants.TAG, "querying unknown URI: " + uri); 1305 } 1306 throw new IllegalArgumentException("Unknown URI: " + uri); 1307 } 1308 1309 if (match == MY_DOWNLOADS_ID_HEADERS || match == ALL_DOWNLOADS_ID_HEADERS) { 1310 if (projection != null || selection != null || sort != null) { 1311 throw new UnsupportedOperationException("Request header queries do not support " 1312 + "projections, selections or sorting"); 1313 } 1314 1315 // Headers are only available to callers with full access. 1316 getContext().enforceCallingOrSelfPermission( 1317 Downloads.Impl.PERMISSION_ACCESS_ALL, Constants.TAG); 1318 1319 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1320 projection = new String[] { 1321 Downloads.Impl.RequestHeaders.COLUMN_HEADER, 1322 Downloads.Impl.RequestHeaders.COLUMN_VALUE 1323 }; 1324 return qb.query(db, projection, null, null, null, null, null); 1325 } 1326 1327 if (Constants.LOGVV) { 1328 logVerboseQueryInfo(projection, selection, selectionArgs, sort, db); 1329 } 1330 1331 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1332 1333 final Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sort); 1334 1335 if (ret != null) { 1336 ret.setNotificationUri(getContext().getContentResolver(), uri); 1337 if (Constants.LOGVV) { 1338 Log.v(Constants.TAG, 1339 "created cursor " + ret + " on behalf of " + Binder.getCallingPid()); 1340 } 1341 } else { 1342 if (Constants.LOGV) { 1343 Log.v(Constants.TAG, "query failed in downloads database"); 1344 } 1345 } 1346 1347 return ret; 1348 } 1349 logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db)1350 private void logVerboseQueryInfo(String[] projection, final String selection, 1351 final String[] selectionArgs, final String sort, SQLiteDatabase db) { 1352 java.lang.StringBuilder sb = new java.lang.StringBuilder(); 1353 sb.append("starting query, database is "); 1354 if (db != null) { 1355 sb.append("not "); 1356 } 1357 sb.append("null; "); 1358 if (projection == null) { 1359 sb.append("projection is null; "); 1360 } else if (projection.length == 0) { 1361 sb.append("projection is empty; "); 1362 } else { 1363 for (int i = 0; i < projection.length; ++i) { 1364 sb.append("projection["); 1365 sb.append(i); 1366 sb.append("] is "); 1367 sb.append(projection[i]); 1368 sb.append("; "); 1369 } 1370 } 1371 sb.append("selection is "); 1372 sb.append(selection); 1373 sb.append("; "); 1374 if (selectionArgs == null) { 1375 sb.append("selectionArgs is null; "); 1376 } else if (selectionArgs.length == 0) { 1377 sb.append("selectionArgs is empty; "); 1378 } else { 1379 for (int i = 0; i < selectionArgs.length; ++i) { 1380 sb.append("selectionArgs["); 1381 sb.append(i); 1382 sb.append("] is "); 1383 sb.append(selectionArgs[i]); 1384 sb.append("; "); 1385 } 1386 } 1387 sb.append("sort is "); 1388 sb.append(sort); 1389 sb.append("."); 1390 Log.v(Constants.TAG, sb.toString()); 1391 } 1392 getDownloadIdFromUri(final Uri uri)1393 private String getDownloadIdFromUri(final Uri uri) { 1394 return uri.getPathSegments().get(1); 1395 } 1396 1397 /** 1398 * Insert request headers for a download into the DB. 1399 */ insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values)1400 private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) { 1401 ContentValues rowValues = new ContentValues(); 1402 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId); 1403 for (Map.Entry<String, Object> entry : values.valueSet()) { 1404 String key = entry.getKey(); 1405 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 1406 String headerLine = entry.getValue().toString(); 1407 if (!headerLine.contains(":")) { 1408 throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine); 1409 } 1410 String[] parts = headerLine.split(":", 2); 1411 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim()); 1412 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim()); 1413 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues); 1414 } 1415 } 1416 } 1417 1418 /** 1419 * Updates a row in the database 1420 */ 1421 @Override update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)1422 public int update(final Uri uri, final ContentValues values, 1423 final String where, final String[] whereArgs) { 1424 final Context context = getContext(); 1425 final ContentResolver resolver = context.getContentResolver(); 1426 1427 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1428 1429 int count; 1430 boolean updateSchedule = false; 1431 boolean isCompleting = false; 1432 1433 ContentValues filteredValues; 1434 if (Binder.getCallingPid() != Process.myPid()) { 1435 filteredValues = new ContentValues(); 1436 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); 1437 copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues); 1438 Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL); 1439 if (i != null) { 1440 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i); 1441 updateSchedule = true; 1442 } 1443 1444 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); 1445 copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues); 1446 copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues); 1447 copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues); 1448 copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues); 1449 } else { 1450 filteredValues = values; 1451 String filename = values.getAsString(Downloads.Impl._DATA); 1452 if (filename != null) { 1453 try { 1454 filteredValues.put(Downloads.Impl._DATA, new File(filename).getCanonicalPath()); 1455 } catch (IOException e) { 1456 throw new IllegalStateException("Invalid path: " + filename); 1457 } 1458 1459 Cursor c = null; 1460 try { 1461 c = query(uri, new String[] 1462 { Downloads.Impl.COLUMN_TITLE }, null, null, null); 1463 if (!c.moveToFirst() || c.getString(0).isEmpty()) { 1464 values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName()); 1465 } 1466 } finally { 1467 IoUtils.closeQuietly(c); 1468 } 1469 } 1470 1471 Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS); 1472 boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING; 1473 boolean isUserBypassingSizeLimit = 1474 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); 1475 if (isRestart || isUserBypassingSizeLimit) { 1476 updateSchedule = true; 1477 } 1478 isCompleting = status != null && Downloads.Impl.isStatusCompleted(status); 1479 } 1480 1481 int match = sURIMatcher.match(uri); 1482 switch (match) { 1483 case MY_DOWNLOADS: 1484 case MY_DOWNLOADS_ID: 1485 case ALL_DOWNLOADS: 1486 case ALL_DOWNLOADS_ID: 1487 if (filteredValues.size() == 0) { 1488 count = 0; 1489 break; 1490 } 1491 1492 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1493 count = qb.update(db, filteredValues, where, whereArgs); 1494 final CallingIdentity token = clearCallingIdentity(); 1495 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null); 1496 ContentProviderClient client = getContext().getContentResolver() 1497 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 1498 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, 1499 cursor); 1500 final DownloadInfo info = new DownloadInfo(context); 1501 final ContentValues updateValues = new ContentValues(); 1502 while (cursor.moveToNext()) { 1503 reader.updateFromDatabase(info); 1504 final boolean visibleToUser = info.mIsVisibleInDownloadsUi 1505 || (info.mMediaScanned != MEDIA_NOT_SCANNABLE); 1506 if (info.mFileName == null) { 1507 if (info.mMediaStoreUri != null) { 1508 // If there was a mediastore entry, it would be deleted in it's 1509 // next idle pass. 1510 updateValues.clear(); 1511 updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI); 1512 qb.update(db, updateValues, Downloads.Impl._ID + "=?", 1513 new String[] { Long.toString(info.mId) }); 1514 } 1515 } else if ((info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL 1516 || info.mDestination == Downloads.Impl.DESTINATION_FILE_URI 1517 || info.mDestination == Downloads.Impl 1518 .DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 1519 && visibleToUser) { 1520 final ContentValues mediaValues = convertToMediaProviderValues(info); 1521 final Uri mediaStoreUri; 1522 if (Downloads.Impl.isStatusCompleted(info.mStatus)) { 1523 // Set size to 0 to ensure MediaScanner will scan this file. 1524 mediaValues.put(MediaStore.Downloads.SIZE, 0); 1525 updateMediaProvider(client, mediaValues); 1526 mediaStoreUri = triggerMediaScan(client, new File(info.mFileName)); 1527 } else { 1528 // Don't insert/update MediaStore db until the download is complete. 1529 // Incomplete files can only be inserted to MediaStore by setting 1530 // IS_PENDING=1 and using RELATIVE_PATH and DISPLAY_NAME in 1531 // MediaProvider#insert operation. We use DATA column, IS_PENDING 1532 // with DATA column will not be respected by MediaProvider. 1533 mediaStoreUri = null; 1534 } 1535 if (!TextUtils.equals(info.mMediaStoreUri, 1536 mediaStoreUri == null ? null : mediaStoreUri.toString())) { 1537 updateValues.clear(); 1538 if (mediaStoreUri == null) { 1539 updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI); 1540 updateValues.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI); 1541 updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_NOT_SCANNED); 1542 } else { 1543 updateValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI, 1544 mediaStoreUri.toString()); 1545 updateValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, 1546 mediaStoreUri.toString()); 1547 updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED); 1548 } 1549 qb.update(db, updateValues, Downloads.Impl._ID + "=?", 1550 new String[] { Long.toString(info.mId) }); 1551 } 1552 } 1553 if (updateSchedule) { 1554 Helpers.scheduleJob(context, info); 1555 } 1556 if (isCompleting) { 1557 info.sendIntentIfRequested(); 1558 } 1559 } 1560 } finally { 1561 restoreCallingIdentity(token); 1562 } 1563 break; 1564 1565 default: 1566 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri); 1567 throw new UnsupportedOperationException("Cannot update URI: " + uri); 1568 } 1569 1570 notifyContentChanged(uri, match); 1571 return count; 1572 } 1573 1574 /** 1575 * Notify of a change through both URIs (/my_downloads and /all_downloads) 1576 * @param uri either URI for the changed download(s) 1577 * @param uriMatch the match ID from {@link #sURIMatcher} 1578 */ notifyContentChanged(final Uri uri, int uriMatch)1579 private void notifyContentChanged(final Uri uri, int uriMatch) { 1580 Long downloadId = null; 1581 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) { 1582 downloadId = Long.parseLong(getDownloadIdFromUri(uri)); 1583 } 1584 for (Uri uriToNotify : BASE_URIS) { 1585 if (downloadId != null) { 1586 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId); 1587 } 1588 getContext().getContentResolver().notifyChange(uriToNotify, null); 1589 } 1590 } 1591 1592 /** 1593 * Create a query builder that filters access to the underlying database 1594 * based on both the requested {@link Uri} and permissions of the caller. 1595 */ getQueryBuilder(final Uri uri, int match)1596 private SQLiteQueryBuilder getQueryBuilder(final Uri uri, int match) { 1597 final String table; 1598 final Map<String, String> projectionMap; 1599 1600 final StringBuilder where = new StringBuilder(); 1601 switch (match) { 1602 // The "my_downloads" view normally limits the caller to operating 1603 // on downloads that they either directly own, or have been given 1604 // indirect ownership of via OTHER_UID. 1605 case MY_DOWNLOADS_ID: 1606 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri)); 1607 // fall-through 1608 case MY_DOWNLOADS: 1609 table = DB_TABLE; 1610 projectionMap = sDownloadsMap; 1611 if (getContext().checkCallingOrSelfPermission( 1612 PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) { 1613 appendWhereExpression(where, Constants.UID + "=" + Binder.getCallingUid() 1614 + " OR " + COLUMN_OTHER_UID + "=" + Binder.getCallingUid()); 1615 } 1616 break; 1617 1618 // The "all_downloads" view is already limited via <path-permission> 1619 // to only callers holding the ACCESS_ALL_DOWNLOADS permission, but 1620 // access may also be delegated via Uri permission grants. 1621 case ALL_DOWNLOADS_ID: 1622 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri)); 1623 // fall-through 1624 case ALL_DOWNLOADS: 1625 table = DB_TABLE; 1626 projectionMap = sDownloadsMap; 1627 break; 1628 1629 // Headers are limited to callers holding the ACCESS_ALL_DOWNLOADS 1630 // permission, since they're only needed for executing downloads. 1631 case MY_DOWNLOADS_ID_HEADERS: 1632 case ALL_DOWNLOADS_ID_HEADERS: 1633 table = Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE; 1634 projectionMap = sHeadersMap; 1635 appendWhereExpression(where, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" 1636 + getDownloadIdFromUri(uri)); 1637 break; 1638 1639 default: 1640 throw new UnsupportedOperationException("Unknown URI: " + uri); 1641 } 1642 1643 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1644 qb.setTables(table); 1645 qb.setProjectionMap(projectionMap); 1646 qb.setStrict(true); 1647 qb.setStrictColumns(true); 1648 qb.setStrictGrammar(true); 1649 qb.appendWhere(where); 1650 return qb; 1651 } 1652 appendWhereExpression(StringBuilder sb, String expression)1653 private static void appendWhereExpression(StringBuilder sb, String expression) { 1654 if (sb.length() > 0) { 1655 sb.append(" AND "); 1656 } 1657 sb.append('(').append(expression).append(')'); 1658 } 1659 1660 /** 1661 * Deletes a row in the database 1662 */ 1663 @Override delete(final Uri uri, final String where, final String[] whereArgs)1664 public int delete(final Uri uri, final String where, final String[] whereArgs) { 1665 final Context context = getContext(); 1666 final ContentResolver resolver = context.getContentResolver(); 1667 final JobScheduler scheduler = context.getSystemService(JobScheduler.class); 1668 1669 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1670 int count; 1671 int match = sURIMatcher.match(uri); 1672 switch (match) { 1673 case MY_DOWNLOADS: 1674 case MY_DOWNLOADS_ID: 1675 case ALL_DOWNLOADS: 1676 case ALL_DOWNLOADS_ID: 1677 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1678 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null)) { 1679 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); 1680 final DownloadInfo info = new DownloadInfo(context); 1681 while (cursor.moveToNext()) { 1682 reader.updateFromDatabase(info); 1683 scheduler.cancel((int) info.mId); 1684 1685 revokeAllDownloadsPermission(info.mId); 1686 DownloadStorageProvider.onDownloadProviderDelete(getContext(), info.mId); 1687 1688 final String path = info.mFileName; 1689 if (!TextUtils.isEmpty(path)) { 1690 try { 1691 final File file = new File(path).getCanonicalFile(); 1692 if (Helpers.isFilenameValid(getContext(), file)) { 1693 Log.v(Constants.TAG, 1694 "Deleting " + file + " via provider delete"); 1695 file.delete(); 1696 MediaStore.scanFile(getContext().getContentResolver(), file); 1697 } else { 1698 Log.d(Constants.TAG, "Ignoring invalid file: " + file); 1699 } 1700 } catch (IOException e) { 1701 Log.e(Constants.TAG, "Couldn't delete file: " + path, e); 1702 } 1703 } 1704 1705 // If the download wasn't completed yet, we're 1706 // effectively completing it now, and we need to send 1707 // any requested broadcasts 1708 if (!Downloads.Impl.isStatusCompleted(info.mStatus)) { 1709 info.sendIntentIfRequested(); 1710 } 1711 1712 // Delete any headers for this download 1713 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, 1714 Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=?", 1715 new String[] { Long.toString(info.mId) }); 1716 } 1717 } 1718 1719 count = qb.delete(db, where, whereArgs); 1720 break; 1721 1722 default: 1723 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri); 1724 throw new UnsupportedOperationException("Cannot delete URI: " + uri); 1725 } 1726 notifyContentChanged(uri, match); 1727 final long token = Binder.clearCallingIdentity(); 1728 try { 1729 Helpers.getDownloadNotifier(getContext()).update(); 1730 } finally { 1731 Binder.restoreCallingIdentity(token); 1732 } 1733 return count; 1734 } 1735 1736 /** 1737 * Remotely opens a file 1738 */ 1739 @Override openFile(final Uri uri, String mode)1740 public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException { 1741 if (Constants.LOGVV) { 1742 logVerboseOpenFileInfo(uri, mode); 1743 } 1744 1745 // Perform normal query to enforce caller identity access before 1746 // clearing it to reach internal-only columns 1747 final Cursor probeCursor = query(uri, new String[] { 1748 Downloads.Impl._DATA }, null, null, null); 1749 try { 1750 if ((probeCursor == null) || (probeCursor.getCount() == 0)) { 1751 throw new FileNotFoundException( 1752 "No file found for " + uri + " as UID " + Binder.getCallingUid()); 1753 } 1754 } finally { 1755 IoUtils.closeQuietly(probeCursor); 1756 } 1757 1758 final Cursor cursor = queryCleared(uri, new String[] { 1759 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS, 1760 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null, 1761 null, null); 1762 final String path; 1763 final boolean shouldScan; 1764 try { 1765 int count = (cursor != null) ? cursor.getCount() : 0; 1766 if (count != 1) { 1767 // If there is not exactly one result, throw an appropriate exception. 1768 if (count == 0) { 1769 throw new FileNotFoundException("No entry for " + uri); 1770 } 1771 throw new FileNotFoundException("Multiple items at " + uri); 1772 } 1773 1774 if (cursor.moveToFirst()) { 1775 final int status = cursor.getInt(1); 1776 final int destination = cursor.getInt(2); 1777 final int mediaScanned = cursor.getInt(3); 1778 1779 path = cursor.getString(0); 1780 shouldScan = Downloads.Impl.isStatusSuccess(status) && ( 1781 destination == Downloads.Impl.DESTINATION_EXTERNAL 1782 || destination == Downloads.Impl.DESTINATION_FILE_URI 1783 || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 1784 && mediaScanned != Downloads.Impl.MEDIA_NOT_SCANNABLE; 1785 } else { 1786 throw new FileNotFoundException("Failed moveToFirst"); 1787 } 1788 } finally { 1789 IoUtils.closeQuietly(cursor); 1790 } 1791 1792 if (path == null) { 1793 throw new FileNotFoundException("No filename found."); 1794 } 1795 1796 final File file; 1797 try { 1798 file = new File(path).getCanonicalFile(); 1799 } catch (IOException e) { 1800 throw new FileNotFoundException(e.getMessage()); 1801 } 1802 1803 if (!Helpers.isFilenameValid(getContext(), file)) { 1804 throw new FileNotFoundException("Invalid file: " + file); 1805 } 1806 1807 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 1808 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) { 1809 return ParcelFileDescriptor.open(file, pfdMode); 1810 } else { 1811 try { 1812 // When finished writing, update size and timestamp 1813 return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(), 1814 new OnCloseListener() { 1815 @Override 1816 public void onClose(IOException e) { 1817 final ContentValues values = new ContentValues(); 1818 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length()); 1819 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, 1820 mSystemFacade.currentTimeMillis()); 1821 update(uri, values, null, null); 1822 1823 if (shouldScan) { 1824 final Intent intent = new Intent( 1825 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 1826 intent.setData(Uri.fromFile(file)); 1827 getContext().sendBroadcast(intent); 1828 } 1829 } 1830 }); 1831 } catch (IOException e) { 1832 throw new FileNotFoundException("Failed to open for writing: " + e); 1833 } 1834 } 1835 } 1836 1837 @Override 1838 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1839 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120); 1840 1841 pw.println("Downloads updated in last hour:"); 1842 pw.increaseIndent(); 1843 1844 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1845 final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS; 1846 final Cursor cursor = db.query(DB_TABLE, null, 1847 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null, 1848 Downloads.Impl._ID + " ASC"); 1849 try { 1850 final String[] cols = cursor.getColumnNames(); 1851 final int idCol = cursor.getColumnIndex(BaseColumns._ID); 1852 while (cursor.moveToNext()) { 1853 pw.println("Download #" + cursor.getInt(idCol) + ":"); 1854 pw.increaseIndent(); 1855 for (int i = 0; i < cols.length; i++) { 1856 // Omit sensitive data when dumping 1857 if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) { 1858 continue; 1859 } 1860 pw.printPair(cols[i], cursor.getString(i)); 1861 } 1862 pw.println(); 1863 pw.decreaseIndent(); 1864 } 1865 } finally { 1866 cursor.close(); 1867 } 1868 1869 pw.decreaseIndent(); 1870 } 1871 1872 private void logVerboseOpenFileInfo(Uri uri, String mode) { 1873 Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode 1874 + ", uid: " + Binder.getCallingUid()); 1875 Cursor cursor = query(Downloads.Impl.CONTENT_URI, 1876 new String[] { "_id" }, null, null, "_id"); 1877 if (cursor == null) { 1878 Log.v(Constants.TAG, "null cursor in openFile"); 1879 } else { 1880 try { 1881 if (!cursor.moveToFirst()) { 1882 Log.v(Constants.TAG, "empty cursor in openFile"); 1883 } else { 1884 do { 1885 Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available"); 1886 } while(cursor.moveToNext()); 1887 } 1888 } finally { 1889 cursor.close(); 1890 } 1891 } 1892 cursor = query(uri, new String[] { "_data" }, null, null, null); 1893 if (cursor == null) { 1894 Log.v(Constants.TAG, "null cursor in openFile"); 1895 } else { 1896 try { 1897 if (!cursor.moveToFirst()) { 1898 Log.v(Constants.TAG, "empty cursor in openFile"); 1899 } else { 1900 String filename = cursor.getString(0); 1901 Log.v(Constants.TAG, "filename in openFile: " + filename); 1902 if (new java.io.File(filename).isFile()) { 1903 Log.v(Constants.TAG, "file exists in openFile"); 1904 } 1905 } 1906 } finally { 1907 cursor.close(); 1908 } 1909 } 1910 } 1911 1912 private static final void copyInteger(String key, ContentValues from, ContentValues to) { 1913 Integer i = from.getAsInteger(key); 1914 if (i != null) { 1915 to.put(key, i); 1916 } 1917 } 1918 1919 private static final void copyBoolean(String key, ContentValues from, ContentValues to) { 1920 Boolean b = from.getAsBoolean(key); 1921 if (b != null) { 1922 to.put(key, b); 1923 } 1924 } 1925 1926 private static final void copyString(String key, ContentValues from, ContentValues to) { 1927 String s = from.getAsString(key); 1928 if (s != null) { 1929 to.put(key, s); 1930 } 1931 } 1932 1933 private static final void copyStringWithDefault(String key, ContentValues from, 1934 ContentValues to, String defaultValue) { 1935 copyString(key, from, to); 1936 if (!to.containsKey(key)) { 1937 to.put(key, defaultValue); 1938 } 1939 } 1940 1941 private void grantAllDownloadsPermission(String toPackage, long id) { 1942 final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); 1943 getContext().grantUriPermission(toPackage, uri, 1944 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 1945 } 1946 1947 private void revokeAllDownloadsPermission(long id) { 1948 final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); 1949 getContext().revokeUriPermission(uri, ~0); 1950 } 1951 } 1952