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