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