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