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