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