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 return true; 472 } 473 474 /** 475 * Returns the content-provider-style MIME types of the various 476 * types accessible through this content provider. 477 */ 478 @Override getType(final Uri uri)479 public String getType(final Uri uri) { 480 int match = sURIMatcher.match(uri); 481 switch (match) { 482 case MY_DOWNLOADS: 483 case ALL_DOWNLOADS: { 484 return DOWNLOAD_LIST_TYPE; 485 } 486 case MY_DOWNLOADS_ID: 487 case ALL_DOWNLOADS_ID: 488 case PUBLIC_DOWNLOAD_ID: { 489 // return the mimetype of this id from the database 490 final String id = getDownloadIdFromUri(uri); 491 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 492 final String mimeType = DatabaseUtils.stringForQuery(db, 493 "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE + 494 " WHERE " + Downloads.Impl._ID + " = ?", 495 new String[]{id}); 496 if (TextUtils.isEmpty(mimeType)) { 497 return DOWNLOAD_TYPE; 498 } else { 499 return mimeType; 500 } 501 } 502 default: { 503 if (Constants.LOGV) { 504 Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri); 505 } 506 throw new IllegalArgumentException("Unknown URI: " + uri); 507 } 508 } 509 } 510 511 /** 512 * Inserts a row in the database 513 */ 514 @Override insert(final Uri uri, final ContentValues values)515 public Uri insert(final Uri uri, final ContentValues values) { 516 checkInsertPermissions(values); 517 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 518 519 // note we disallow inserting into ALL_DOWNLOADS 520 int match = sURIMatcher.match(uri); 521 if (match != MY_DOWNLOADS) { 522 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri); 523 throw new IllegalArgumentException("Unknown/Invalid URI " + uri); 524 } 525 526 // copy some of the input values as it 527 ContentValues filteredValues = new ContentValues(); 528 copyString(Downloads.Impl.COLUMN_URI, values, filteredValues); 529 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); 530 copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues); 531 copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues); 532 copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues); 533 copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues); 534 535 boolean isPublicApi = 536 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE; 537 538 // validate the destination column 539 Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION); 540 if (dest != null) { 541 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED) 542 != PackageManager.PERMISSION_GRANTED 543 && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION 544 || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING 545 || dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION)) { 546 throw new SecurityException("setting destination to : " + dest + 547 " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted"); 548 } 549 // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically 550 // switch to non-purgeable download 551 boolean hasNonPurgeablePermission = 552 getContext().checkCallingOrSelfPermission( 553 Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE) 554 == PackageManager.PERMISSION_GRANTED; 555 if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE 556 && hasNonPurgeablePermission) { 557 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION; 558 } 559 if (dest == Downloads.Impl.DESTINATION_FILE_URI) { 560 checkFileUriDestination(values); 561 562 } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 563 getContext().enforceCallingOrSelfPermission( 564 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 565 "No permission to write"); 566 567 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 568 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, 569 getCallingPackage()) != AppOpsManager.MODE_ALLOWED) { 570 throw new SecurityException("No permission to write"); 571 } 572 573 } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) { 574 getContext().enforcePermission( 575 android.Manifest.permission.ACCESS_CACHE_FILESYSTEM, 576 Binder.getCallingPid(), Binder.getCallingUid(), 577 "need ACCESS_CACHE_FILESYSTEM permission to use system cache"); 578 } 579 filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest); 580 } 581 582 // validate the visibility column 583 Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY); 584 if (vis == null) { 585 if (dest == Downloads.Impl.DESTINATION_EXTERNAL) { 586 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, 587 Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); 588 } else { 589 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, 590 Downloads.Impl.VISIBILITY_HIDDEN); 591 } 592 } else { 593 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis); 594 } 595 // copy the control column as is 596 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); 597 598 /* 599 * requests coming from 600 * DownloadManager.addCompletedDownload(String, String, String, 601 * boolean, String, String, long) need special treatment 602 */ 603 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == 604 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 605 // these requests always are marked as 'completed' 606 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS); 607 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, 608 values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES)); 609 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); 610 copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues); 611 copyString(Downloads.Impl._DATA, values, filteredValues); 612 copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues); 613 } else { 614 filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING); 615 filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1); 616 filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); 617 } 618 619 // set lastupdate to current time 620 long lastMod = mSystemFacade.currentTimeMillis(); 621 filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod); 622 623 // use packagename of the caller to set the notification columns 624 String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); 625 String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS); 626 if (pckg != null && (clazz != null || isPublicApi)) { 627 int uid = Binder.getCallingUid(); 628 try { 629 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) { 630 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg); 631 if (clazz != null) { 632 filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz); 633 } 634 } 635 } catch (PackageManager.NameNotFoundException ex) { 636 /* ignored for now */ 637 } 638 } 639 640 // copy some more columns as is 641 copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues); 642 copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues); 643 copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues); 644 copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues); 645 646 // UID, PID columns 647 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED) 648 == PackageManager.PERMISSION_GRANTED) { 649 copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues); 650 } 651 filteredValues.put(Constants.UID, Binder.getCallingUid()); 652 if (Binder.getCallingUid() == 0) { 653 copyInteger(Constants.UID, values, filteredValues); 654 } 655 656 // copy some more columns as is 657 copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, ""); 658 copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, ""); 659 660 // is_visible_in_downloads_ui column 661 if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) { 662 copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues); 663 } else { 664 // by default, make external downloads visible in the UI 665 boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL); 666 filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal); 667 } 668 669 // public api requests and networktypes/roaming columns 670 if (isPublicApi) { 671 copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues); 672 copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues); 673 copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues); 674 copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues); 675 } 676 677 if (Constants.LOGVV) { 678 Log.v(Constants.TAG, "initiating download with UID " 679 + filteredValues.getAsInteger(Constants.UID)); 680 if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) { 681 Log.v(Constants.TAG, "other UID " + 682 filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID)); 683 } 684 } 685 686 long rowID = db.insert(DB_TABLE, null, filteredValues); 687 if (rowID == -1) { 688 Log.d(Constants.TAG, "couldn't insert into downloads database"); 689 return null; 690 } 691 692 insertRequestHeaders(db, rowID, values); 693 notifyContentChanged(uri, match); 694 695 final long token = Binder.clearCallingIdentity(); 696 try { 697 Helpers.scheduleJob(getContext(), rowID); 698 } finally { 699 Binder.restoreCallingIdentity(token); 700 } 701 702 if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD 703 && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) { 704 DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA), 705 values.getAsString(COLUMN_MIME_TYPE)); 706 } 707 708 return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID); 709 } 710 711 /** 712 * Check that the file URI provided for DESTINATION_FILE_URI is valid. 713 */ checkFileUriDestination(ContentValues values)714 private void checkFileUriDestination(ContentValues values) { 715 String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 716 if (fileUri == null) { 717 throw new IllegalArgumentException( 718 "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT"); 719 } 720 Uri uri = Uri.parse(fileUri); 721 String scheme = uri.getScheme(); 722 if (scheme == null || !scheme.equals("file")) { 723 throw new IllegalArgumentException("Not a file URI: " + uri); 724 } 725 final String path = uri.getPath(); 726 if (path == null) { 727 throw new IllegalArgumentException("Invalid file URI: " + uri); 728 } 729 730 final File file; 731 try { 732 file = new File(path).getCanonicalFile(); 733 } catch (IOException e) { 734 throw new SecurityException(e); 735 } 736 737 if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) { 738 // No permissions required for paths belonging to calling package 739 return; 740 } else if (Helpers.isFilenameValidInExternal(getContext(), file)) { 741 // Otherwise we require write permission 742 getContext().enforceCallingOrSelfPermission( 743 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 744 "No permission to write to " + file); 745 746 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 747 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, 748 getCallingPackage()) != AppOpsManager.MODE_ALLOWED) { 749 throw new SecurityException("No permission to write to " + file); 750 } 751 752 } else { 753 throw new SecurityException("Unsupported path " + file); 754 } 755 } 756 757 /** 758 * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to 759 * constraints in the rest of the code. Apps without that may still access this provider through 760 * the public API, but additional restrictions are imposed. We check those restrictions here. 761 * 762 * @param values ContentValues provided to insert() 763 * @throws SecurityException if the caller has insufficient permissions 764 */ checkInsertPermissions(ContentValues values)765 private void checkInsertPermissions(ContentValues values) { 766 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS) 767 == PackageManager.PERMISSION_GRANTED) { 768 return; 769 } 770 771 getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, 772 "INTERNET permission is required to use the download manager"); 773 774 // ensure the request fits within the bounds of a public API request 775 // first copy so we can remove values 776 values = new ContentValues(values); 777 778 // check columns whose values are restricted 779 enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE); 780 781 // validate the destination column 782 if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == 783 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { 784 /* this row is inserted by 785 * DownloadManager.addCompletedDownload(String, String, String, 786 * boolean, String, String, long) 787 */ 788 values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES); 789 values.remove(Downloads.Impl._DATA); 790 values.remove(Downloads.Impl.COLUMN_STATUS); 791 } 792 enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION, 793 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE, 794 Downloads.Impl.DESTINATION_FILE_URI, 795 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD); 796 797 if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION) 798 == PackageManager.PERMISSION_GRANTED) { 799 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 800 Request.VISIBILITY_HIDDEN, 801 Request.VISIBILITY_VISIBLE, 802 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 803 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 804 } else { 805 enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY, 806 Request.VISIBILITY_VISIBLE, 807 Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, 808 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); 809 } 810 811 // remove the rest of the columns that are allowed (with any value) 812 values.remove(Downloads.Impl.COLUMN_URI); 813 values.remove(Downloads.Impl.COLUMN_TITLE); 814 values.remove(Downloads.Impl.COLUMN_DESCRIPTION); 815 values.remove(Downloads.Impl.COLUMN_MIME_TYPE); 816 values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert() 817 values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert() 818 values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); 819 values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING); 820 values.remove(Downloads.Impl.COLUMN_ALLOW_METERED); 821 values.remove(Downloads.Impl.COLUMN_FLAGS); 822 values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI); 823 values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED); 824 values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE); 825 Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator(); 826 while (iterator.hasNext()) { 827 String key = iterator.next().getKey(); 828 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 829 iterator.remove(); 830 } 831 } 832 833 // any extra columns are extraneous and disallowed 834 if (values.size() > 0) { 835 StringBuilder error = new StringBuilder("Invalid columns in request: "); 836 boolean first = true; 837 for (Map.Entry<String, Object> entry : values.valueSet()) { 838 if (!first) { 839 error.append(", "); 840 } 841 error.append(entry.getKey()); 842 } 843 throw new SecurityException(error.toString()); 844 } 845 } 846 847 /** 848 * Remove column from values, and throw a SecurityException if the value isn't within the 849 * specified allowedValues. 850 */ enforceAllowedValues(ContentValues values, String column, Object... allowedValues)851 private void enforceAllowedValues(ContentValues values, String column, 852 Object... allowedValues) { 853 Object value = values.get(column); 854 values.remove(column); 855 for (Object allowedValue : allowedValues) { 856 if (value == null && allowedValue == null) { 857 return; 858 } 859 if (value != null && value.equals(allowedValue)) { 860 return; 861 } 862 } 863 throw new SecurityException("Invalid value for " + column + ": " + value); 864 } 865 queryCleared(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort)866 private Cursor queryCleared(Uri uri, String[] projection, String selection, 867 String[] selectionArgs, String sort) { 868 final long token = Binder.clearCallingIdentity(); 869 try { 870 return query(uri, projection, selection, selectionArgs, sort); 871 } finally { 872 Binder.restoreCallingIdentity(token); 873 } 874 } 875 876 /** 877 * Starts a database query 878 */ 879 @Override query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort)880 public Cursor query(final Uri uri, String[] projection, 881 final String selection, final String[] selectionArgs, 882 final String sort) { 883 884 Helpers.validateSelection(selection, sAppReadableColumnsSet); 885 886 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 887 888 int match = sURIMatcher.match(uri); 889 if (match == -1) { 890 if (Constants.LOGV) { 891 Log.v(Constants.TAG, "querying unknown URI: " + uri); 892 } 893 throw new IllegalArgumentException("Unknown URI: " + uri); 894 } 895 896 if (match == REQUEST_HEADERS_URI) { 897 if (projection != null || selection != null || sort != null) { 898 throw new UnsupportedOperationException("Request header queries do not support " 899 + "projections, selections or sorting"); 900 } 901 return queryRequestHeaders(db, uri); 902 } 903 904 SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match); 905 906 if (shouldRestrictVisibility()) { 907 if (projection == null) { 908 projection = sAppReadableColumnsArray.clone(); 909 } else { 910 // check the validity of the columns in projection 911 for (int i = 0; i < projection.length; ++i) { 912 if (!sAppReadableColumnsSet.contains(projection[i]) && 913 !downloadManagerColumnsList.contains(projection[i])) { 914 throw new IllegalArgumentException( 915 "column " + projection[i] + " is not allowed in queries"); 916 } 917 } 918 } 919 920 for (int i = 0; i < projection.length; i++) { 921 final String newColumn = sColumnsMap.get(projection[i]); 922 if (newColumn != null) { 923 projection[i] = newColumn; 924 } 925 } 926 } 927 928 if (Constants.LOGVV) { 929 logVerboseQueryInfo(projection, selection, selectionArgs, sort, db); 930 } 931 932 Cursor ret = db.query(DB_TABLE, projection, fullSelection.getSelection(), 933 fullSelection.getParameters(), null, null, sort); 934 935 if (ret != null) { 936 ret.setNotificationUri(getContext().getContentResolver(), uri); 937 if (Constants.LOGVV) { 938 Log.v(Constants.TAG, 939 "created cursor " + ret + " on behalf of " + Binder.getCallingPid()); 940 } 941 } else { 942 if (Constants.LOGV) { 943 Log.v(Constants.TAG, "query failed in downloads database"); 944 } 945 } 946 947 return ret; 948 } 949 logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db)950 private void logVerboseQueryInfo(String[] projection, final String selection, 951 final String[] selectionArgs, final String sort, SQLiteDatabase db) { 952 java.lang.StringBuilder sb = new java.lang.StringBuilder(); 953 sb.append("starting query, database is "); 954 if (db != null) { 955 sb.append("not "); 956 } 957 sb.append("null; "); 958 if (projection == null) { 959 sb.append("projection is null; "); 960 } else if (projection.length == 0) { 961 sb.append("projection is empty; "); 962 } else { 963 for (int i = 0; i < projection.length; ++i) { 964 sb.append("projection["); 965 sb.append(i); 966 sb.append("] is "); 967 sb.append(projection[i]); 968 sb.append("; "); 969 } 970 } 971 sb.append("selection is "); 972 sb.append(selection); 973 sb.append("; "); 974 if (selectionArgs == null) { 975 sb.append("selectionArgs is null; "); 976 } else if (selectionArgs.length == 0) { 977 sb.append("selectionArgs is empty; "); 978 } else { 979 for (int i = 0; i < selectionArgs.length; ++i) { 980 sb.append("selectionArgs["); 981 sb.append(i); 982 sb.append("] is "); 983 sb.append(selectionArgs[i]); 984 sb.append("; "); 985 } 986 } 987 sb.append("sort is "); 988 sb.append(sort); 989 sb.append("."); 990 Log.v(Constants.TAG, sb.toString()); 991 } 992 getDownloadIdFromUri(final Uri uri)993 private String getDownloadIdFromUri(final Uri uri) { 994 return uri.getPathSegments().get(1); 995 } 996 997 /** 998 * Insert request headers for a download into the DB. 999 */ insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values)1000 private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) { 1001 ContentValues rowValues = new ContentValues(); 1002 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId); 1003 for (Map.Entry<String, Object> entry : values.valueSet()) { 1004 String key = entry.getKey(); 1005 if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { 1006 String headerLine = entry.getValue().toString(); 1007 if (!headerLine.contains(":")) { 1008 throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine); 1009 } 1010 String[] parts = headerLine.split(":", 2); 1011 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim()); 1012 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim()); 1013 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues); 1014 } 1015 } 1016 } 1017 1018 /** 1019 * Handle a query for the custom request headers registered for a download. 1020 */ queryRequestHeaders(SQLiteDatabase db, Uri uri)1021 private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) { 1022 String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" 1023 + getDownloadIdFromUri(uri); 1024 String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER, 1025 Downloads.Impl.RequestHeaders.COLUMN_VALUE}; 1026 return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where, 1027 null, null, null, null); 1028 } 1029 1030 /** 1031 * Delete request headers for downloads matching the given query. 1032 */ deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs)1033 private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) { 1034 String[] projection = new String[] {Downloads.Impl._ID}; 1035 Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null); 1036 try { 1037 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 1038 long id = cursor.getLong(0); 1039 String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id; 1040 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null); 1041 } 1042 } finally { 1043 cursor.close(); 1044 } 1045 } 1046 1047 /** 1048 * @return true if we should restrict the columns readable by this caller 1049 */ shouldRestrictVisibility()1050 private boolean shouldRestrictVisibility() { 1051 int callingUid = Binder.getCallingUid(); 1052 return Binder.getCallingPid() != Process.myPid() && 1053 callingUid != mSystemUid && 1054 callingUid != mDefContainerUid; 1055 } 1056 1057 /** 1058 * Updates a row in the database 1059 */ 1060 @Override update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)1061 public int update(final Uri uri, final ContentValues values, 1062 final String where, final String[] whereArgs) { 1063 1064 Helpers.validateSelection(where, sAppReadableColumnsSet); 1065 1066 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1067 1068 int count; 1069 boolean updateSchedule = false; 1070 1071 ContentValues filteredValues; 1072 if (Binder.getCallingPid() != Process.myPid()) { 1073 filteredValues = new ContentValues(); 1074 copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); 1075 copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues); 1076 Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL); 1077 if (i != null) { 1078 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i); 1079 updateSchedule = true; 1080 } 1081 1082 copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); 1083 copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues); 1084 copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues); 1085 copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues); 1086 copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues); 1087 } else { 1088 filteredValues = values; 1089 String filename = values.getAsString(Downloads.Impl._DATA); 1090 if (filename != null) { 1091 Cursor c = null; 1092 try { 1093 c = query(uri, new String[] 1094 { Downloads.Impl.COLUMN_TITLE }, null, null, null); 1095 if (!c.moveToFirst() || c.getString(0).isEmpty()) { 1096 values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName()); 1097 } 1098 } finally { 1099 IoUtils.closeQuietly(c); 1100 } 1101 } 1102 1103 Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS); 1104 boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING; 1105 boolean isUserBypassingSizeLimit = 1106 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); 1107 if (isRestart || isUserBypassingSizeLimit) { 1108 updateSchedule = true; 1109 } 1110 } 1111 1112 int match = sURIMatcher.match(uri); 1113 switch (match) { 1114 case MY_DOWNLOADS: 1115 case MY_DOWNLOADS_ID: 1116 case ALL_DOWNLOADS: 1117 case ALL_DOWNLOADS_ID: 1118 if (filteredValues.size() == 0) { 1119 count = 0; 1120 break; 1121 } 1122 1123 final SqlSelection selection = getWhereClause(uri, where, whereArgs, match); 1124 count = db.update(DB_TABLE, filteredValues, selection.getSelection(), 1125 selection.getParameters()); 1126 if (updateSchedule) { 1127 final long token = Binder.clearCallingIdentity(); 1128 try { 1129 try (Cursor cursor = db.query(DB_TABLE, new String[] { _ID }, 1130 selection.getSelection(), selection.getParameters(), 1131 null, null, null)) { 1132 while (cursor.moveToNext()) { 1133 Helpers.scheduleJob(getContext(), cursor.getInt(0)); 1134 } 1135 } 1136 } finally { 1137 Binder.restoreCallingIdentity(token); 1138 } 1139 } 1140 break; 1141 1142 default: 1143 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri); 1144 throw new UnsupportedOperationException("Cannot update URI: " + uri); 1145 } 1146 1147 notifyContentChanged(uri, match); 1148 return count; 1149 } 1150 1151 /** 1152 * Notify of a change through both URIs (/my_downloads and /all_downloads) 1153 * @param uri either URI for the changed download(s) 1154 * @param uriMatch the match ID from {@link #sURIMatcher} 1155 */ notifyContentChanged(final Uri uri, int uriMatch)1156 private void notifyContentChanged(final Uri uri, int uriMatch) { 1157 Long downloadId = null; 1158 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) { 1159 downloadId = Long.parseLong(getDownloadIdFromUri(uri)); 1160 } 1161 for (Uri uriToNotify : BASE_URIS) { 1162 if (downloadId != null) { 1163 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId); 1164 } 1165 getContext().getContentResolver().notifyChange(uriToNotify, null); 1166 } 1167 } 1168 getWhereClause(final Uri uri, final String where, final String[] whereArgs, int uriMatch)1169 private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs, 1170 int uriMatch) { 1171 SqlSelection selection = new SqlSelection(); 1172 selection.appendClause(where, whereArgs); 1173 if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID || 1174 uriMatch == PUBLIC_DOWNLOAD_ID) { 1175 selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri)); 1176 } 1177 if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID) 1178 && getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ALL) 1179 != PackageManager.PERMISSION_GRANTED) { 1180 selection.appendClause( 1181 Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?", 1182 Binder.getCallingUid(), Binder.getCallingUid()); 1183 } 1184 return selection; 1185 } 1186 1187 /** 1188 * Deletes a row in the database 1189 */ 1190 @Override delete(final Uri uri, final String where, final String[] whereArgs)1191 public int delete(final Uri uri, final String where, final String[] whereArgs) { 1192 if (shouldRestrictVisibility()) { 1193 Helpers.validateSelection(where, sAppReadableColumnsSet); 1194 } 1195 1196 final JobScheduler scheduler = getContext().getSystemService(JobScheduler.class); 1197 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1198 int count; 1199 int match = sURIMatcher.match(uri); 1200 switch (match) { 1201 case MY_DOWNLOADS: 1202 case MY_DOWNLOADS_ID: 1203 case ALL_DOWNLOADS: 1204 case ALL_DOWNLOADS_ID: 1205 final SqlSelection selection = getWhereClause(uri, where, whereArgs, match); 1206 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters()); 1207 1208 try (Cursor cursor = db.query(DB_TABLE, new String[] { 1209 _ID, _DATA, COLUMN_MEDIAPROVIDER_URI 1210 }, selection.getSelection(), selection.getParameters(), null, null, null)) { 1211 while (cursor.moveToNext()) { 1212 final long id = cursor.getLong(0); 1213 scheduler.cancel((int) id); 1214 1215 DownloadStorageProvider.onDownloadProviderDelete(getContext(), id); 1216 1217 final String path = cursor.getString(1); 1218 if (!TextUtils.isEmpty(path)) { 1219 try { 1220 final File file = new File(path).getCanonicalFile(); 1221 if (Helpers.isFilenameValid(getContext(), file)) { 1222 Log.v(Constants.TAG, 1223 "Deleting " + file + " via provider delete"); 1224 file.delete(); 1225 } 1226 } catch (IOException ignored) { 1227 } 1228 } 1229 1230 final String mediaUri = cursor.getString(2); 1231 if (!TextUtils.isEmpty(mediaUri)) { 1232 final long token = Binder.clearCallingIdentity(); 1233 try { 1234 getContext().getContentResolver().delete(Uri.parse(mediaUri), null, 1235 null); 1236 } finally { 1237 Binder.restoreCallingIdentity(token); 1238 } 1239 } 1240 } 1241 } 1242 1243 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters()); 1244 break; 1245 1246 default: 1247 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri); 1248 throw new UnsupportedOperationException("Cannot delete URI: " + uri); 1249 } 1250 notifyContentChanged(uri, match); 1251 return count; 1252 } 1253 1254 /** 1255 * Remotely opens a file 1256 */ 1257 @Override openFile(final Uri uri, String mode)1258 public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException { 1259 if (Constants.LOGVV) { 1260 logVerboseOpenFileInfo(uri, mode); 1261 } 1262 1263 final Cursor cursor = queryCleared(uri, new String[] { 1264 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS, 1265 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null, 1266 null, null); 1267 final String path; 1268 final boolean shouldScan; 1269 try { 1270 int count = (cursor != null) ? cursor.getCount() : 0; 1271 if (count != 1) { 1272 // If there is not exactly one result, throw an appropriate exception. 1273 if (count == 0) { 1274 throw new FileNotFoundException("No entry for " + uri); 1275 } 1276 throw new FileNotFoundException("Multiple items at " + uri); 1277 } 1278 1279 if (cursor.moveToFirst()) { 1280 final int status = cursor.getInt(1); 1281 final int destination = cursor.getInt(2); 1282 final int mediaScanned = cursor.getInt(3); 1283 1284 path = cursor.getString(0); 1285 shouldScan = Downloads.Impl.isStatusSuccess(status) && ( 1286 destination == Downloads.Impl.DESTINATION_EXTERNAL 1287 || destination == Downloads.Impl.DESTINATION_FILE_URI 1288 || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 1289 && mediaScanned != 2; 1290 } else { 1291 throw new FileNotFoundException("Failed moveToFirst"); 1292 } 1293 } finally { 1294 IoUtils.closeQuietly(cursor); 1295 } 1296 1297 if (path == null) { 1298 throw new FileNotFoundException("No filename found."); 1299 } 1300 1301 final File file; 1302 try { 1303 file = new File(path).getCanonicalFile(); 1304 } catch (IOException e) { 1305 throw new FileNotFoundException(e.getMessage()); 1306 } 1307 1308 if (!Helpers.isFilenameValid(getContext(), file)) { 1309 throw new FileNotFoundException("Invalid file: " + file); 1310 } 1311 1312 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 1313 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) { 1314 return ParcelFileDescriptor.open(file, pfdMode); 1315 } else { 1316 try { 1317 // When finished writing, update size and timestamp 1318 return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(), 1319 new OnCloseListener() { 1320 @Override 1321 public void onClose(IOException e) { 1322 final ContentValues values = new ContentValues(); 1323 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length()); 1324 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, 1325 System.currentTimeMillis()); 1326 update(uri, values, null, null); 1327 1328 if (shouldScan) { 1329 final Intent intent = new Intent( 1330 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 1331 intent.setData(Uri.fromFile(file)); 1332 getContext().sendBroadcast(intent); 1333 } 1334 } 1335 }); 1336 } catch (IOException e) { 1337 throw new FileNotFoundException("Failed to open for writing: " + e); 1338 } 1339 } 1340 } 1341 1342 @Override 1343 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1344 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120); 1345 1346 pw.println("Downloads updated in last hour:"); 1347 pw.increaseIndent(); 1348 1349 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1350 final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS; 1351 final Cursor cursor = db.query(DB_TABLE, null, 1352 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null, 1353 Downloads.Impl._ID + " ASC"); 1354 try { 1355 final String[] cols = cursor.getColumnNames(); 1356 final int idCol = cursor.getColumnIndex(BaseColumns._ID); 1357 while (cursor.moveToNext()) { 1358 pw.println("Download #" + cursor.getInt(idCol) + ":"); 1359 pw.increaseIndent(); 1360 for (int i = 0; i < cols.length; i++) { 1361 // Omit sensitive data when dumping 1362 if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) { 1363 continue; 1364 } 1365 pw.printPair(cols[i], cursor.getString(i)); 1366 } 1367 pw.println(); 1368 pw.decreaseIndent(); 1369 } 1370 } finally { 1371 cursor.close(); 1372 } 1373 1374 pw.decreaseIndent(); 1375 } 1376 1377 private void logVerboseOpenFileInfo(Uri uri, String mode) { 1378 Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode 1379 + ", uid: " + Binder.getCallingUid()); 1380 Cursor cursor = query(Downloads.Impl.CONTENT_URI, 1381 new String[] { "_id" }, null, null, "_id"); 1382 if (cursor == null) { 1383 Log.v(Constants.TAG, "null cursor in openFile"); 1384 } else { 1385 try { 1386 if (!cursor.moveToFirst()) { 1387 Log.v(Constants.TAG, "empty cursor in openFile"); 1388 } else { 1389 do { 1390 Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available"); 1391 } while(cursor.moveToNext()); 1392 } 1393 } finally { 1394 cursor.close(); 1395 } 1396 } 1397 cursor = query(uri, new String[] { "_data" }, null, null, null); 1398 if (cursor == null) { 1399 Log.v(Constants.TAG, "null cursor in openFile"); 1400 } else { 1401 try { 1402 if (!cursor.moveToFirst()) { 1403 Log.v(Constants.TAG, "empty cursor in openFile"); 1404 } else { 1405 String filename = cursor.getString(0); 1406 Log.v(Constants.TAG, "filename in openFile: " + filename); 1407 if (new java.io.File(filename).isFile()) { 1408 Log.v(Constants.TAG, "file exists in openFile"); 1409 } 1410 } 1411 } finally { 1412 cursor.close(); 1413 } 1414 } 1415 } 1416 1417 private static final void copyInteger(String key, ContentValues from, ContentValues to) { 1418 Integer i = from.getAsInteger(key); 1419 if (i != null) { 1420 to.put(key, i); 1421 } 1422 } 1423 1424 private static final void copyBoolean(String key, ContentValues from, ContentValues to) { 1425 Boolean b = from.getAsBoolean(key); 1426 if (b != null) { 1427 to.put(key, b); 1428 } 1429 } 1430 1431 private static final void copyString(String key, ContentValues from, ContentValues to) { 1432 String s = from.getAsString(key); 1433 if (s != null) { 1434 to.put(key, s); 1435 } 1436 } 1437 1438 private static final void copyStringWithDefault(String key, ContentValues from, 1439 ContentValues to, String defaultValue) { 1440 copyString(key, from, to); 1441 if (!to.containsKey(key)) { 1442 to.put(key, defaultValue); 1443 } 1444 } 1445 } 1446