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_MEDIA_SCANNED; 22 import static android.provider.Downloads.Impl.COLUMN_MIME_TYPE; 23 import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD; 24 import static android.provider.Downloads.Impl._DATA; 25 26 import android.app.AppOpsManager; 27 import android.app.DownloadManager; 28 import android.app.DownloadManager.Request; 29 import android.app.job.JobScheduler; 30 import android.content.ContentProvider; 31 import android.content.ContentResolver; 32 import android.content.ContentUris; 33 import android.content.ContentValues; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.content.UriMatcher; 37 import android.content.pm.ApplicationInfo; 38 import android.content.pm.PackageManager; 39 import android.content.pm.PackageManager.NameNotFoundException; 40 import android.database.Cursor; 41 import android.database.DatabaseUtils; 42 import android.database.SQLException; 43 import android.database.sqlite.SQLiteDatabase; 44 import android.database.sqlite.SQLiteOpenHelper; 45 import android.database.sqlite.SQLiteQueryBuilder; 46 import android.net.Uri; 47 import android.os.Binder; 48 import android.os.ParcelFileDescriptor; 49 import android.os.ParcelFileDescriptor.OnCloseListener; 50 import android.os.Process; 51 import android.provider.BaseColumns; 52 import android.provider.Downloads; 53 import android.provider.OpenableColumns; 54 import android.text.TextUtils; 55 import android.text.format.DateUtils; 56 import android.util.Log; 57 58 import com.android.internal.util.IndentingPrintWriter; 59 60 import libcore.io.IoUtils; 61 62 import com.google.android.collect.Maps; 63 import com.google.common.annotations.VisibleForTesting; 64 65 import java.io.File; 66 import java.io.FileDescriptor; 67 import java.io.FileNotFoundException; 68 import java.io.IOException; 69 import java.io.PrintWriter; 70 import java.util.ArrayList; 71 import java.util.Arrays; 72 import java.util.HashMap; 73 import java.util.HashSet; 74 import java.util.Iterator; 75 import java.util.List; 76 import java.util.Map; 77 78 /** 79 * Allows application to interact with the download manager. 80 */ 81 public final class DownloadProvider extends ContentProvider { 82 /** Database filename */ 83 private static final String DB_NAME = "downloads.db"; 84 /** Current database version */ 85 private static final int DB_VERSION = 110; 86 /** Name of table in the database */ 87 private static final String DB_TABLE = "downloads"; 88 89 /** MIME type for the entire download list */ 90 private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download"; 91 /** MIME type for an individual download */ 92 private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download"; 93 94 /** URI matcher used to recognize URIs sent by applications */ 95 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 96 /** URI matcher constant for the URI of all downloads belonging to the calling UID */ 97 private static final int MY_DOWNLOADS = 1; 98 /** URI matcher constant for the URI of an individual download belonging to the calling UID */ 99 private static final int MY_DOWNLOADS_ID = 2; 100 /** URI matcher constant for the URI of all downloads in the system */ 101 private static final int ALL_DOWNLOADS = 3; 102 /** URI matcher constant for the URI of an individual download */ 103 private static final int ALL_DOWNLOADS_ID = 4; 104 /** URI matcher constant for the URI of a download's request headers */ 105 private static final int REQUEST_HEADERS_URI = 5; 106 /** URI matcher constant for the public URI returned by 107 * {@link DownloadManager#getUriForDownloadedFile(long)} if the given downloaded file 108 * is publicly accessible. 109 */ 110 private static final int PUBLIC_DOWNLOAD_ID = 6; 111 static { 112 sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS); 113 sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID); 114 sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS); 115 sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID); 116 sURIMatcher.addURI("downloads", 117 "my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT, 118 REQUEST_HEADERS_URI); 119 sURIMatcher.addURI("downloads", 120 "all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT, 121 REQUEST_HEADERS_URI); 122 // temporary, for backwards compatibility 123 sURIMatcher.addURI("downloads", "download", MY_DOWNLOADS); 124 sURIMatcher.addURI("downloads", "download/#", MY_DOWNLOADS_ID); 125 sURIMatcher.addURI("downloads", 126 "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT, 127 REQUEST_HEADERS_URI); 128 sURIMatcher.addURI("downloads", 129 Downloads.Impl.PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT + "/#", 130 PUBLIC_DOWNLOAD_ID); 131 } 132 133 /** Different base URIs that could be used to access an individual download */ 134 private static final Uri[] BASE_URIS = new Uri[] { 135 Downloads.Impl.CONTENT_URI, 136 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 137 }; 138 139 private static final String[] sAppReadableColumnsArray = new String[] { 140 Downloads.Impl._ID, 141 Downloads.Impl.COLUMN_APP_DATA, 142 Downloads.Impl._DATA, 143 Downloads.Impl.COLUMN_MIME_TYPE, 144 Downloads.Impl.COLUMN_VISIBILITY, 145 Downloads.Impl.COLUMN_DESTINATION, 146 Downloads.Impl.COLUMN_CONTROL, 147 Downloads.Impl.COLUMN_STATUS, 148 Downloads.Impl.COLUMN_LAST_MODIFICATION, 149 Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, 150 Downloads.Impl.COLUMN_NOTIFICATION_CLASS, 151 Downloads.Impl.COLUMN_TOTAL_BYTES, 152 Downloads.Impl.COLUMN_CURRENT_BYTES, 153 Downloads.Impl.COLUMN_TITLE, 154 Downloads.Impl.COLUMN_DESCRIPTION, 155 Downloads.Impl.COLUMN_URI, 156 Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, 157 Downloads.Impl.COLUMN_FILE_NAME_HINT, 158 Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, 159 Downloads.Impl.COLUMN_DELETED, 160 OpenableColumns.DISPLAY_NAME, 161 OpenableColumns.SIZE, 162 }; 163 164 private static final HashSet<String> sAppReadableColumnsSet; 165 private static final HashMap<String, String> sColumnsMap; 166 167 static { 168 sAppReadableColumnsSet = new HashSet<String>(); 169 for (int i = 0; i < sAppReadableColumnsArray.length; ++i) { 170 sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]); 171 } 172 173 sColumnsMap = Maps.newHashMap(); sColumnsMap.put(OpenableColumns.DISPLAY_NAME, Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME)174 sColumnsMap.put(OpenableColumns.DISPLAY_NAME, 175 Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME); sColumnsMap.put(OpenableColumns.SIZE, Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE)176 sColumnsMap.put(OpenableColumns.SIZE, 177 Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE); 178 } 179 private static final List<String> downloadManagerColumnsList = 180 Arrays.asList(DownloadManager.UNDERLYING_COLUMNS); 181 182 @VisibleForTesting 183 SystemFacade mSystemFacade; 184 185 /** The database that lies underneath this content provider */ 186 private SQLiteOpenHelper mOpenHelper = null; 187 188 /** List of uids that can access the downloads */ 189 private int mSystemUid = -1; 190 private int mDefContainerUid = -1; 191 192 /** 193 * This class encapsulates a SQL where clause and its parameters. It makes it possible for 194 * shared methods (like {@link DownloadProvider#getWhereClause(Uri, String, String[], int)}) 195 * to return both pieces of information, and provides some utility logic to ease piece-by-piece 196 * construction of selections. 197 */ 198 private static class SqlSelection { 199 public StringBuilder mWhereClause = new StringBuilder(); 200 public List<String> mParameters = new ArrayList<String>(); 201 appendClause(String newClause, final T... parameters)202 public <T> void appendClause(String newClause, final T... parameters) { 203 if (newClause == null || newClause.isEmpty()) { 204 return; 205 } 206 if (mWhereClause.length() != 0) { 207 mWhereClause.append(" AND "); 208 } 209 mWhereClause.append("("); 210 mWhereClause.append(newClause); 211 mWhereClause.append(")"); 212 if (parameters != null) { 213 for (Object parameter : parameters) { 214 mParameters.add(parameter.toString()); 215 } 216 } 217 } 218 getSelection()219 public String getSelection() { 220 return mWhereClause.toString(); 221 } 222 getParameters()223 public String[] getParameters() { 224 String[] array = new String[mParameters.size()]; 225 return mParameters.toArray(array); 226 } 227 } 228 229 /** 230 * Creates and updated database on demand when opening it. 231 * Helper class to create database the first time the provider is 232 * initialized and upgrade it when a new version of the provider needs 233 * an updated version of the database. 234 */ 235 private final class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(final Context context)236 public DatabaseHelper(final Context context) { 237 super(context, DB_NAME, null, DB_VERSION); 238 } 239 240 /** 241 * Creates database the first time we try to open it. 242 */ 243 @Override onCreate(final SQLiteDatabase db)244 public void onCreate(final SQLiteDatabase db) { 245 if (Constants.LOGVV) { 246 Log.v(Constants.TAG, "populating new database"); 247 } 248 onUpgrade(db, 0, DB_VERSION); 249 } 250 251 /** 252 * Updates the database format when a content provider is used 253 * with a database that was created with a different format. 254 * 255 * Note: to support downgrades, creating a table should always drop it first if it already 256 * exists. 257 */ 258 @Override onUpgrade(final SQLiteDatabase db, int oldV, final int newV)259 public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) { 260 if (oldV == 31) { 261 // 31 and 100 are identical, just in different codelines. Upgrading from 31 is the 262 // same as upgrading from 100. 263 oldV = 100; 264 } else if (oldV < 100) { 265 // no logic to upgrade from these older version, just recreate the DB 266 Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV 267 + " to version " + newV + ", which will destroy all old data"); 268 oldV = 99; 269 } else if (oldV > newV) { 270 // user must have downgraded software; we have no way to know how to downgrade the 271 // DB, so just recreate it 272 Log.i(Constants.TAG, "Downgrading downloads database from version " + oldV 273 + " (current version is " + newV + "), destroying all old data"); 274 oldV = 99; 275 } 276 277 for (int version = oldV + 1; version <= newV; version++) { 278 upgradeTo(db, version); 279 } 280 } 281 282 /** 283 * Upgrade database from (version - 1) to version. 284 */ upgradeTo(SQLiteDatabase db, int version)285 private void upgradeTo(SQLiteDatabase db, int version) { 286 switch (version) { 287 case 100: 288 createDownloadsTable(db); 289 break; 290 291 case 101: 292 createHeadersTable(db); 293 break; 294 295 case 102: 296 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_PUBLIC_API, 297 "INTEGER NOT NULL DEFAULT 0"); 298 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_ROAMING, 299 "INTEGER NOT NULL DEFAULT 0"); 300 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, 301 "INTEGER NOT NULL DEFAULT 0"); 302 break; 303 304 case 103: 305 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, 306 "INTEGER NOT NULL DEFAULT 1"); 307 makeCacheDownloadsInvisible(db); 308 break; 309 310 case 104: 311 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT, 312 "INTEGER NOT NULL DEFAULT 0"); 313 break; 314 315 case 105: 316 fillNullValues(db); 317 break; 318 319 case 106: 320 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, "TEXT"); 321 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_DELETED, 322 "BOOLEAN NOT NULL DEFAULT 0"); 323 break; 324 325 case 107: 326 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT"); 327 break; 328 329 case 108: 330 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_METERED, 331 "INTEGER NOT NULL DEFAULT 1"); 332 break; 333 334 case 109: 335 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_WRITE, 336 "BOOLEAN NOT NULL DEFAULT 0"); 337 break; 338 339 case 110: 340 addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_FLAGS, 341 "INTEGER NOT NULL DEFAULT 0"); 342 break; 343 344 default: 345 throw new IllegalStateException("Don't know how to upgrade to " + version); 346 } 347 } 348 349 /** 350 * insert() now ensures these four columns are never null for new downloads, so this method 351 * makes that true for existing columns, so that code can rely on this assumption. 352 */ fillNullValues(SQLiteDatabase db)353 private void fillNullValues(SQLiteDatabase db) { 354 ContentValues values = new ContentValues(); 355 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); 356 fillNullValuesForColumn(db, values); 357 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1); 358 fillNullValuesForColumn(db, values); 359 values.put(Downloads.Impl.COLUMN_TITLE, ""); 360 fillNullValuesForColumn(db, values); 361 values.put(Downloads.Impl.COLUMN_DESCRIPTION, ""); 362 fillNullValuesForColumn(db, values); 363 } 364 fillNullValuesForColumn(SQLiteDatabase db, ContentValues values)365 private void fillNullValuesForColumn(SQLiteDatabase db, ContentValues values) { 366 String column = values.valueSet().iterator().next().getKey(); 367 db.update(DB_TABLE, values, column + " is null", null); 368 values.clear(); 369 } 370 371 /** 372 * Set all existing downloads to the cache partition to be invisible in the downloads UI. 373 */ makeCacheDownloadsInvisible(SQLiteDatabase db)374 private void makeCacheDownloadsInvisible(SQLiteDatabase db) { 375 ContentValues values = new ContentValues(); 376 values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, false); 377 String cacheSelection = Downloads.Impl.COLUMN_DESTINATION 378 + " != " + Downloads.Impl.DESTINATION_EXTERNAL; 379 db.update(DB_TABLE, values, cacheSelection, null); 380 } 381 382 /** 383 * Add a column to a table using ALTER TABLE. 384 * @param dbTable name of the table 385 * @param columnName name of the column to add 386 * @param columnDefinition SQL for the column definition 387 */ addColumn(SQLiteDatabase db, String dbTable, String columnName, String columnDefinition)388 private void addColumn(SQLiteDatabase db, String dbTable, String columnName, 389 String columnDefinition) { 390 db.execSQL("ALTER TABLE " + dbTable + " ADD COLUMN " + columnName + " " 391 + columnDefinition); 392 } 393 394 /** 395 * Creates the table that'll hold the download information. 396 */ createDownloadsTable(SQLiteDatabase db)397 private void createDownloadsTable(SQLiteDatabase db) { 398 try { 399 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); 400 db.execSQL("CREATE TABLE " + DB_TABLE + "(" + 401 Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 402 Downloads.Impl.COLUMN_URI + " TEXT, " + 403 Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " + 404 Downloads.Impl.COLUMN_APP_DATA + " TEXT, " + 405 Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " + 406 Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " + 407 Constants.OTA_UPDATE + " BOOLEAN, " + 408 Downloads.Impl._DATA + " TEXT, " + 409 Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " + 410 Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " + 411 Constants.NO_SYSTEM_FILES + " BOOLEAN, " + 412 Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " + 413 Downloads.Impl.COLUMN_CONTROL + " INTEGER, " + 414 Downloads.Impl.COLUMN_STATUS + " INTEGER, " + 415 Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " + 416 Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " + 417 Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " + 418 Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " + 419 Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " + 420 Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " + 421 Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " + 422 Downloads.Impl.COLUMN_REFERER + " TEXT, " + 423 Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " + 424 Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " + 425 Constants.ETAG + " TEXT, " + 426 Constants.UID + " INTEGER, " + 427 Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " + 428 Downloads.Impl.COLUMN_TITLE + " TEXT, " + 429 Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " + 430 Downloads.Impl.COLUMN_MEDIA_SCANNED + " BOOLEAN);"); 431 } catch (SQLException ex) { 432 Log.e(Constants.TAG, "couldn't create table in downloads database"); 433 throw ex; 434 } 435 } 436 createHeadersTable(SQLiteDatabase db)437 private void createHeadersTable(SQLiteDatabase db) { 438 db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE); 439 db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" + 440 "id INTEGER PRIMARY KEY AUTOINCREMENT," + 441 Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," + 442 Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," + 443 Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" + 444 ");"); 445 } 446 } 447 448 /** 449 * Initializes the content provider when it is created. 450 */ 451 @Override onCreate()452 public boolean onCreate() { 453 if (mSystemFacade == null) { 454 mSystemFacade = new RealSystemFacade(getContext()); 455 } 456 457 mOpenHelper = new DatabaseHelper(getContext()); 458 // Initialize the system uid 459 mSystemUid = Process.SYSTEM_UID; 460 // Initialize the default container uid. Package name hardcoded 461 // for now. 462 ApplicationInfo appInfo = null; 463 try { 464 appInfo = getContext().getPackageManager(). 465 getApplicationInfo("com.android.defcontainer", 0); 466 } catch (NameNotFoundException e) { 467 Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e); 468 } 469 if (appInfo != null) { 470 mDefContainerUid = appInfo.uid; 471 } 472 473 // Grant access permissions for all known downloads to the owning apps 474 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 475 final Cursor cursor = db.query(DB_TABLE, new String[] { 476 Downloads.Impl._ID, Constants.UID }, null, null, null, null, null); 477 final ArrayList<Long> idsToDelete = new ArrayList<>(); 478 try { 479 while (cursor.moveToNext()) { 480 final long downloadId = cursor.getLong(0); 481 final int uid = cursor.getInt(1); 482 final String ownerPackage = getPackageForUid(uid); 483 if (ownerPackage == null) { 484 idsToDelete.add(downloadId); 485 } else { 486 grantAllDownloadsPermission(ownerPackage, downloadId); 487 } 488 } 489 } finally { 490 cursor.close(); 491 } 492 if (idsToDelete.size() > 0) { 493 Log.i(Constants.TAG, 494 "Deleting downloads with ids " + idsToDelete + " as owner package is missing"); 495 deleteDownloadsWithIds(idsToDelete); 496 } 497 return true; 498 } 499 deleteDownloadsWithIds(ArrayList<Long> downloadIds)500 private void deleteDownloadsWithIds(ArrayList<Long> downloadIds) { 501 final int N = downloadIds.size(); 502 if (N == 0) { 503 return; 504 } 505 final StringBuilder queryBuilder = new StringBuilder(Downloads.Impl._ID + " in ("); 506 for (int i = 0; i < N; i++) { 507 queryBuilder.append(downloadIds.get(i)); 508 queryBuilder.append((i == N - 1) ? ")" : ","); 509 } 510 delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, queryBuilder.toString(), null); 511 } 512 513 /** 514 * Returns the content-provider-style MIME types of the various 515 * types accessible through this content provider. 516 */ 517 @Override getType(final Uri uri)518 public String getType(final Uri uri) { 519 int match = sURIMatcher.match(uri); 520 switch (match) { 521 case MY_DOWNLOADS: 522 case ALL_DOWNLOADS: { 523 return DOWNLOAD_LIST_TYPE; 524 } 525 case MY_DOWNLOADS_ID: 526 case ALL_DOWNLOADS_ID: 527 case PUBLIC_DOWNLOAD_ID: { 528 // return the mimetype of this id from the database 529 final String id = getDownloadIdFromUri(uri); 530 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 531 final String mimeType = DatabaseUtils.stringForQuery(db, 532 "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE + 533 " WHERE " + Downloads.Impl._ID + " = ?", 534 new String[]{id}); 535 if (TextUtils.isEmpty(mimeType)) { 536 return DOWNLOAD_TYPE; 537 } else { 538 return mimeType; 539 } 540 } 541 default: { 542 if (Constants.LOGV) { 543 Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri); 544 } 545 throw new IllegalArgumentException("Unknown URI: " + uri); 546 } 547 } 548 } 549 550 /** 551 * Inserts a row in the database 552 */ 553 @Override insert(final Uri uri, final ContentValues values)554 public Uri insert(final Uri uri, final ContentValues values) { 555 checkInsertPermissions(values); 556 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 557 558 // note we disallow inserting into ALL_DOWNLOADS 559 int match = sURIMatcher.match(uri); 560 if (match != MY_DOWNLOADS) { 561 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri); 562 throw new IllegalArgumentException("Unknown/Invalid URI " + uri); 563 } 564 565 // copy some of the input values as it 566 ContentValues filteredValues = new ContentValues(); 567 copyString(Downloads.Impl.COLUMN_URI, values, filteredValues); 568 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); 569 copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues); 570 copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues); 571 copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues); 572 copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues); 573 574 boolean isPublicApi = 575 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE; 576 577 // validate the destination column 578 Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION); 579 if (dest != null) { 580 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED) 581 != PackageManager.PERMISSION_GRANTED 582 && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION 583 || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING 584 || dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION)) { 585 throw new SecurityException("setting destination to : " + dest + 586 " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted"); 587 } 588 // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically 589 // switch to non-purgeable download 590 boolean hasNonPurgeablePermission = 591 getContext().checkCallingOrSelfPermission( 592 Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE) 593 == PackageManager.PERMISSION_GRANTED; 594 if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE 595 && hasNonPurgeablePermission) { 596 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION; 597 } 598 if (dest == Downloads.Impl.DESTINATION_FILE_URI) { 599 checkFileUriDestination(values); 600 601 } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 602 getContext().enforceCallingOrSelfPermission( 603 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 604 "No permission to write"); 605 606 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 607 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, 608 getCallingPackage()) != AppOpsManager.MODE_ALLOWED) { 609 throw new SecurityException("No permission to write"); 610 } 611 612 } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) { 613 getContext().enforcePermission( 614 android.Manifest.permission.ACCESS_CACHE_FILESYSTEM, 615 Binder.getCallingPid(), Binder.getCallingUid(), 616 "need ACCESS_CACHE_FILESYSTEM permission to use system cache"); 617 } 618 filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest); 619 } 620 621 // validate the visibility column 622 Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY); 623 if (vis == null) { 624 if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 625 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, 626 Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); 627 } else { 628 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, 629 Downloads.Impl.VISIBILITY_HIDDEN); 630 } 631 } else { 632 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis); 633 } 634 // copy the control column as is 635 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); 636 637 /* 638 * requests coming from 639 * DownloadManager.addCompletedDownload(String, String, String, 640 * boolean, String, String, long) need special treatment 641 */ 642 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == 643 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 644 // these requests always are marked as 'completed' 645 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS); 646 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, 647 values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES)); 648 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); 649 copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues); 650 copyString(Downloads.Impl._DATA, values, filteredValues); 651 copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues); 652 } else { 653 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING); 654 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1); 655 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); 656 } 657 658 // set lastupdate to current time 659 long lastMod = mSystemFacade.currentTimeMillis(); 660 filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod); 661 662 // use packagename of the caller to set the notification columns 663 String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); 664 String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS); 665 if (pckg != null && (clazz != null || isPublicApi)) { 666 int uid = Binder.getCallingUid(); 667 try { 668 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) { 669 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg); 670 if (clazz != null) { 671 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz); 672 } 673 } 674 } catch (PackageManager.NameNotFoundException ex) { 675 /* ignored for now */ 676 } 677 } 678 679 // copy some more columns as is 680 copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues); 681 copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues); 682 copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues); 683 copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues); 684 685 // UID, PID columns 686 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED) 687 == PackageManager.PERMISSION_GRANTED) { 688 copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues); 689 } 690 filteredValues.put(Constants.UID, Binder.getCallingUid()); 691 if (Binder.getCallingUid() == 0) { 692 copyInteger(Constants.UID, values, filteredValues); 693 } 694 695 // copy some more columns as is 696 copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, ""); 697 copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, ""); 698 699 // is_visible_in_downloads_ui column 700 if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) { 701 copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues); 702 } else { 703 // by default, make external downloads visible in the UI 704 boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL); 705 filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal); 706 } 707 708 // public api requests and networktypes/roaming columns 709 if (isPublicApi) { 710 copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues); 711 copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues); 712 copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues); 713 copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues); 714 } 715 716 if (Constants.LOGVV) { 717 Log.v(Constants.TAG, "initiating download with UID " 718 + filteredValues.getAsInteger(Constants.UID)); 719 if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) { 720 Log.v(Constants.TAG, "other UID " + 721 filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID)); 722 } 723 } 724 725 long rowID = db.insert(DB_TABLE, null, filteredValues); 726 if (rowID == -1) { 727 Log.d(Constants.TAG, "couldn't insert into downloads database"); 728 return null; 729 } 730 731 insertRequestHeaders(db, rowID, values); 732 733 final String callingPackage = getPackageForUid(Binder.getCallingUid()); 734 if (callingPackage == null) { 735 Log.e(Constants.TAG, "Package does not exist for calling uid"); 736 return null; 737 } 738 grantAllDownloadsPermission(callingPackage, rowID); 739 notifyContentChanged(uri, match); 740 741 final long token = Binder.clearCallingIdentity(); 742 try { 743 Helpers.scheduleJob(getContext(), rowID); 744 } finally { 745 Binder.restoreCallingIdentity(token); 746 } 747 748 if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD 749 && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) { 750 DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA), 751 values.getAsString(COLUMN_MIME_TYPE)); 752 } 753 754 return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID); 755 } 756 getPackageForUid(int uid)757 private String getPackageForUid(int uid) { 758 String[] packages = getContext().getPackageManager().getPackagesForUid(uid); 759 if (packages == null || packages.length == 0) { 760 return null; 761 } 762 // For permission related purposes, any package belonging to the given uid should work. 763 return packages[0]; 764 } 765 766 /** 767 * Check that the file URI provided for DESTINATION_FILE_URI is valid. 768 */ checkFileUriDestination(ContentValues values)769 private void checkFileUriDestination(ContentValues values) { 770 String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 771 if (fileUri == null) { 772 throw new IllegalArgumentException( 773 "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT"); 774 } 775 Uri uri = Uri.parse(fileUri); 776 String scheme = uri.getScheme(); 777 if (scheme == null || !scheme.equals("file")) { 778 throw new IllegalArgumentException("Not a file URI: " + uri); 779 } 780 final String path = uri.getPath(); 781 if (path == null) { 782 throw new IllegalArgumentException("Invalid file URI: " + uri); 783 } 784 785 final File file; 786 try { 787 file = new File(path).getCanonicalFile(); 788 } catch (IOException e) { 789 throw new SecurityException(e); 790 } 791 792 if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) { 793 // No permissions required for paths belonging to calling package 794 return; 795 } else if (Helpers.isFilenameValidInExternal(getContext(), file)) { 796 // Otherwise we require write permission 797 getContext().enforceCallingOrSelfPermission( 798 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 799 "No permission to write to " + file); 800 801 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 802 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, 803 getCallingPackage()) != AppOpsManager.MODE_ALLOWED) { 804 throw new SecurityException("No permission to write to " + file); 805 } 806 807 } else { 808 throw new SecurityException("Unsupported path " + file); 809 } 810 } 811 812 /** 813 * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to 814 * constraints in the rest of the code. Apps without that may still access this provider through 815 * the public API, but additional restrictions are imposed. We check those restrictions here. 816 * 817 * @param values ContentValues provided to insert() 818 * @throws SecurityException if the caller has insufficient permissions 819 */ checkInsertPermissions(ContentValues values)820 private void checkInsertPermissions(ContentValues values) { 821 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS) 822 == PackageManager.PERMISSION_GRANTED) { 823 return; 824 } 825 826 getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, 827 "INTERNET permission is required to use the download manager"); 828 829 // ensure the request fits within the bounds of a public API request 830 // first copy so we can remove values 831 values = new ContentValues(values); 832 833 // check columns whose values are restricted 834 enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE); 835 836 // validate the destination column 837 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == 838 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 839 /* this row is inserted by 840 * DownloadManager.addCompletedDownload(String, String, String, 841 * boolean, String, String, long) 842 */ 843 values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES); 844 values.remove(Downloads.Impl._DATA); 845 values.remove(Downloads.Impl.COLUMN_STATUS); 846 } 847 enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION, 848 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE, 849 Downloads.Impl.DESTINATION_FILE_URI, 850 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD); 851 852 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION) 853 == PackageManager.PERMISSION_GRANTED) { 854 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 855 Request.VISIBILITY_HIDDEN, 856 Request.VISIBILITY_VISIBLE, 857 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 858 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 859 } else { 860 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 861 Request.VISIBILITY_VISIBLE, 862 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 863 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 864 } 865 866 // remove the rest of the columns that are allowed (with any value) 867 values.remove(Downloads.Impl.COLUMN_URI); 868 values.remove(Downloads.Impl.COLUMN_TITLE); 869 values.remove(Downloads.Impl.COLUMN_DESCRIPTION); 870 values.remove(Downloads.Impl.COLUMN_MIME_TYPE); 871 values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert() 872 values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert() 873 values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); 874 values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING); 875 values.remove(Downloads.Impl.COLUMN_ALLOW_METERED); 876 values.remove(Downloads.Impl.COLUMN_FLAGS); 877 values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI); 878 values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED); 879 values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE); 880 Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator(); 881 while (iterator.hasNext()) { 882 String key = iterator.next().getKey(); 883 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 884 iterator.remove(); 885 } 886 } 887 888 // any extra columns are extraneous and disallowed 889 if (values.size() > 0) { 890 StringBuilder error = new StringBuilder("Invalid columns in request: "); 891 boolean first = true; 892 for (Map.Entry<String, Object> entry : values.valueSet()) { 893 if (!first) { 894 error.append(", "); 895 } 896 error.append(entry.getKey()); 897 } 898 throw new SecurityException(error.toString()); 899 } 900 } 901 902 /** 903 * Remove column from values, and throw a SecurityException if the value isn't within the 904 * specified allowedValues. 905 */ enforceAllowedValues(ContentValues values, String column, Object... allowedValues)906 private void enforceAllowedValues(ContentValues values, String column, 907 Object... allowedValues) { 908 Object value = values.get(column); 909 values.remove(column); 910 for (Object allowedValue : allowedValues) { 911 if (value == null && allowedValue == null) { 912 return; 913 } 914 if (value != null && value.equals(allowedValue)) { 915 return; 916 } 917 } 918 throw new SecurityException("Invalid value for " + column + ": " + value); 919 } 920 queryCleared(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort)921 private Cursor queryCleared(Uri uri, String[] projection, String selection, 922 String[] selectionArgs, String sort) { 923 final long token = Binder.clearCallingIdentity(); 924 try { 925 return query(uri, projection, selection, selectionArgs, sort); 926 } finally { 927 Binder.restoreCallingIdentity(token); 928 } 929 } 930 931 /** 932 * Starts a database query 933 */ 934 @Override query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort)935 public Cursor query(final Uri uri, String[] projection, 936 final String selection, final String[] selectionArgs, 937 final String sort) { 938 939 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 940 941 int match = sURIMatcher.match(uri); 942 if (match == -1) { 943 if (Constants.LOGV) { 944 Log.v(Constants.TAG, "querying unknown URI: " + uri); 945 } 946 throw new IllegalArgumentException("Unknown URI: " + uri); 947 } 948 949 if (match == REQUEST_HEADERS_URI) { 950 if (projection != null || selection != null || sort != null) { 951 throw new UnsupportedOperationException("Request header queries do not support " 952 + "projections, selections or sorting"); 953 } 954 return queryRequestHeaders(db, uri); 955 } 956 957 SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match); 958 959 if (shouldRestrictVisibility()) { 960 if (projection == null) { 961 projection = sAppReadableColumnsArray.clone(); 962 } else { 963 // check the validity of the columns in projection 964 for (int i = 0; i < projection.length; ++i) { 965 if (!sAppReadableColumnsSet.contains(projection[i]) && 966 !downloadManagerColumnsList.contains(projection[i])) { 967 throw new IllegalArgumentException( 968 "column " + projection[i] + " is not allowed in queries"); 969 } 970 } 971 } 972 973 for (int i = 0; i < projection.length; i++) { 974 final String newColumn = sColumnsMap.get(projection[i]); 975 if (newColumn != null) { 976 projection[i] = newColumn; 977 } 978 } 979 } 980 981 if (Constants.LOGVV) { 982 logVerboseQueryInfo(projection, selection, selectionArgs, sort, db); 983 } 984 985 SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); 986 builder.setTables(DB_TABLE); 987 builder.setStrict(true); 988 Cursor ret = builder.query(db, projection, fullSelection.getSelection(), 989 fullSelection.getParameters(), null, null, sort); 990 991 if (ret != null) { 992 ret.setNotificationUri(getContext().getContentResolver(), uri); 993 if (Constants.LOGVV) { 994 Log.v(Constants.TAG, 995 "created cursor " + ret + " on behalf of " + Binder.getCallingPid()); 996 } 997 } else { 998 if (Constants.LOGV) { 999 Log.v(Constants.TAG, "query failed in downloads database"); 1000 } 1001 } 1002 1003 return ret; 1004 } 1005 logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db)1006 private void logVerboseQueryInfo(String[] projection, final String selection, 1007 final String[] selectionArgs, final String sort, SQLiteDatabase db) { 1008 java.lang.StringBuilder sb = new java.lang.StringBuilder(); 1009 sb.append("starting query, database is "); 1010 if (db != null) { 1011 sb.append("not "); 1012 } 1013 sb.append("null; "); 1014 if (projection == null) { 1015 sb.append("projection is null; "); 1016 } else if (projection.length == 0) { 1017 sb.append("projection is empty; "); 1018 } else { 1019 for (int i = 0; i < projection.length; ++i) { 1020 sb.append("projection["); 1021 sb.append(i); 1022 sb.append("] is "); 1023 sb.append(projection[i]); 1024 sb.append("; "); 1025 } 1026 } 1027 sb.append("selection is "); 1028 sb.append(selection); 1029 sb.append("; "); 1030 if (selectionArgs == null) { 1031 sb.append("selectionArgs is null; "); 1032 } else if (selectionArgs.length == 0) { 1033 sb.append("selectionArgs is empty; "); 1034 } else { 1035 for (int i = 0; i < selectionArgs.length; ++i) { 1036 sb.append("selectionArgs["); 1037 sb.append(i); 1038 sb.append("] is "); 1039 sb.append(selectionArgs[i]); 1040 sb.append("; "); 1041 } 1042 } 1043 sb.append("sort is "); 1044 sb.append(sort); 1045 sb.append("."); 1046 Log.v(Constants.TAG, sb.toString()); 1047 } 1048 getDownloadIdFromUri(final Uri uri)1049 private String getDownloadIdFromUri(final Uri uri) { 1050 return uri.getPathSegments().get(1); 1051 } 1052 1053 /** 1054 * Insert request headers for a download into the DB. 1055 */ insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values)1056 private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) { 1057 ContentValues rowValues = new ContentValues(); 1058 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId); 1059 for (Map.Entry<String, Object> entry : values.valueSet()) { 1060 String key = entry.getKey(); 1061 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 1062 String headerLine = entry.getValue().toString(); 1063 if (!headerLine.contains(":")) { 1064 throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine); 1065 } 1066 String[] parts = headerLine.split(":", 2); 1067 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim()); 1068 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim()); 1069 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues); 1070 } 1071 } 1072 } 1073 1074 /** 1075 * Handle a query for the custom request headers registered for a download. 1076 */ queryRequestHeaders(SQLiteDatabase db, Uri uri)1077 private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) { 1078 String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" 1079 + getDownloadIdFromUri(uri); 1080 String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER, 1081 Downloads.Impl.RequestHeaders.COLUMN_VALUE}; 1082 return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where, 1083 null, null, null, null); 1084 } 1085 1086 /** 1087 * Delete request headers for downloads matching the given query. 1088 */ deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs)1089 private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) { 1090 String[] projection = new String[] {Downloads.Impl._ID}; 1091 Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null); 1092 try { 1093 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 1094 long id = cursor.getLong(0); 1095 String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id; 1096 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null); 1097 } 1098 } finally { 1099 cursor.close(); 1100 } 1101 } 1102 1103 /** 1104 * @return true if we should restrict the columns readable by this caller 1105 */ shouldRestrictVisibility()1106 private boolean shouldRestrictVisibility() { 1107 int callingUid = Binder.getCallingUid(); 1108 return Binder.getCallingPid() != Process.myPid() && 1109 callingUid != mSystemUid && 1110 callingUid != mDefContainerUid; 1111 } 1112 1113 /** 1114 * Updates a row in the database 1115 */ 1116 @Override update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)1117 public int update(final Uri uri, final ContentValues values, 1118 final String where, final String[] whereArgs) { 1119 if (shouldRestrictVisibility()) { 1120 Helpers.validateSelection(where, sAppReadableColumnsSet); 1121 } 1122 1123 final Context context = getContext(); 1124 final ContentResolver resolver = context.getContentResolver(); 1125 1126 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1127 1128 int count; 1129 boolean updateSchedule = false; 1130 boolean isCompleting = false; 1131 1132 ContentValues filteredValues; 1133 if (Binder.getCallingPid() != Process.myPid()) { 1134 filteredValues = new ContentValues(); 1135 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); 1136 copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues); 1137 Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL); 1138 if (i != null) { 1139 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i); 1140 updateSchedule = true; 1141 } 1142 1143 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); 1144 copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues); 1145 copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues); 1146 copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues); 1147 copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues); 1148 } else { 1149 filteredValues = values; 1150 String filename = values.getAsString(Downloads.Impl._DATA); 1151 if (filename != null) { 1152 Cursor c = null; 1153 try { 1154 c = query(uri, new String[] 1155 { Downloads.Impl.COLUMN_TITLE }, null, null, null); 1156 if (!c.moveToFirst() || c.getString(0).isEmpty()) { 1157 values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName()); 1158 } 1159 } finally { 1160 IoUtils.closeQuietly(c); 1161 } 1162 } 1163 1164 Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS); 1165 boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING; 1166 boolean isUserBypassingSizeLimit = 1167 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); 1168 if (isRestart || isUserBypassingSizeLimit) { 1169 updateSchedule = true; 1170 } 1171 isCompleting = status != null && Downloads.Impl.isStatusCompleted(status); 1172 } 1173 1174 int match = sURIMatcher.match(uri); 1175 switch (match) { 1176 case MY_DOWNLOADS: 1177 case MY_DOWNLOADS_ID: 1178 case ALL_DOWNLOADS: 1179 case ALL_DOWNLOADS_ID: 1180 if (filteredValues.size() == 0) { 1181 count = 0; 1182 break; 1183 } 1184 1185 final SqlSelection selection = getWhereClause(uri, where, whereArgs, match); 1186 count = db.update(DB_TABLE, filteredValues, selection.getSelection(), 1187 selection.getParameters()); 1188 if (updateSchedule || isCompleting) { 1189 final long token = Binder.clearCallingIdentity(); 1190 try (Cursor cursor = db.query(DB_TABLE, null, selection.getSelection(), 1191 selection.getParameters(), null, null, null)) { 1192 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, 1193 cursor); 1194 final DownloadInfo info = new DownloadInfo(context); 1195 while (cursor.moveToNext()) { 1196 reader.updateFromDatabase(info); 1197 if (updateSchedule) { 1198 Helpers.scheduleJob(context, info); 1199 } 1200 if (isCompleting) { 1201 info.sendIntentIfRequested(); 1202 } 1203 } 1204 } finally { 1205 Binder.restoreCallingIdentity(token); 1206 } 1207 } 1208 break; 1209 1210 default: 1211 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri); 1212 throw new UnsupportedOperationException("Cannot update URI: " + uri); 1213 } 1214 1215 notifyContentChanged(uri, match); 1216 return count; 1217 } 1218 1219 /** 1220 * Notify of a change through both URIs (/my_downloads and /all_downloads) 1221 * @param uri either URI for the changed download(s) 1222 * @param uriMatch the match ID from {@link #sURIMatcher} 1223 */ notifyContentChanged(final Uri uri, int uriMatch)1224 private void notifyContentChanged(final Uri uri, int uriMatch) { 1225 Long downloadId = null; 1226 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) { 1227 downloadId = Long.parseLong(getDownloadIdFromUri(uri)); 1228 } 1229 for (Uri uriToNotify : BASE_URIS) { 1230 if (downloadId != null) { 1231 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId); 1232 } 1233 getContext().getContentResolver().notifyChange(uriToNotify, null); 1234 } 1235 } 1236 getWhereClause(final Uri uri, final String where, final String[] whereArgs, int uriMatch)1237 private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs, 1238 int uriMatch) { 1239 SqlSelection selection = new SqlSelection(); 1240 selection.appendClause(where, whereArgs); 1241 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID || 1242 uriMatch == PUBLIC_DOWNLOAD_ID) { 1243 selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri)); 1244 } 1245 if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID) 1246 && getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ALL) 1247 != PackageManager.PERMISSION_GRANTED) { 1248 selection.appendClause( 1249 Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?", 1250 Binder.getCallingUid(), Binder.getCallingUid()); 1251 } 1252 return selection; 1253 } 1254 1255 /** 1256 * Deletes a row in the database 1257 */ 1258 @Override delete(final Uri uri, final String where, final String[] whereArgs)1259 public int delete(final Uri uri, final String where, final String[] whereArgs) { 1260 if (shouldRestrictVisibility()) { 1261 Helpers.validateSelection(where, sAppReadableColumnsSet); 1262 } 1263 1264 final Context context = getContext(); 1265 final ContentResolver resolver = context.getContentResolver(); 1266 final JobScheduler scheduler = context.getSystemService(JobScheduler.class); 1267 1268 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1269 int count; 1270 int match = sURIMatcher.match(uri); 1271 switch (match) { 1272 case MY_DOWNLOADS: 1273 case MY_DOWNLOADS_ID: 1274 case ALL_DOWNLOADS: 1275 case ALL_DOWNLOADS_ID: 1276 final SqlSelection selection = getWhereClause(uri, where, whereArgs, match); 1277 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters()); 1278 1279 try (Cursor cursor = db.query(DB_TABLE, null, selection.getSelection(), 1280 selection.getParameters(), null, null, null)) { 1281 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); 1282 final DownloadInfo info = new DownloadInfo(context); 1283 while (cursor.moveToNext()) { 1284 reader.updateFromDatabase(info); 1285 scheduler.cancel((int) info.mId); 1286 1287 revokeAllDownloadsPermission(info.mId); 1288 DownloadStorageProvider.onDownloadProviderDelete(getContext(), info.mId); 1289 1290 final String path = info.mFileName; 1291 if (!TextUtils.isEmpty(path)) { 1292 try { 1293 final File file = new File(path).getCanonicalFile(); 1294 if (Helpers.isFilenameValid(getContext(), file)) { 1295 Log.v(Constants.TAG, 1296 "Deleting " + file + " via provider delete"); 1297 file.delete(); 1298 } 1299 } catch (IOException ignored) { 1300 } 1301 } 1302 1303 final String mediaUri = info.mMediaProviderUri; 1304 if (!TextUtils.isEmpty(mediaUri)) { 1305 final long token = Binder.clearCallingIdentity(); 1306 try { 1307 getContext().getContentResolver().delete(Uri.parse(mediaUri), null, 1308 null); 1309 } finally { 1310 Binder.restoreCallingIdentity(token); 1311 } 1312 } 1313 1314 // If the download wasn't completed yet, we're 1315 // effectively completing it now, and we need to send 1316 // any requested broadcasts 1317 if (!Downloads.Impl.isStatusCompleted(info.mStatus)) { 1318 info.sendIntentIfRequested(); 1319 } 1320 } 1321 } 1322 1323 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters()); 1324 break; 1325 1326 default: 1327 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri); 1328 throw new UnsupportedOperationException("Cannot delete URI: " + uri); 1329 } 1330 notifyContentChanged(uri, match); 1331 final long token = Binder.clearCallingIdentity(); 1332 try { 1333 Helpers.getDownloadNotifier(getContext()).update(); 1334 } finally { 1335 Binder.restoreCallingIdentity(token); 1336 } 1337 return count; 1338 } 1339 1340 /** 1341 * Remotely opens a file 1342 */ 1343 @Override openFile(final Uri uri, String mode)1344 public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException { 1345 if (Constants.LOGVV) { 1346 logVerboseOpenFileInfo(uri, mode); 1347 } 1348 1349 // Perform normal query to enforce caller identity access before 1350 // clearing it to reach internal-only columns 1351 final Cursor probeCursor = query(uri, new String[] { 1352 Downloads.Impl._DATA }, null, null, null); 1353 try { 1354 if ((probeCursor == null) || (probeCursor.getCount() == 0)) { 1355 throw new FileNotFoundException( 1356 "No file found for " + uri + " as UID " + Binder.getCallingUid()); 1357 } 1358 } finally { 1359 IoUtils.closeQuietly(probeCursor); 1360 } 1361 1362 final Cursor cursor = queryCleared(uri, new String[] { 1363 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS, 1364 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null, 1365 null, null); 1366 final String path; 1367 final boolean shouldScan; 1368 try { 1369 int count = (cursor != null) ? cursor.getCount() : 0; 1370 if (count != 1) { 1371 // If there is not exactly one result, throw an appropriate exception. 1372 if (count == 0) { 1373 throw new FileNotFoundException("No entry for " + uri); 1374 } 1375 throw new FileNotFoundException("Multiple items at " + uri); 1376 } 1377 1378 if (cursor.moveToFirst()) { 1379 final int status = cursor.getInt(1); 1380 final int destination = cursor.getInt(2); 1381 final int mediaScanned = cursor.getInt(3); 1382 1383 path = cursor.getString(0); 1384 shouldScan = Downloads.Impl.isStatusSuccess(status) && ( 1385 destination == Downloads.Impl.DESTINATION_EXTERNAL 1386 || destination == Downloads.Impl.DESTINATION_FILE_URI 1387 || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 1388 && mediaScanned != 2; 1389 } else { 1390 throw new FileNotFoundException("Failed moveToFirst"); 1391 } 1392 } finally { 1393 IoUtils.closeQuietly(cursor); 1394 } 1395 1396 if (path == null) { 1397 throw new FileNotFoundException("No filename found."); 1398 } 1399 1400 final File file; 1401 try { 1402 file = new File(path).getCanonicalFile(); 1403 } catch (IOException e) { 1404 throw new FileNotFoundException(e.getMessage()); 1405 } 1406 1407 if (!Helpers.isFilenameValid(getContext(), file)) { 1408 throw new FileNotFoundException("Invalid file: " + file); 1409 } 1410 1411 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 1412 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) { 1413 return ParcelFileDescriptor.open(file, pfdMode); 1414 } else { 1415 try { 1416 // When finished writing, update size and timestamp 1417 return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(), 1418 new OnCloseListener() { 1419 @Override 1420 public void onClose(IOException e) { 1421 final ContentValues values = new ContentValues(); 1422 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length()); 1423 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, 1424 System.currentTimeMillis()); 1425 update(uri, values, null, null); 1426 1427 if (shouldScan) { 1428 final Intent intent = new Intent( 1429 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 1430 intent.setData(Uri.fromFile(file)); 1431 getContext().sendBroadcast(intent); 1432 } 1433 } 1434 }); 1435 } catch (IOException e) { 1436 throw new FileNotFoundException("Failed to open for writing: " + e); 1437 } 1438 } 1439 } 1440 1441 @Override 1442 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1443 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120); 1444 1445 pw.println("Downloads updated in last hour:"); 1446 pw.increaseIndent(); 1447 1448 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1449 final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS; 1450 final Cursor cursor = db.query(DB_TABLE, null, 1451 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null, 1452 Downloads.Impl._ID + " ASC"); 1453 try { 1454 final String[] cols = cursor.getColumnNames(); 1455 final int idCol = cursor.getColumnIndex(BaseColumns._ID); 1456 while (cursor.moveToNext()) { 1457 pw.println("Download #" + cursor.getInt(idCol) + ":"); 1458 pw.increaseIndent(); 1459 for (int i = 0; i < cols.length; i++) { 1460 // Omit sensitive data when dumping 1461 if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) { 1462 continue; 1463 } 1464 pw.printPair(cols[i], cursor.getString(i)); 1465 } 1466 pw.println(); 1467 pw.decreaseIndent(); 1468 } 1469 } finally { 1470 cursor.close(); 1471 } 1472 1473 pw.decreaseIndent(); 1474 } 1475 1476 private void logVerboseOpenFileInfo(Uri uri, String mode) { 1477 Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode 1478 + ", uid: " + Binder.getCallingUid()); 1479 Cursor cursor = query(Downloads.Impl.CONTENT_URI, 1480 new String[] { "_id" }, null, null, "_id"); 1481 if (cursor == null) { 1482 Log.v(Constants.TAG, "null cursor in openFile"); 1483 } else { 1484 try { 1485 if (!cursor.moveToFirst()) { 1486 Log.v(Constants.TAG, "empty cursor in openFile"); 1487 } else { 1488 do { 1489 Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available"); 1490 } while(cursor.moveToNext()); 1491 } 1492 } finally { 1493 cursor.close(); 1494 } 1495 } 1496 cursor = query(uri, new String[] { "_data" }, null, null, null); 1497 if (cursor == null) { 1498 Log.v(Constants.TAG, "null cursor in openFile"); 1499 } else { 1500 try { 1501 if (!cursor.moveToFirst()) { 1502 Log.v(Constants.TAG, "empty cursor in openFile"); 1503 } else { 1504 String filename = cursor.getString(0); 1505 Log.v(Constants.TAG, "filename in openFile: " + filename); 1506 if (new java.io.File(filename).isFile()) { 1507 Log.v(Constants.TAG, "file exists in openFile"); 1508 } 1509 } 1510 } finally { 1511 cursor.close(); 1512 } 1513 } 1514 } 1515 1516 private static final void copyInteger(String key, ContentValues from, ContentValues to) { 1517 Integer i = from.getAsInteger(key); 1518 if (i != null) { 1519 to.put(key, i); 1520 } 1521 } 1522 1523 private static final void copyBoolean(String key, ContentValues from, ContentValues to) { 1524 Boolean b = from.getAsBoolean(key); 1525 if (b != null) { 1526 to.put(key, b); 1527 } 1528 } 1529 1530 private static final void copyString(String key, ContentValues from, ContentValues to) { 1531 String s = from.getAsString(key); 1532 if (s != null) { 1533 to.put(key, s); 1534 } 1535 } 1536 1537 private static final void copyStringWithDefault(String key, ContentValues from, 1538 ContentValues to, String defaultValue) { 1539 copyString(key, from, to); 1540 if (!to.containsKey(key)) { 1541 to.put(key, defaultValue); 1542 } 1543 } 1544 1545 private void grantAllDownloadsPermission(String toPackage, long id) { 1546 final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); 1547 getContext().grantUriPermission(toPackage, uri, 1548 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 1549 } 1550 1551 private void revokeAllDownloadsPermission(long id) { 1552 final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); 1553 getContext().revokeUriPermission(uri, ~0); 1554 } 1555 } 1556