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, R)184 addMapping(map, DownloadManager.COLUMN_LOCAL_URI, 185 "'placeholder'"); addMapping(map, DownloadManager.COLUMN_REASON, R)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, R)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 @Override call(String method, String arg, Bundle extras)652 public Bundle call(String method, String arg, Bundle extras) { 653 switch (method) { 654 case Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED: { 655 getContext().enforceCallingOrSelfPermission( 656 android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG); 657 final long[] deletedDownloadIds = extras.getLongArray(Downloads.EXTRA_IDS); 658 final String[] mimeTypes = extras.getStringArray(Downloads.EXTRA_MIME_TYPES); 659 DownloadStorageProvider.onMediaProviderDownloadsDelete(getContext(), 660 deletedDownloadIds, mimeTypes); 661 return null; 662 } 663 case Downloads.CALL_CREATE_EXTERNAL_PUBLIC_DIR: { 664 final String dirType = extras.getString(Downloads.DIR_TYPE); 665 if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, dirType)) { 666 throw new IllegalStateException("Not one of standard directories: " + dirType); 667 } 668 final File file = Environment.getExternalStoragePublicDirectory(dirType); 669 if (file.exists()) { 670 if (!file.isDirectory()) { 671 throw new IllegalStateException(file.getAbsolutePath() + 672 " already exists and is not a directory"); 673 } 674 } else if (!file.mkdirs()) { 675 throw new IllegalStateException("Unable to create directory: " + 676 file.getAbsolutePath()); 677 } 678 return null; 679 } 680 case Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS : { 681 getContext().enforceCallingOrSelfPermission( 682 android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG); 683 DownloadStorageProvider.revokeAllMediaStoreUriPermissions(getContext()); 684 return null; 685 } 686 default: 687 throw new UnsupportedOperationException("Unsupported call: " + method); 688 } 689 } 690 691 /** 692 * Inserts a row in the database 693 */ 694 @Override insert(final Uri uri, final ContentValues values)695 public Uri insert(final Uri uri, final ContentValues values) { 696 checkInsertPermissions(values); 697 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 698 699 // note we disallow inserting into ALL_DOWNLOADS 700 int match = sURIMatcher.match(uri); 701 if (match != MY_DOWNLOADS) { 702 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri); 703 throw new IllegalArgumentException("Unknown/Invalid URI " + uri); 704 } 705 706 ContentValues filteredValues = new ContentValues(); 707 708 boolean isPublicApi = 709 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE; 710 711 // validate the destination column 712 Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION); 713 if (dest != null) { 714 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED) 715 != PackageManager.PERMISSION_GRANTED 716 && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION 717 || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING)) { 718 throw new SecurityException("setting destination to : " + dest + 719 " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted"); 720 } 721 // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically 722 // switch to non-purgeable download 723 boolean hasNonPurgeablePermission = 724 getContext().checkCallingOrSelfPermission( 725 Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE) 726 == PackageManager.PERMISSION_GRANTED; 727 if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE 728 && hasNonPurgeablePermission) { 729 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION; 730 } 731 if (dest == Downloads.Impl.DESTINATION_FILE_URI) { 732 checkFileUriDestination(values); 733 } else if (dest == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 734 checkDownloadedFilePath(values); 735 } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 736 getContext().enforceCallingOrSelfPermission( 737 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 738 "No permission to write"); 739 740 if (mAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, 741 getCallingPackage(), Binder.getCallingUid(), getCallingAttributionTag(), 742 null) != AppOpsManager.MODE_ALLOWED) { 743 throw new SecurityException("No permission to write"); 744 } 745 } 746 747 filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest); 748 } 749 750 ensureDefaultColumns(values); 751 752 // copy some of the input values as is 753 copyString(Downloads.Impl.COLUMN_URI, values, filteredValues); 754 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); 755 copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues); 756 copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues); 757 copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues); 758 copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues); 759 760 // validate the visibility column 761 Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY); 762 if (vis == null) { 763 if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 764 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, 765 Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); 766 } else { 767 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, 768 Downloads.Impl.VISIBILITY_HIDDEN); 769 } 770 } else { 771 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis); 772 } 773 // copy the control column as is 774 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); 775 776 /* 777 * requests coming from 778 * DownloadManager.addCompletedDownload(String, String, String, 779 * boolean, String, String, long) need special treatment 780 */ 781 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == 782 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 783 // these requests always are marked as 'completed' 784 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS); 785 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, 786 values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES)); 787 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); 788 copyString(Downloads.Impl._DATA, values, filteredValues); 789 copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues); 790 } else { 791 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING); 792 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1); 793 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); 794 } 795 796 // set lastupdate to current time 797 long lastMod = mSystemFacade.currentTimeMillis(); 798 filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod); 799 800 // use packagename of the caller to set the notification columns 801 String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); 802 String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS); 803 if (pckg != null && (clazz != null || isPublicApi)) { 804 int uid = Binder.getCallingUid(); 805 try { 806 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) { 807 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg); 808 if (clazz != null) { 809 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz); 810 } 811 } 812 } catch (PackageManager.NameNotFoundException ex) { 813 /* ignored for now */ 814 } 815 } 816 817 // copy some more columns as is 818 copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues); 819 copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues); 820 copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues); 821 copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues); 822 823 // UID, PID columns 824 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED) 825 == PackageManager.PERMISSION_GRANTED) { 826 copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues); 827 } 828 filteredValues.put(Constants.UID, Binder.getCallingUid()); 829 if (Binder.getCallingUid() == 0) { 830 copyInteger(Constants.UID, values, filteredValues); 831 } 832 833 // copy some more columns as is 834 copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, ""); 835 copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, ""); 836 837 // is_visible_in_downloads_ui column 838 copyBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues); 839 840 // public api requests and networktypes/roaming columns 841 if (isPublicApi) { 842 copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues); 843 copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues); 844 copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues); 845 copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues); 846 } 847 848 final Integer mediaScanned = values.getAsInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED); 849 filteredValues.put(COLUMN_MEDIA_SCANNED, 850 mediaScanned == null ? MEDIA_NOT_SCANNED : mediaScanned); 851 852 final boolean shouldBeVisibleToUser 853 = filteredValues.getAsBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI) 854 || filteredValues.getAsInteger(COLUMN_MEDIA_SCANNED) == MEDIA_NOT_SCANNED; 855 if (shouldBeVisibleToUser && filteredValues.getAsInteger(COLUMN_DESTINATION) 856 == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 857 final CallingIdentity token = clearCallingIdentity(); 858 try { 859 final Uri mediaStoreUri = MediaStore.scanFile(getContext().getContentResolver(), 860 new File(filteredValues.getAsString(Downloads.Impl._DATA))); 861 if (mediaStoreUri != null) { 862 final ContentValues mediaValues = new ContentValues(); 863 mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, 864 filteredValues.getAsString(Downloads.Impl.COLUMN_URI)); 865 mediaValues.put(MediaStore.Downloads.REFERER_URI, 866 filteredValues.getAsString(Downloads.Impl.COLUMN_REFERER)); 867 mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME, 868 Helpers.getPackageForUid(getContext(), 869 filteredValues.getAsInteger(Constants.UID))); 870 getContext().getContentResolver().update( 871 convertToMediaStoreDownloadsUri(mediaStoreUri), 872 mediaValues, null, null); 873 874 filteredValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI, 875 mediaStoreUri.toString()); 876 filteredValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, 877 mediaStoreUri.toString()); 878 filteredValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED); 879 } 880 } finally { 881 restoreCallingIdentity(token); 882 } 883 } 884 885 if (Constants.LOGVV) { 886 Log.v(Constants.TAG, "initiating download with UID " 887 + filteredValues.getAsInteger(Constants.UID)); 888 if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) { 889 Log.v(Constants.TAG, "other UID " + 890 filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID)); 891 } 892 } 893 894 long rowID = db.insert(DB_TABLE, null, filteredValues); 895 if (rowID == -1) { 896 Log.d(Constants.TAG, "couldn't insert into downloads database"); 897 return null; 898 } 899 900 insertRequestHeaders(db, rowID, values); 901 902 final String callingPackage = Helpers.getPackageForUid(getContext(), 903 Binder.getCallingUid()); 904 if (callingPackage == null) { 905 Log.e(Constants.TAG, "Package does not exist for calling uid"); 906 return null; 907 } 908 grantAllDownloadsPermission(callingPackage, rowID); 909 notifyContentChanged(uri, match); 910 911 final long token = Binder.clearCallingIdentity(); 912 try { 913 Helpers.scheduleJob(getContext(), rowID); 914 } finally { 915 Binder.restoreCallingIdentity(token); 916 } 917 918 return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID); 919 } 920 921 /** 922 * If an entry corresponding to given mediaValues doesn't already exist in MediaProvider, 923 * add it, otherwise update that entry with the given values. 924 */ updateMediaProvider(@onNull ContentProviderClient mediaProvider, @NonNull ContentValues mediaValues)925 Uri updateMediaProvider(@NonNull ContentProviderClient mediaProvider, 926 @NonNull ContentValues mediaValues) { 927 final String filePath = mediaValues.getAsString(MediaStore.DownloadColumns.DATA); 928 Uri mediaStoreUri = getMediaStoreUri(mediaProvider, filePath); 929 930 try { 931 if (mediaStoreUri == null) { 932 mediaStoreUri = mediaProvider.insert( 933 Helpers.getContentUriForPath(getContext(), filePath), 934 mediaValues); 935 if (mediaStoreUri == null) { 936 Log.e(Constants.TAG, "Error inserting into mediaProvider: " + mediaValues); 937 } 938 return mediaStoreUri; 939 } else { 940 if (mediaProvider.update(mediaStoreUri, mediaValues, null, null) != 1) { 941 Log.e(Constants.TAG, "Error updating MediaProvider, uri: " + mediaStoreUri 942 + ", values: " + mediaValues); 943 } 944 return mediaStoreUri; 945 } 946 } catch (IllegalArgumentException ignored) { 947 // Insert or update MediaStore failed. At this point we can't do 948 // much here. If the file belongs to MediaStore collection, it will 949 // get added to MediaStore collection during next scan, and we will 950 // obtain the uri to the file in the next MediaStore#scanFile 951 // initiated by us 952 Log.w(Constants.TAG, "Couldn't update MediaStore for " + filePath, ignored); 953 } catch (RemoteException e) { 954 // Should not happen 955 } 956 return null; 957 } 958 getMediaStoreUri(@onNull ContentProviderClient mediaProvider, @NonNull String filePath)959 private Uri getMediaStoreUri(@NonNull ContentProviderClient mediaProvider, 960 @NonNull String filePath) { 961 final Uri filesUri = MediaStore.setIncludePending( 962 Helpers.getContentUriForPath(getContext(), filePath)); 963 try (Cursor cursor = mediaProvider.query(filesUri, 964 new String[] { MediaStore.Files.FileColumns._ID }, 965 MediaStore.Files.FileColumns.DATA + "=?", new String[] { filePath }, null, null)) { 966 if (cursor.moveToNext()) { 967 return ContentUris.withAppendedId(filesUri, cursor.getLong(0)); 968 } 969 } catch (RemoteException e) { 970 // Should not happen 971 } 972 return null; 973 } 974 convertToMediaProviderValues(DownloadInfo info)975 ContentValues convertToMediaProviderValues(DownloadInfo info) { 976 final String filePath; 977 try { 978 filePath = new File(info.mFileName).getCanonicalPath(); 979 } catch (IOException e) { 980 throw new IllegalArgumentException(e); 981 } 982 final boolean downloadCompleted = Downloads.Impl.isStatusCompleted(info.mStatus); 983 final ContentValues mediaValues = new ContentValues(); 984 mediaValues.put(MediaStore.Downloads.DATA, filePath); 985 mediaValues.put(MediaStore.Downloads.VOLUME_NAME, Helpers.extractVolumeName(filePath)); 986 mediaValues.put(MediaStore.Downloads.RELATIVE_PATH, Helpers.extractRelativePath(filePath)); 987 mediaValues.put(MediaStore.Downloads.DISPLAY_NAME, Helpers.extractDisplayName(filePath)); 988 mediaValues.put(MediaStore.Downloads.SIZE, 989 downloadCompleted ? info.mTotalBytes : info.mCurrentBytes); 990 mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri); 991 mediaValues.put(MediaStore.Downloads.REFERER_URI, info.mReferer); 992 mediaValues.put(MediaStore.Downloads.MIME_TYPE, info.mMimeType); 993 mediaValues.put(MediaStore.Downloads.IS_PENDING, downloadCompleted ? 0 : 1); 994 mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME, 995 Helpers.getPackageForUid(getContext(), info.mUid)); 996 return mediaValues; 997 } 998 getFileUri(String uriString)999 private static Uri getFileUri(String uriString) { 1000 final Uri uri = Uri.parse(uriString); 1001 return TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE) ? uri : null; 1002 } 1003 ensureDefaultColumns(ContentValues values)1004 private void ensureDefaultColumns(ContentValues values) { 1005 final Integer dest = values.getAsInteger(COLUMN_DESTINATION); 1006 if (dest != null) { 1007 final int mediaScannable; 1008 final boolean visibleInDownloadsUi; 1009 if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 1010 mediaScannable = MEDIA_NOT_SCANNED; 1011 visibleInDownloadsUi = true; 1012 } else if (dest != DESTINATION_FILE_URI 1013 && dest != DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 1014 mediaScannable = MEDIA_NOT_SCANNABLE; 1015 visibleInDownloadsUi = false; 1016 } else { 1017 final File file; 1018 if (dest == Downloads.Impl.DESTINATION_FILE_URI) { 1019 final String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 1020 file = new File(getFileUri(fileUri).getPath()); 1021 } else { 1022 file = new File(values.getAsString(Downloads.Impl._DATA)); 1023 } 1024 1025 if (Helpers.isFileInExternalAndroidDirs(file.getAbsolutePath())) { 1026 mediaScannable = MEDIA_NOT_SCANNABLE; 1027 visibleInDownloadsUi = false; 1028 } else if (Helpers.isFilenameValidInPublicDownloadsDir(file)) { 1029 mediaScannable = MEDIA_NOT_SCANNED; 1030 visibleInDownloadsUi = true; 1031 } else { 1032 mediaScannable = MEDIA_NOT_SCANNED; 1033 visibleInDownloadsUi = false; 1034 } 1035 } 1036 values.put(COLUMN_MEDIA_SCANNED, mediaScannable); 1037 values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, visibleInDownloadsUi); 1038 } else { 1039 if (!values.containsKey(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) { 1040 values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, true); 1041 } 1042 } 1043 } 1044 1045 /** 1046 * Check that the file URI provided for DESTINATION_FILE_URI is valid. 1047 */ checkFileUriDestination(ContentValues values)1048 private void checkFileUriDestination(ContentValues values) { 1049 String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 1050 if (fileUri == null) { 1051 throw new IllegalArgumentException( 1052 "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT"); 1053 } 1054 final Uri uri = getFileUri(fileUri); 1055 if (uri == null) { 1056 throw new IllegalArgumentException("Not a file URI: " + uri); 1057 } 1058 final String path = uri.getPath(); 1059 if (path == null || ("/" + path + "/").contains("/../")) { 1060 throw new IllegalArgumentException("Invalid file URI: " + uri); 1061 } 1062 1063 final File file; 1064 try { 1065 file = new File(path).getCanonicalFile(); 1066 values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, Uri.fromFile(file).toString()); 1067 } catch (IOException e) { 1068 throw new SecurityException(e); 1069 } 1070 1071 final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE, 1072 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED; 1073 Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(), 1074 mAppOpsManager, getCallingAttributionTag(), isLegacyMode, 1075 /* allowDownloadsDirOnly */ false); 1076 } 1077 checkDownloadedFilePath(ContentValues values)1078 private void checkDownloadedFilePath(ContentValues values) { 1079 final String path = values.getAsString(Downloads.Impl._DATA); 1080 if (path == null || ("/" + path + "/").contains("/../")) { 1081 throw new IllegalArgumentException("Invalid file path: " 1082 + (path == null ? "null" : path)); 1083 } 1084 1085 final File file; 1086 try { 1087 file = new File(path).getCanonicalFile(); 1088 values.put(Downloads.Impl._DATA, file.getPath()); 1089 } catch (IOException e) { 1090 throw new SecurityException(e); 1091 } 1092 1093 if (!file.exists()) { 1094 throw new IllegalArgumentException("File doesn't exist: " + file); 1095 } 1096 1097 if (Binder.getCallingPid() == Process.myPid()) { 1098 return; 1099 } 1100 1101 final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE, 1102 Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED; 1103 Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(), 1104 mAppOpsManager, getCallingAttributionTag(), isLegacyMode, 1105 /* allowDownloadsDirOnly */ true); 1106 } 1107 1108 /** 1109 * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to 1110 * constraints in the rest of the code. Apps without that may still access this provider through 1111 * the public API, but additional restrictions are imposed. We check those restrictions here. 1112 * 1113 * @param values ContentValues provided to insert() 1114 * @throws SecurityException if the caller has insufficient permissions 1115 */ checkInsertPermissions(ContentValues values)1116 private void checkInsertPermissions(ContentValues values) { 1117 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS) 1118 == PackageManager.PERMISSION_GRANTED) { 1119 return; 1120 } 1121 1122 getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, 1123 "INTERNET permission is required to use the download manager"); 1124 1125 // ensure the request fits within the bounds of a public API request 1126 // first copy so we can remove values 1127 values = new ContentValues(values); 1128 1129 // check columns whose values are restricted 1130 enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE); 1131 1132 // validate the destination column 1133 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == 1134 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 1135 /* this row is inserted by 1136 * DownloadManager.addCompletedDownload(String, String, String, 1137 * boolean, String, String, long) 1138 */ 1139 values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES); 1140 values.remove(Downloads.Impl._DATA); 1141 values.remove(Downloads.Impl.COLUMN_STATUS); 1142 } 1143 enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION, 1144 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE, 1145 Downloads.Impl.DESTINATION_FILE_URI, 1146 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD); 1147 1148 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION) 1149 == PackageManager.PERMISSION_GRANTED) { 1150 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 1151 Request.VISIBILITY_HIDDEN, 1152 Request.VISIBILITY_VISIBLE, 1153 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 1154 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 1155 } else { 1156 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 1157 Request.VISIBILITY_VISIBLE, 1158 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 1159 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 1160 } 1161 1162 // remove the rest of the columns that are allowed (with any value) 1163 values.remove(Downloads.Impl.COLUMN_URI); 1164 values.remove(Downloads.Impl.COLUMN_TITLE); 1165 values.remove(Downloads.Impl.COLUMN_DESCRIPTION); 1166 values.remove(Downloads.Impl.COLUMN_MIME_TYPE); 1167 values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert() 1168 values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert() 1169 values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); 1170 values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING); 1171 values.remove(Downloads.Impl.COLUMN_ALLOW_METERED); 1172 values.remove(Downloads.Impl.COLUMN_FLAGS); 1173 values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI); 1174 values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED); 1175 values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE); 1176 Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator(); 1177 while (iterator.hasNext()) { 1178 String key = iterator.next().getKey(); 1179 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 1180 iterator.remove(); 1181 } 1182 } 1183 1184 // any extra columns are extraneous and disallowed 1185 if (values.size() > 0) { 1186 StringBuilder error = new StringBuilder("Invalid columns in request: "); 1187 boolean first = true; 1188 for (Map.Entry<String, Object> entry : values.valueSet()) { 1189 if (!first) { 1190 error.append(", "); 1191 } 1192 error.append(entry.getKey()); 1193 first = false; 1194 } 1195 throw new SecurityException(error.toString()); 1196 } 1197 } 1198 1199 /** 1200 * Remove column from values, and throw a SecurityException if the value isn't within the 1201 * specified allowedValues. 1202 */ enforceAllowedValues(ContentValues values, String column, Object... allowedValues)1203 private void enforceAllowedValues(ContentValues values, String column, 1204 Object... allowedValues) { 1205 Object value = values.get(column); 1206 values.remove(column); 1207 for (Object allowedValue : allowedValues) { 1208 if (value == null && allowedValue == null) { 1209 return; 1210 } 1211 if (value != null && value.equals(allowedValue)) { 1212 return; 1213 } 1214 } 1215 throw new SecurityException("Invalid value for " + column + ": " + value); 1216 } 1217 queryCleared(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort)1218 private Cursor queryCleared(Uri uri, String[] projection, String selection, 1219 String[] selectionArgs, String sort) { 1220 final long token = Binder.clearCallingIdentity(); 1221 try { 1222 return query(uri, projection, selection, selectionArgs, sort); 1223 } finally { 1224 Binder.restoreCallingIdentity(token); 1225 } 1226 } 1227 1228 /** 1229 * Starts a database query 1230 */ 1231 @Override query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort)1232 public Cursor query(final Uri uri, String[] projection, 1233 final String selection, final String[] selectionArgs, 1234 final String sort) { 1235 1236 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1237 1238 int match = sURIMatcher.match(uri); 1239 if (match == -1) { 1240 if (Constants.LOGV) { 1241 Log.v(Constants.TAG, "querying unknown URI: " + uri); 1242 } 1243 throw new IllegalArgumentException("Unknown URI: " + uri); 1244 } 1245 1246 if (match == MY_DOWNLOADS_ID_HEADERS || match == ALL_DOWNLOADS_ID_HEADERS) { 1247 if (projection != null || selection != null || sort != null) { 1248 throw new UnsupportedOperationException("Request header queries do not support " 1249 + "projections, selections or sorting"); 1250 } 1251 1252 // Headers are only available to callers with full access. 1253 getContext().enforceCallingOrSelfPermission( 1254 Downloads.Impl.PERMISSION_ACCESS_ALL, Constants.TAG); 1255 1256 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1257 projection = new String[] { 1258 Downloads.Impl.RequestHeaders.COLUMN_HEADER, 1259 Downloads.Impl.RequestHeaders.COLUMN_VALUE 1260 }; 1261 return qb.query(db, projection, null, null, null, null, null); 1262 } 1263 1264 if (Constants.LOGVV) { 1265 logVerboseQueryInfo(projection, selection, selectionArgs, sort, db); 1266 } 1267 1268 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1269 1270 final Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sort); 1271 1272 if (ret != null) { 1273 ret.setNotificationUri(getContext().getContentResolver(), uri); 1274 if (Constants.LOGVV) { 1275 Log.v(Constants.TAG, 1276 "created cursor " + ret + " on behalf of " + Binder.getCallingPid()); 1277 } 1278 } else { 1279 if (Constants.LOGV) { 1280 Log.v(Constants.TAG, "query failed in downloads database"); 1281 } 1282 } 1283 1284 return ret; 1285 } 1286 logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db)1287 private void logVerboseQueryInfo(String[] projection, final String selection, 1288 final String[] selectionArgs, final String sort, SQLiteDatabase db) { 1289 java.lang.StringBuilder sb = new java.lang.StringBuilder(); 1290 sb.append("starting query, database is "); 1291 if (db != null) { 1292 sb.append("not "); 1293 } 1294 sb.append("null; "); 1295 if (projection == null) { 1296 sb.append("projection is null; "); 1297 } else if (projection.length == 0) { 1298 sb.append("projection is empty; "); 1299 } else { 1300 for (int i = 0; i < projection.length; ++i) { 1301 sb.append("projection["); 1302 sb.append(i); 1303 sb.append("] is "); 1304 sb.append(projection[i]); 1305 sb.append("; "); 1306 } 1307 } 1308 sb.append("selection is "); 1309 sb.append(selection); 1310 sb.append("; "); 1311 if (selectionArgs == null) { 1312 sb.append("selectionArgs is null; "); 1313 } else if (selectionArgs.length == 0) { 1314 sb.append("selectionArgs is empty; "); 1315 } else { 1316 for (int i = 0; i < selectionArgs.length; ++i) { 1317 sb.append("selectionArgs["); 1318 sb.append(i); 1319 sb.append("] is "); 1320 sb.append(selectionArgs[i]); 1321 sb.append("; "); 1322 } 1323 } 1324 sb.append("sort is "); 1325 sb.append(sort); 1326 sb.append("."); 1327 Log.v(Constants.TAG, sb.toString()); 1328 } 1329 getDownloadIdFromUri(final Uri uri)1330 private String getDownloadIdFromUri(final Uri uri) { 1331 return uri.getPathSegments().get(1); 1332 } 1333 1334 /** 1335 * Insert request headers for a download into the DB. 1336 */ insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values)1337 private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) { 1338 ContentValues rowValues = new ContentValues(); 1339 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId); 1340 for (Map.Entry<String, Object> entry : values.valueSet()) { 1341 String key = entry.getKey(); 1342 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 1343 String headerLine = entry.getValue().toString(); 1344 if (!headerLine.contains(":")) { 1345 throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine); 1346 } 1347 String[] parts = headerLine.split(":", 2); 1348 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim()); 1349 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim()); 1350 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues); 1351 } 1352 } 1353 } 1354 1355 /** 1356 * Updates a row in the database 1357 */ 1358 @Override update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)1359 public int update(final Uri uri, final ContentValues values, 1360 final String where, final String[] whereArgs) { 1361 final Context context = getContext(); 1362 final ContentResolver resolver = context.getContentResolver(); 1363 1364 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1365 1366 int count; 1367 boolean updateSchedule = false; 1368 boolean isCompleting = false; 1369 1370 ContentValues filteredValues; 1371 if (Binder.getCallingPid() != Process.myPid()) { 1372 filteredValues = new ContentValues(); 1373 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); 1374 copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues); 1375 Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL); 1376 if (i != null) { 1377 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i); 1378 updateSchedule = true; 1379 } 1380 1381 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); 1382 copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues); 1383 copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues); 1384 copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues); 1385 copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues); 1386 } else { 1387 filteredValues = values; 1388 String filename = values.getAsString(Downloads.Impl._DATA); 1389 if (filename != null) { 1390 try { 1391 filteredValues.put(Downloads.Impl._DATA, new File(filename).getCanonicalPath()); 1392 } catch (IOException e) { 1393 throw new IllegalStateException("Invalid path: " + filename); 1394 } 1395 1396 Cursor c = null; 1397 try { 1398 c = query(uri, new String[] 1399 { Downloads.Impl.COLUMN_TITLE }, null, null, null); 1400 if (!c.moveToFirst() || c.getString(0).isEmpty()) { 1401 values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName()); 1402 } 1403 } finally { 1404 IoUtils.closeQuietly(c); 1405 } 1406 } 1407 1408 Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS); 1409 boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING; 1410 boolean isUserBypassingSizeLimit = 1411 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); 1412 if (isRestart || isUserBypassingSizeLimit) { 1413 updateSchedule = true; 1414 } 1415 isCompleting = status != null && Downloads.Impl.isStatusCompleted(status); 1416 } 1417 1418 int match = sURIMatcher.match(uri); 1419 switch (match) { 1420 case MY_DOWNLOADS: 1421 case MY_DOWNLOADS_ID: 1422 case ALL_DOWNLOADS: 1423 case ALL_DOWNLOADS_ID: 1424 if (filteredValues.size() == 0) { 1425 count = 0; 1426 break; 1427 } 1428 1429 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1430 count = qb.update(db, filteredValues, where, whereArgs); 1431 final CallingIdentity token = clearCallingIdentity(); 1432 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null); 1433 ContentProviderClient client = getContext().getContentResolver() 1434 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 1435 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, 1436 cursor); 1437 final DownloadInfo info = new DownloadInfo(context); 1438 final ContentValues updateValues = new ContentValues(); 1439 while (cursor.moveToNext()) { 1440 reader.updateFromDatabase(info); 1441 final boolean visibleToUser = info.mIsVisibleInDownloadsUi 1442 || (info.mMediaScanned != MEDIA_NOT_SCANNABLE); 1443 if (info.mFileName == null) { 1444 if (info.mMediaStoreUri != null) { 1445 // If there was a mediastore entry, it would be deleted in it's 1446 // next idle pass. 1447 updateValues.clear(); 1448 updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI); 1449 qb.update(db, updateValues, Downloads.Impl._ID + "=?", 1450 new String[] { Long.toString(info.mId) }); 1451 } 1452 } else if ((info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL 1453 || info.mDestination == Downloads.Impl.DESTINATION_FILE_URI 1454 || info.mDestination == Downloads.Impl 1455 .DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 1456 && visibleToUser) { 1457 final ContentValues mediaValues = convertToMediaProviderValues(info); 1458 final Uri mediaStoreUri; 1459 if (Downloads.Impl.isStatusCompleted(info.mStatus)) { 1460 // Set size to 0 to ensure MediaScanner will scan this file. 1461 mediaValues.put(MediaStore.Downloads.SIZE, 0); 1462 updateMediaProvider(client, mediaValues); 1463 mediaStoreUri = triggerMediaScan(client, new File(info.mFileName)); 1464 } else { 1465 // Don't insert/update MediaStore db until the download is complete. 1466 // Incomplete files can only be inserted to MediaStore by setting 1467 // IS_PENDING=1 and using RELATIVE_PATH and DISPLAY_NAME in 1468 // MediaProvider#insert operation. We use DATA column, IS_PENDING 1469 // with DATA column will not be respected by MediaProvider. 1470 mediaStoreUri = null; 1471 } 1472 if (!TextUtils.equals(info.mMediaStoreUri, 1473 mediaStoreUri == null ? null : mediaStoreUri.toString())) { 1474 updateValues.clear(); 1475 if (mediaStoreUri == null) { 1476 updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI); 1477 updateValues.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI); 1478 updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_NOT_SCANNED); 1479 } else { 1480 updateValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI, 1481 mediaStoreUri.toString()); 1482 updateValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, 1483 mediaStoreUri.toString()); 1484 updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED); 1485 } 1486 qb.update(db, updateValues, Downloads.Impl._ID + "=?", 1487 new String[] { Long.toString(info.mId) }); 1488 } 1489 } 1490 if (updateSchedule) { 1491 Helpers.scheduleJob(context, info); 1492 } 1493 if (isCompleting) { 1494 info.sendIntentIfRequested(); 1495 } 1496 } 1497 } finally { 1498 restoreCallingIdentity(token); 1499 } 1500 break; 1501 1502 default: 1503 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri); 1504 throw new UnsupportedOperationException("Cannot update URI: " + uri); 1505 } 1506 1507 notifyContentChanged(uri, match); 1508 return count; 1509 } 1510 1511 /** 1512 * Notify of a change through both URIs (/my_downloads and /all_downloads) 1513 * @param uri either URI for the changed download(s) 1514 * @param uriMatch the match ID from {@link #sURIMatcher} 1515 */ notifyContentChanged(final Uri uri, int uriMatch)1516 private void notifyContentChanged(final Uri uri, int uriMatch) { 1517 Long downloadId = null; 1518 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) { 1519 downloadId = Long.parseLong(getDownloadIdFromUri(uri)); 1520 } 1521 for (Uri uriToNotify : BASE_URIS) { 1522 if (downloadId != null) { 1523 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId); 1524 } 1525 getContext().getContentResolver().notifyChange(uriToNotify, null); 1526 } 1527 } 1528 1529 /** 1530 * Create a query builder that filters access to the underlying database 1531 * based on both the requested {@link Uri} and permissions of the caller. 1532 */ getQueryBuilder(final Uri uri, int match)1533 private SQLiteQueryBuilder getQueryBuilder(final Uri uri, int match) { 1534 final String table; 1535 final Map<String, String> projectionMap; 1536 1537 final StringBuilder where = new StringBuilder(); 1538 switch (match) { 1539 // The "my_downloads" view normally limits the caller to operating 1540 // on downloads that they either directly own, or have been given 1541 // indirect ownership of via OTHER_UID. 1542 case MY_DOWNLOADS_ID: 1543 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri)); 1544 // fall-through 1545 case MY_DOWNLOADS: 1546 table = DB_TABLE; 1547 projectionMap = sDownloadsMap; 1548 if (getContext().checkCallingOrSelfPermission( 1549 PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) { 1550 appendWhereExpression(where, Constants.UID + "=" + Binder.getCallingUid() 1551 + " OR " + COLUMN_OTHER_UID + "=" + Binder.getCallingUid()); 1552 } 1553 break; 1554 1555 // The "all_downloads" view is already limited via <path-permission> 1556 // to only callers holding the ACCESS_ALL_DOWNLOADS permission, but 1557 // access may also be delegated via Uri permission grants. 1558 case ALL_DOWNLOADS_ID: 1559 appendWhereExpression(where, _ID + "=" + getDownloadIdFromUri(uri)); 1560 // fall-through 1561 case ALL_DOWNLOADS: 1562 table = DB_TABLE; 1563 projectionMap = sDownloadsMap; 1564 break; 1565 1566 // Headers are limited to callers holding the ACCESS_ALL_DOWNLOADS 1567 // permission, since they're only needed for executing downloads. 1568 case MY_DOWNLOADS_ID_HEADERS: 1569 case ALL_DOWNLOADS_ID_HEADERS: 1570 table = Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE; 1571 projectionMap = sHeadersMap; 1572 appendWhereExpression(where, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" 1573 + getDownloadIdFromUri(uri)); 1574 break; 1575 1576 default: 1577 throw new UnsupportedOperationException("Unknown URI: " + uri); 1578 } 1579 1580 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1581 qb.setTables(table); 1582 qb.setProjectionMap(projectionMap); 1583 qb.setStrict(true); 1584 qb.setStrictColumns(true); 1585 qb.setStrictGrammar(true); 1586 qb.appendWhere(where); 1587 return qb; 1588 } 1589 appendWhereExpression(StringBuilder sb, String expression)1590 private static void appendWhereExpression(StringBuilder sb, String expression) { 1591 if (sb.length() > 0) { 1592 sb.append(" AND "); 1593 } 1594 sb.append('(').append(expression).append(')'); 1595 } 1596 1597 /** 1598 * Deletes a row in the database 1599 */ 1600 @Override delete(final Uri uri, final String where, final String[] whereArgs)1601 public int delete(final Uri uri, final String where, final String[] whereArgs) { 1602 final Context context = getContext(); 1603 final ContentResolver resolver = context.getContentResolver(); 1604 final JobScheduler scheduler = context.getSystemService(JobScheduler.class); 1605 1606 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1607 int count; 1608 int match = sURIMatcher.match(uri); 1609 switch (match) { 1610 case MY_DOWNLOADS: 1611 case MY_DOWNLOADS_ID: 1612 case ALL_DOWNLOADS: 1613 case ALL_DOWNLOADS_ID: 1614 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); 1615 try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null)) { 1616 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); 1617 final DownloadInfo info = new DownloadInfo(context); 1618 while (cursor.moveToNext()) { 1619 reader.updateFromDatabase(info); 1620 scheduler.cancel((int) info.mId); 1621 1622 revokeAllDownloadsPermission(info.mId); 1623 DownloadStorageProvider.onDownloadProviderDelete(getContext(), info.mId); 1624 1625 final String path = info.mFileName; 1626 if (!TextUtils.isEmpty(path)) { 1627 try { 1628 final File file = new File(path).getCanonicalFile(); 1629 if (Helpers.isFilenameValid(getContext(), file)) { 1630 Log.v(Constants.TAG, 1631 "Deleting " + file + " via provider delete"); 1632 file.delete(); 1633 MediaStore.scanFile(getContext().getContentResolver(), file); 1634 } else { 1635 Log.d(Constants.TAG, "Ignoring invalid file: " + file); 1636 } 1637 } catch (IOException e) { 1638 Log.e(Constants.TAG, "Couldn't delete file: " + path, e); 1639 } 1640 } 1641 1642 // If the download wasn't completed yet, we're 1643 // effectively completing it now, and we need to send 1644 // any requested broadcasts 1645 if (!Downloads.Impl.isStatusCompleted(info.mStatus)) { 1646 info.sendIntentIfRequested(); 1647 } 1648 1649 // Delete any headers for this download 1650 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, 1651 Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=?", 1652 new String[] { Long.toString(info.mId) }); 1653 } 1654 } 1655 1656 count = qb.delete(db, where, whereArgs); 1657 break; 1658 1659 default: 1660 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri); 1661 throw new UnsupportedOperationException("Cannot delete URI: " + uri); 1662 } 1663 notifyContentChanged(uri, match); 1664 final long token = Binder.clearCallingIdentity(); 1665 try { 1666 Helpers.getDownloadNotifier(getContext()).update(); 1667 } finally { 1668 Binder.restoreCallingIdentity(token); 1669 } 1670 return count; 1671 } 1672 1673 /** 1674 * Remotely opens a file 1675 */ 1676 @Override openFile(final Uri uri, String mode)1677 public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException { 1678 if (Constants.LOGVV) { 1679 logVerboseOpenFileInfo(uri, mode); 1680 } 1681 1682 // Perform normal query to enforce caller identity access before 1683 // clearing it to reach internal-only columns 1684 final Cursor probeCursor = query(uri, new String[] { 1685 Downloads.Impl._DATA }, null, null, null); 1686 try { 1687 if ((probeCursor == null) || (probeCursor.getCount() == 0)) { 1688 throw new FileNotFoundException( 1689 "No file found for " + uri + " as UID " + Binder.getCallingUid()); 1690 } 1691 } finally { 1692 IoUtils.closeQuietly(probeCursor); 1693 } 1694 1695 final Cursor cursor = queryCleared(uri, new String[] { 1696 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS, 1697 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null, 1698 null, null); 1699 final String path; 1700 final boolean shouldScan; 1701 try { 1702 int count = (cursor != null) ? cursor.getCount() : 0; 1703 if (count != 1) { 1704 // If there is not exactly one result, throw an appropriate exception. 1705 if (count == 0) { 1706 throw new FileNotFoundException("No entry for " + uri); 1707 } 1708 throw new FileNotFoundException("Multiple items at " + uri); 1709 } 1710 1711 if (cursor.moveToFirst()) { 1712 final int status = cursor.getInt(1); 1713 final int destination = cursor.getInt(2); 1714 final int mediaScanned = cursor.getInt(3); 1715 1716 path = cursor.getString(0); 1717 shouldScan = Downloads.Impl.isStatusSuccess(status) && ( 1718 destination == Downloads.Impl.DESTINATION_EXTERNAL 1719 || destination == Downloads.Impl.DESTINATION_FILE_URI 1720 || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 1721 && mediaScanned != Downloads.Impl.MEDIA_NOT_SCANNABLE; 1722 } else { 1723 throw new FileNotFoundException("Failed moveToFirst"); 1724 } 1725 } finally { 1726 IoUtils.closeQuietly(cursor); 1727 } 1728 1729 if (path == null) { 1730 throw new FileNotFoundException("No filename found."); 1731 } 1732 1733 final File file; 1734 try { 1735 file = new File(path).getCanonicalFile(); 1736 } catch (IOException e) { 1737 throw new FileNotFoundException(e.getMessage()); 1738 } 1739 1740 if (!Helpers.isFilenameValid(getContext(), file)) { 1741 throw new FileNotFoundException("Invalid file: " + file); 1742 } 1743 1744 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 1745 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) { 1746 return ParcelFileDescriptor.open(file, pfdMode); 1747 } else { 1748 try { 1749 // When finished writing, update size and timestamp 1750 return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(), 1751 new OnCloseListener() { 1752 @Override 1753 public void onClose(IOException e) { 1754 final ContentValues values = new ContentValues(); 1755 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length()); 1756 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, 1757 mSystemFacade.currentTimeMillis()); 1758 update(uri, values, null, null); 1759 1760 if (shouldScan) { 1761 final Intent intent = new Intent( 1762 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 1763 intent.setData(Uri.fromFile(file)); 1764 getContext().sendBroadcast(intent); 1765 } 1766 } 1767 }); 1768 } catch (IOException e) { 1769 throw new FileNotFoundException("Failed to open for writing: " + e); 1770 } 1771 } 1772 } 1773 1774 @Override 1775 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1776 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120); 1777 1778 pw.println("Downloads updated in last hour:"); 1779 pw.increaseIndent(); 1780 1781 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1782 final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS; 1783 final Cursor cursor = db.query(DB_TABLE, null, 1784 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null, 1785 Downloads.Impl._ID + " ASC"); 1786 try { 1787 final String[] cols = cursor.getColumnNames(); 1788 final int idCol = cursor.getColumnIndex(BaseColumns._ID); 1789 while (cursor.moveToNext()) { 1790 pw.println("Download #" + cursor.getInt(idCol) + ":"); 1791 pw.increaseIndent(); 1792 for (int i = 0; i < cols.length; i++) { 1793 // Omit sensitive data when dumping 1794 if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) { 1795 continue; 1796 } 1797 pw.printPair(cols[i], cursor.getString(i)); 1798 } 1799 pw.println(); 1800 pw.decreaseIndent(); 1801 } 1802 } finally { 1803 cursor.close(); 1804 } 1805 1806 pw.decreaseIndent(); 1807 } 1808 1809 private void logVerboseOpenFileInfo(Uri uri, String mode) { 1810 Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode 1811 + ", uid: " + Binder.getCallingUid()); 1812 Cursor cursor = query(Downloads.Impl.CONTENT_URI, 1813 new String[] { "_id" }, null, null, "_id"); 1814 if (cursor == null) { 1815 Log.v(Constants.TAG, "null cursor in openFile"); 1816 } else { 1817 try { 1818 if (!cursor.moveToFirst()) { 1819 Log.v(Constants.TAG, "empty cursor in openFile"); 1820 } else { 1821 do { 1822 Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available"); 1823 } while(cursor.moveToNext()); 1824 } 1825 } finally { 1826 cursor.close(); 1827 } 1828 } 1829 cursor = query(uri, new String[] { "_data" }, null, null, null); 1830 if (cursor == null) { 1831 Log.v(Constants.TAG, "null cursor in openFile"); 1832 } else { 1833 try { 1834 if (!cursor.moveToFirst()) { 1835 Log.v(Constants.TAG, "empty cursor in openFile"); 1836 } else { 1837 String filename = cursor.getString(0); 1838 Log.v(Constants.TAG, "filename in openFile: " + filename); 1839 if (new java.io.File(filename).isFile()) { 1840 Log.v(Constants.TAG, "file exists in openFile"); 1841 } 1842 } 1843 } finally { 1844 cursor.close(); 1845 } 1846 } 1847 } 1848 1849 private static final void copyInteger(String key, ContentValues from, ContentValues to) { 1850 Integer i = from.getAsInteger(key); 1851 if (i != null) { 1852 to.put(key, i); 1853 } 1854 } 1855 1856 private static final void copyBoolean(String key, ContentValues from, ContentValues to) { 1857 Boolean b = from.getAsBoolean(key); 1858 if (b != null) { 1859 to.put(key, b); 1860 } 1861 } 1862 1863 private static final void copyString(String key, ContentValues from, ContentValues to) { 1864 String s = from.getAsString(key); 1865 if (s != null) { 1866 to.put(key, s); 1867 } 1868 } 1869 1870 private static final void copyStringWithDefault(String key, ContentValues from, 1871 ContentValues to, String defaultValue) { 1872 copyString(key, from, to); 1873 if (!to.containsKey(key)) { 1874 to.put(key, defaultValue); 1875 } 1876 } 1877 1878 private void grantAllDownloadsPermission(String toPackage, long id) { 1879 final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); 1880 getContext().grantUriPermission(toPackage, uri, 1881 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 1882 } 1883 1884 private void revokeAllDownloadsPermission(long id) { 1885 final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); 1886 getContext().revokeUriPermission(uri, ~0); 1887 } 1888 } 1889