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