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