1 /* 2 * Copyright (C) 2008 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 android.net; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.ParcelFileDescriptor; 26 import android.os.SystemClock; 27 import android.provider.BaseColumns; 28 import android.util.Log; 29 30 import java.io.File; 31 import java.io.FileNotFoundException; 32 import java.io.IOException; 33 import java.io.File; 34 import java.io.InputStream; 35 36 /** 37 * The Download Manager 38 * 39 * @hide 40 */ 41 public final class Downloads { 42 43 44 /** 45 * Download status codes 46 */ 47 48 /** 49 * This download hasn't started yet 50 */ 51 public static final int STATUS_PENDING = 190; 52 53 /** 54 * This download has started 55 */ 56 public static final int STATUS_RUNNING = 192; 57 58 /** 59 * This download has successfully completed. 60 * Warning: there might be other status values that indicate success 61 * in the future. 62 * Use isSucccess() to capture the entire category. 63 */ 64 public static final int STATUS_SUCCESS = 200; 65 66 /** 67 * This download can't be performed because the content type cannot be 68 * handled. 69 */ 70 public static final int STATUS_NOT_ACCEPTABLE = 406; 71 72 /** 73 * This download has completed with an error. 74 * Warning: there will be other status values that indicate errors in 75 * the future. Use isStatusError() to capture the entire category. 76 */ 77 public static final int STATUS_UNKNOWN_ERROR = 491; 78 79 /** 80 * This download couldn't be completed because of an HTTP 81 * redirect response that the download manager couldn't 82 * handle. 83 */ 84 public static final int STATUS_UNHANDLED_REDIRECT = 493; 85 86 /** 87 * This download couldn't be completed due to insufficient storage 88 * space. Typically, this is because the SD card is full. 89 */ 90 public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; 91 92 /** 93 * This download couldn't be completed because no external storage 94 * device was found. Typically, this is because the SD card is not 95 * mounted. 96 */ 97 public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; 98 99 /** 100 * Returns whether the status is a success (i.e. 2xx). 101 */ 102 public static boolean isStatusSuccess(int status) { 103 return (status >= 200 && status < 300); 104 } 105 106 /** 107 * Returns whether the status is an error (i.e. 4xx or 5xx). 108 */ 109 public static boolean isStatusError(int status) { 110 return (status >= 400 && status < 600); 111 } 112 113 /** 114 * Download destinations 115 */ 116 117 /** 118 * This download will be saved to the external storage. This is the 119 * default behavior, and should be used for any file that the user 120 * can freely access, copy, delete. Even with that destination, 121 * unencrypted DRM files are saved in secure internal storage. 122 * Downloads to the external destination only write files for which 123 * there is a registered handler. The resulting files are accessible 124 * by filename to all applications. 125 */ 126 public static final int DOWNLOAD_DESTINATION_EXTERNAL = 1; 127 128 /** 129 * This download will be saved to the download manager's private 130 * partition. This is the behavior used by applications that want to 131 * download private files that are used and deleted soon after they 132 * get downloaded. All file types are allowed, and only the initiating 133 * application can access the file (indirectly through a content 134 * provider). This requires the 135 * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission. 136 */ 137 public static final int DOWNLOAD_DESTINATION_CACHE = 2; 138 139 /** 140 * This download will be saved to the download manager's private 141 * partition and will be purged as necessary to make space. This is 142 * for private files (similar to CACHE_PARTITION) that aren't deleted 143 * immediately after they are used, and are kept around by the download 144 * manager as long as space is available. 145 */ 146 public static final int DOWNLOAD_DESTINATION_CACHE_PURGEABLE = 3; 147 148 149 /** 150 * An invalid download id 151 */ 152 public static final long DOWNLOAD_ID_INVALID = -1; 153 154 155 /** 156 * Broadcast Action: this is sent by the download manager to the app 157 * that had initiated a download when that download completes. The 158 * download's content: uri is specified in the intent's data. 159 */ 160 public static final String ACTION_DOWNLOAD_COMPLETED = 161 "android.intent.action.DOWNLOAD_COMPLETED"; 162 163 /** 164 * If extras are specified when requesting a download they will be provided in the intent that 165 * is sent to the specified class and package when a download has finished. 166 * <P>Type: TEXT</P> 167 * <P>Owner can Init</P> 168 */ 169 public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras"; 170 171 172 /** 173 * Status class for a download 174 */ 175 public static final class StatusInfo { 176 public boolean completed = false; 177 /** The filename of the active download. */ 178 public String filename = null; 179 /** An opaque id for the download */ 180 public long id = DOWNLOAD_ID_INVALID; 181 /** An opaque status code for the download */ 182 public int statusCode = -1; 183 /** Approximate number of bytes downloaded so far, for debugging purposes. */ 184 public long bytesSoFar = -1; 185 186 /** 187 * Returns whether the download is completed 188 * @return a boolean whether the download is complete. 189 */ 190 public boolean isComplete() { 191 return android.provider.Downloads.Impl.isStatusCompleted(statusCode); 192 } 193 194 /** 195 * Returns whether the download is successful 196 * @return a boolean whether the download is successful. 197 */ 198 public boolean isSuccessful() { 199 return android.provider.Downloads.Impl.isStatusCompleted(statusCode); 200 } 201 } 202 203 /** 204 * Class to access initiate and query download by server uri 205 */ 206 public static final class ByUri extends DownloadBase { 207 /** @hide */ 208 private ByUri() {} 209 210 /** 211 * Query where clause by app data. 212 * @hide 213 */ 214 private static final String QUERY_WHERE_APP_DATA_CLAUSE = 215 android.provider.Downloads.Impl.COLUMN_APP_DATA + "=?"; 216 217 /** 218 * Gets a Cursor pointing to the download(s) of the current system update. 219 * @hide 220 */ 221 private static final Cursor getCurrentOtaDownloads(Context context, String url) { 222 return context.getContentResolver().query( 223 android.provider.Downloads.Impl.CONTENT_URI, 224 DOWNLOADS_PROJECTION, 225 QUERY_WHERE_APP_DATA_CLAUSE, 226 new String[] {url}, 227 null); 228 } 229 230 /** 231 * Returns a StatusInfo with the result of trying to download the 232 * given URL. Returns null if no attempts have been made. 233 */ 234 public static final StatusInfo getStatus( 235 Context context, 236 String url, 237 long redownload_threshold) { 238 StatusInfo result = null; 239 boolean hasFailedDownload = false; 240 long failedDownloadModificationTime = 0; 241 Cursor c = getCurrentOtaDownloads(context, url); 242 try { 243 while (c != null && c.moveToNext()) { 244 if (result == null) { 245 result = new StatusInfo(); 246 } 247 int status = getStatusOfDownload(c, redownload_threshold); 248 if (status == STATUS_DOWNLOADING_UPDATE || 249 status == STATUS_DOWNLOADED_UPDATE) { 250 result.completed = (status == STATUS_DOWNLOADED_UPDATE); 251 result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME); 252 result.id = c.getLong(DOWNLOADS_COLUMN_ID); 253 result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); 254 result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); 255 return result; 256 } 257 258 long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); 259 if (hasFailedDownload && 260 modTime < failedDownloadModificationTime) { 261 // older than the one already in result; skip it. 262 continue; 263 } 264 265 hasFailedDownload = true; 266 failedDownloadModificationTime = modTime; 267 result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); 268 result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); 269 } 270 } finally { 271 if (c != null) { 272 c.close(); 273 } 274 } 275 return result; 276 } 277 278 /** 279 * Query where clause for general querying. 280 */ 281 private static final String QUERY_WHERE_CLAUSE = 282 android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + "=? AND " + 283 android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS + "=?"; 284 285 /** 286 * Delete all the downloads for a package/class pair. 287 */ 288 public static final void removeAllDownloadsByPackage( 289 Context context, 290 String notification_package, 291 String notification_class) { 292 context.getContentResolver().delete( 293 android.provider.Downloads.Impl.CONTENT_URI, 294 QUERY_WHERE_CLAUSE, 295 new String[] { notification_package, notification_class }); 296 } 297 298 /** 299 * The column for the id in the Cursor returned by 300 * getProgressCursor() 301 */ 302 public static final int getProgressColumnId() { 303 return 0; 304 } 305 306 /** 307 * The column for the current byte count in the Cursor returned by 308 * getProgressCursor() 309 */ 310 public static final int getProgressColumnCurrentBytes() { 311 return 1; 312 } 313 314 /** 315 * The column for the total byte count in the Cursor returned by 316 * getProgressCursor() 317 */ 318 public static final int getProgressColumnTotalBytes() { 319 return 2; 320 } 321 322 /** @hide */ 323 private static final String[] PROJECTION = { 324 BaseColumns._ID, 325 android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES, 326 android.provider.Downloads.Impl.COLUMN_TOTAL_BYTES 327 }; 328 329 /** 330 * Returns a Cursor representing the progress of the download identified by the ID. 331 */ 332 public static final Cursor getProgressCursor(Context context, long id) { 333 Uri downloadUri = Uri.withAppendedPath(android.provider.Downloads.Impl.CONTENT_URI, 334 String.valueOf(id)); 335 return context.getContentResolver().query(downloadUri, PROJECTION, null, null, null); 336 } 337 } 338 339 /** 340 * Class to access downloads by opaque download id 341 */ 342 public static final class ById extends DownloadBase { 343 /** @hide */ 344 private ById() {} 345 346 /** 347 * Get the mime tupe of the download specified by the download id 348 */ 349 public static String getMimeTypeForId(Context context, long downloadId) { 350 ContentResolver cr = context.getContentResolver(); 351 352 String mimeType = null; 353 Cursor downloadCursor = null; 354 355 try { 356 Uri downloadUri = getDownloadUri(downloadId); 357 358 downloadCursor = cr.query( 359 downloadUri, new String[]{android.provider.Downloads.Impl.COLUMN_MIME_TYPE}, 360 null, null, null); 361 if (downloadCursor.moveToNext()) { 362 mimeType = downloadCursor.getString(0); 363 } 364 } finally { 365 if (downloadCursor != null) downloadCursor.close(); 366 } 367 return mimeType; 368 } 369 370 /** 371 * Delete a download by Id 372 */ 373 public static void deleteDownload(Context context, long downloadId) { 374 ContentResolver cr = context.getContentResolver(); 375 376 String mimeType = null; 377 378 Uri downloadUri = getDownloadUri(downloadId); 379 380 cr.delete(downloadUri, null, null); 381 } 382 383 /** 384 * Open a filedescriptor to a particular download 385 */ 386 public static ParcelFileDescriptor openDownload( 387 Context context, long downloadId, String mode) 388 throws FileNotFoundException 389 { 390 ContentResolver cr = context.getContentResolver(); 391 392 String mimeType = null; 393 394 Uri downloadUri = getDownloadUri(downloadId); 395 396 return cr.openFileDescriptor(downloadUri, mode); 397 } 398 399 /** 400 * Open a stream to a particular download 401 */ 402 public static InputStream openDownloadStream(Context context, long downloadId) 403 throws FileNotFoundException, IOException 404 { 405 ContentResolver cr = context.getContentResolver(); 406 407 String mimeType = null; 408 409 Uri downloadUri = getDownloadUri(downloadId); 410 411 return cr.openInputStream(downloadUri); 412 } 413 414 private static Uri getDownloadUri(long downloadId) { 415 return Uri.parse(android.provider.Downloads.Impl.CONTENT_URI + "/" + downloadId); 416 } 417 418 /** 419 * Returns a StatusInfo with the result of trying to download the 420 * given URL. Returns null if no attempts have been made. 421 */ 422 public static final StatusInfo getStatus( 423 Context context, 424 long downloadId) { 425 StatusInfo result = null; 426 boolean hasFailedDownload = false; 427 long failedDownloadModificationTime = 0; 428 429 Uri downloadUri = getDownloadUri(downloadId); 430 431 ContentResolver cr = context.getContentResolver(); 432 433 Cursor c = cr.query( 434 downloadUri, DOWNLOADS_PROJECTION, null /* selection */, null /* selection args */, 435 null /* sort order */); 436 try { 437 if (!c.moveToNext()) { 438 return result; 439 } 440 441 if (result == null) { 442 result = new StatusInfo(); 443 } 444 int status = getStatusOfDownload(c,0); 445 if (status == STATUS_DOWNLOADING_UPDATE || 446 status == STATUS_DOWNLOADED_UPDATE) { 447 result.completed = (status == STATUS_DOWNLOADED_UPDATE); 448 result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME); 449 result.id = c.getLong(DOWNLOADS_COLUMN_ID); 450 result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); 451 result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); 452 return result; 453 } 454 455 long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); 456 457 result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); 458 result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); 459 } finally { 460 if (c != null) { 461 c.close(); 462 } 463 } 464 return result; 465 } 466 } 467 468 469 /** 470 * Base class with common functionality for the various download classes 471 */ 472 public static class DownloadBase { 473 /** @hide */ 474 DownloadBase() {} 475 476 /** 477 * Initiate a download where the download will be tracked by its URI. 478 */ 479 public static long startDownloadByUri( 480 Context context, 481 String url, 482 String cookieData, 483 boolean showDownload, 484 int downloadDestination, 485 boolean allowRoaming, 486 boolean skipIntegrityCheck, 487 String title, 488 String notification_package, 489 String notification_class, 490 String notification_extras) { 491 ContentResolver cr = context.getContentResolver(); 492 493 // Tell download manager to start downloading update. 494 ContentValues values = new ContentValues(); 495 values.put(android.provider.Downloads.Impl.COLUMN_URI, url); 496 values.put(android.provider.Downloads.Impl.COLUMN_COOKIE_DATA, cookieData); 497 values.put(android.provider.Downloads.Impl.COLUMN_VISIBILITY, 498 showDownload ? android.provider.Downloads.Impl.VISIBILITY_VISIBLE 499 : android.provider.Downloads.Impl.VISIBILITY_HIDDEN); 500 if (title != null) { 501 values.put(android.provider.Downloads.Impl.COLUMN_TITLE, title); 502 } 503 values.put(android.provider.Downloads.Impl.COLUMN_APP_DATA, url); 504 505 506 // NOTE: destination should be seperated from whether the download 507 // can happen when roaming 508 int destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; 509 switch (downloadDestination) { 510 case DOWNLOAD_DESTINATION_EXTERNAL: 511 destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; 512 break; 513 case DOWNLOAD_DESTINATION_CACHE: 514 if (allowRoaming) { 515 destination = android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION; 516 } else { 517 destination = 518 android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING; 519 } 520 break; 521 case DOWNLOAD_DESTINATION_CACHE_PURGEABLE: 522 destination = 523 android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE; 524 break; 525 } 526 values.put(android.provider.Downloads.Impl.COLUMN_DESTINATION, destination); 527 values.put(android.provider.Downloads.Impl.COLUMN_NO_INTEGRITY, 528 skipIntegrityCheck); // Don't check ETag 529 if (notification_package != null && notification_class != null) { 530 values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, 531 notification_package); 532 values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS, 533 notification_class); 534 535 if (notification_extras != null) { 536 values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, 537 notification_extras); 538 } 539 } 540 541 Uri downloadUri = cr.insert(android.provider.Downloads.Impl.CONTENT_URI, values); 542 543 long downloadId = DOWNLOAD_ID_INVALID; 544 if (downloadUri != null) { 545 downloadId = Long.parseLong(downloadUri.getLastPathSegment()); 546 } 547 return downloadId; 548 } 549 } 550 551 /** @hide */ 552 private static final int STATUS_INVALID = 0; 553 /** @hide */ 554 private static final int STATUS_DOWNLOADING_UPDATE = 3; 555 /** @hide */ 556 private static final int STATUS_DOWNLOADED_UPDATE = 4; 557 558 /** 559 * Column projection for the query to the download manager. This must match 560 * with the constants DOWNLOADS_COLUMN_*. 561 * @hide 562 */ 563 private static final String[] DOWNLOADS_PROJECTION = { 564 BaseColumns._ID, 565 android.provider.Downloads.Impl.COLUMN_APP_DATA, 566 android.provider.Downloads.Impl.COLUMN_STATUS, 567 android.provider.Downloads.Impl._DATA, 568 android.provider.Downloads.Impl.COLUMN_LAST_MODIFICATION, 569 android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES, 570 }; 571 572 /** 573 * The column index for the ID. 574 * @hide 575 */ 576 private static final int DOWNLOADS_COLUMN_ID = 0; 577 /** 578 * The column index for the URI. 579 * @hide 580 */ 581 private static final int DOWNLOADS_COLUMN_URI = 1; 582 /** 583 * The column index for the status code. 584 * @hide 585 */ 586 private static final int DOWNLOADS_COLUMN_STATUS = 2; 587 /** 588 * The column index for the filename. 589 * @hide 590 */ 591 private static final int DOWNLOADS_COLUMN_FILENAME = 3; 592 /** 593 * The column index for the last modification time. 594 * @hide 595 */ 596 private static final int DOWNLOADS_COLUMN_LAST_MODIFICATION = 4; 597 /** 598 * The column index for the number of bytes downloaded so far. 599 * @hide 600 */ 601 private static final int DOWNLOADS_COLUMN_CURRENT_BYTES = 5; 602 603 /** 604 * Gets the status of a download. 605 * 606 * @param c A Cursor pointing to a download. The URL column is assumed to be valid. 607 * @return The status of the download. 608 * @hide 609 */ 610 private static final int getStatusOfDownload( Cursor c, long redownload_threshold) { 611 int status = c.getInt(DOWNLOADS_COLUMN_STATUS); 612 long realtime = SystemClock.elapsedRealtime(); 613 614 // TODO(dougz): special handling of 503, 404? (eg, special 615 // explanatory messages to user) 616 617 if (!android.provider.Downloads.Impl.isStatusCompleted(status)) { 618 // Check if it's stuck 619 long modified = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); 620 long now = System.currentTimeMillis(); 621 if (now < modified || now - modified > redownload_threshold) { 622 return STATUS_INVALID; 623 } 624 625 return STATUS_DOWNLOADING_UPDATE; 626 } 627 628 if (android.provider.Downloads.Impl.isStatusError(status)) { 629 return STATUS_INVALID; 630 } 631 632 String filename = c.getString(DOWNLOADS_COLUMN_FILENAME); 633 if (filename == null) { 634 return STATUS_INVALID; 635 } 636 637 return STATUS_DOWNLOADED_UPDATE; 638 } 639 640 641 /** 642 * @hide 643 */ 644 private Downloads() {} 645 } 646