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