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.isStatusSuccess(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(downloadUri, DOWNLOADS_PROJECTION, null /* selection */, 434 null /* selection args */, null /* sort order */); 435 try { 436 if (c == null || !c.moveToNext()) { 437 return result; 438 } 439 440 if (result == null) { 441 result = new StatusInfo(); 442 } 443 int status = getStatusOfDownload(c,0); 444 if (status == STATUS_DOWNLOADING_UPDATE || 445 status == STATUS_DOWNLOADED_UPDATE) { 446 result.completed = (status == STATUS_DOWNLOADED_UPDATE); 447 result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME); 448 result.id = c.getLong(DOWNLOADS_COLUMN_ID); 449 result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); 450 result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); 451 return result; 452 } 453 454 long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); 455 456 result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); 457 result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); 458 } finally { 459 if (c != null) { 460 c.close(); 461 } 462 } 463 return result; 464 } 465 } 466 467 468 /** 469 * Base class with common functionality for the various download classes 470 */ 471 public static class DownloadBase { 472 /** @hide */ 473 DownloadBase() {} 474 475 /** 476 * Initiate a download where the download will be tracked by its URI. 477 */ 478 public static long startDownloadByUri( 479 Context context, 480 String url, 481 String cookieData, 482 boolean showDownload, 483 int downloadDestination, 484 boolean allowRoaming, 485 boolean skipIntegrityCheck, 486 String title, 487 String notification_package, 488 String notification_class, 489 String notification_extras) { 490 ContentResolver cr = context.getContentResolver(); 491 492 // Tell download manager to start downloading update. 493 ContentValues values = new ContentValues(); 494 values.put(android.provider.Downloads.Impl.COLUMN_URI, url); 495 values.put(android.provider.Downloads.Impl.COLUMN_COOKIE_DATA, cookieData); 496 values.put(android.provider.Downloads.Impl.COLUMN_VISIBILITY, 497 showDownload ? android.provider.Downloads.Impl.VISIBILITY_VISIBLE 498 : android.provider.Downloads.Impl.VISIBILITY_HIDDEN); 499 if (title != null) { 500 values.put(android.provider.Downloads.Impl.COLUMN_TITLE, title); 501 } 502 values.put(android.provider.Downloads.Impl.COLUMN_APP_DATA, url); 503 504 505 // NOTE: destination should be seperated from whether the download 506 // can happen when roaming 507 int destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; 508 switch (downloadDestination) { 509 case DOWNLOAD_DESTINATION_EXTERNAL: 510 destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; 511 break; 512 case DOWNLOAD_DESTINATION_CACHE: 513 if (allowRoaming) { 514 destination = android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION; 515 } else { 516 destination = 517 android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING; 518 } 519 break; 520 case DOWNLOAD_DESTINATION_CACHE_PURGEABLE: 521 destination = 522 android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE; 523 break; 524 } 525 values.put(android.provider.Downloads.Impl.COLUMN_DESTINATION, destination); 526 values.put(android.provider.Downloads.Impl.COLUMN_NO_INTEGRITY, 527 skipIntegrityCheck); // Don't check ETag 528 if (notification_package != null && notification_class != null) { 529 values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, 530 notification_package); 531 values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS, 532 notification_class); 533 534 if (notification_extras != null) { 535 values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, 536 notification_extras); 537 } 538 } 539 540 Uri downloadUri = cr.insert(android.provider.Downloads.Impl.CONTENT_URI, values); 541 542 long downloadId = DOWNLOAD_ID_INVALID; 543 if (downloadUri != null) { 544 downloadId = Long.parseLong(downloadUri.getLastPathSegment()); 545 } 546 return downloadId; 547 } 548 } 549 550 /** @hide */ 551 private static final int STATUS_INVALID = 0; 552 /** @hide */ 553 private static final int STATUS_DOWNLOADING_UPDATE = 3; 554 /** @hide */ 555 private static final int STATUS_DOWNLOADED_UPDATE = 4; 556 557 /** 558 * Column projection for the query to the download manager. This must match 559 * with the constants DOWNLOADS_COLUMN_*. 560 * @hide 561 */ 562 private static final String[] DOWNLOADS_PROJECTION = { 563 BaseColumns._ID, 564 android.provider.Downloads.Impl.COLUMN_APP_DATA, 565 android.provider.Downloads.Impl.COLUMN_STATUS, 566 android.provider.Downloads.Impl._DATA, 567 android.provider.Downloads.Impl.COLUMN_LAST_MODIFICATION, 568 android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES, 569 }; 570 571 /** 572 * The column index for the ID. 573 * @hide 574 */ 575 private static final int DOWNLOADS_COLUMN_ID = 0; 576 /** 577 * The column index for the URI. 578 * @hide 579 */ 580 private static final int DOWNLOADS_COLUMN_URI = 1; 581 /** 582 * The column index for the status code. 583 * @hide 584 */ 585 private static final int DOWNLOADS_COLUMN_STATUS = 2; 586 /** 587 * The column index for the filename. 588 * @hide 589 */ 590 private static final int DOWNLOADS_COLUMN_FILENAME = 3; 591 /** 592 * The column index for the last modification time. 593 * @hide 594 */ 595 private static final int DOWNLOADS_COLUMN_LAST_MODIFICATION = 4; 596 /** 597 * The column index for the number of bytes downloaded so far. 598 * @hide 599 */ 600 private static final int DOWNLOADS_COLUMN_CURRENT_BYTES = 5; 601 602 /** 603 * Gets the status of a download. 604 * 605 * @param c A Cursor pointing to a download. The URL column is assumed to be valid. 606 * @return The status of the download. 607 * @hide 608 */ 609 private static final int getStatusOfDownload( Cursor c, long redownload_threshold) { 610 int status = c.getInt(DOWNLOADS_COLUMN_STATUS); 611 long realtime = SystemClock.elapsedRealtime(); 612 613 // TODO(dougz): special handling of 503, 404? (eg, special 614 // explanatory messages to user) 615 616 if (!android.provider.Downloads.Impl.isStatusCompleted(status)) { 617 // Check if it's stuck 618 long modified = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); 619 long now = System.currentTimeMillis(); 620 if (now < modified || now - modified > redownload_threshold) { 621 return STATUS_INVALID; 622 } 623 624 return STATUS_DOWNLOADING_UPDATE; 625 } 626 627 if (android.provider.Downloads.Impl.isStatusError(status)) { 628 return STATUS_INVALID; 629 } 630 631 String filename = c.getString(DOWNLOADS_COLUMN_FILENAME); 632 if (filename == null) { 633 return STATUS_INVALID; 634 } 635 636 return STATUS_DOWNLOADED_UPDATE; 637 } 638 639 640 /** 641 * @hide 642 */ 643 private Downloads() {} 644 } 645