| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.net; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.ParcelFileDescriptor; |
| import android.os.SystemClock; |
| import android.provider.BaseColumns; |
| import android.util.Log; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.File; |
| import java.io.InputStream; |
| |
| /** |
| * The Download Manager |
| * |
| * @hide |
| */ |
| public final class Downloads { |
| |
| |
| /** |
| * Download status codes |
| */ |
| |
| /** |
| * This download hasn't started yet |
| */ |
| public static final int STATUS_PENDING = 190; |
| |
| /** |
| * This download has started |
| */ |
| public static final int STATUS_RUNNING = 192; |
| |
| /** |
| * This download has successfully completed. |
| * Warning: there might be other status values that indicate success |
| * in the future. |
| * Use isSucccess() to capture the entire category. |
| */ |
| public static final int STATUS_SUCCESS = 200; |
| |
| /** |
| * This download can't be performed because the content type cannot be |
| * handled. |
| */ |
| public static final int STATUS_NOT_ACCEPTABLE = 406; |
| |
| /** |
| * This download has completed with an error. |
| * Warning: there will be other status values that indicate errors in |
| * the future. Use isStatusError() to capture the entire category. |
| */ |
| public static final int STATUS_UNKNOWN_ERROR = 491; |
| |
| /** |
| * This download couldn't be completed because of an HTTP |
| * redirect response that the download manager couldn't |
| * handle. |
| */ |
| public static final int STATUS_UNHANDLED_REDIRECT = 493; |
| |
| /** |
| * This download couldn't be completed due to insufficient storage |
| * space. Typically, this is because the SD card is full. |
| */ |
| public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; |
| |
| /** |
| * This download couldn't be completed because no external storage |
| * device was found. Typically, this is because the SD card is not |
| * mounted. |
| */ |
| public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; |
| |
| /** |
| * Returns whether the status is a success (i.e. 2xx). |
| */ |
| public static boolean isStatusSuccess(int status) { |
| return (status >= 200 && status < 300); |
| } |
| |
| /** |
| * Returns whether the status is an error (i.e. 4xx or 5xx). |
| */ |
| public static boolean isStatusError(int status) { |
| return (status >= 400 && status < 600); |
| } |
| |
| /** |
| * Download destinations |
| */ |
| |
| /** |
| * This download will be saved to the external storage. This is the |
| * default behavior, and should be used for any file that the user |
| * can freely access, copy, delete. Even with that destination, |
| * unencrypted DRM files are saved in secure internal storage. |
| * Downloads to the external destination only write files for which |
| * there is a registered handler. The resulting files are accessible |
| * by filename to all applications. |
| */ |
| public static final int DOWNLOAD_DESTINATION_EXTERNAL = 1; |
| |
| /** |
| * This download will be saved to the download manager's private |
| * partition. This is the behavior used by applications that want to |
| * download private files that are used and deleted soon after they |
| * get downloaded. All file types are allowed, and only the initiating |
| * application can access the file (indirectly through a content |
| * provider). This requires the |
| * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission. |
| */ |
| public static final int DOWNLOAD_DESTINATION_CACHE = 2; |
| |
| /** |
| * This download will be saved to the download manager's private |
| * partition and will be purged as necessary to make space. This is |
| * for private files (similar to CACHE_PARTITION) that aren't deleted |
| * immediately after they are used, and are kept around by the download |
| * manager as long as space is available. |
| */ |
| public static final int DOWNLOAD_DESTINATION_CACHE_PURGEABLE = 3; |
| |
| |
| /** |
| * An invalid download id |
| */ |
| public static final long DOWNLOAD_ID_INVALID = -1; |
| |
| |
| /** |
| * Broadcast Action: this is sent by the download manager to the app |
| * that had initiated a download when that download completes. The |
| * download's content: uri is specified in the intent's data. |
| */ |
| public static final String ACTION_DOWNLOAD_COMPLETED = |
| "android.intent.action.DOWNLOAD_COMPLETED"; |
| |
| /** |
| * If extras are specified when requesting a download they will be provided in the intent that |
| * is sent to the specified class and package when a download has finished. |
| * <P>Type: TEXT</P> |
| * <P>Owner can Init</P> |
| */ |
| public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras"; |
| |
| |
| /** |
| * Status class for a download |
| */ |
| public static final class StatusInfo { |
| public boolean completed = false; |
| /** The filename of the active download. */ |
| public String filename = null; |
| /** An opaque id for the download */ |
| public long id = DOWNLOAD_ID_INVALID; |
| /** An opaque status code for the download */ |
| public int statusCode = -1; |
| /** Approximate number of bytes downloaded so far, for debugging purposes. */ |
| public long bytesSoFar = -1; |
| |
| /** |
| * Returns whether the download is completed |
| * @return a boolean whether the download is complete. |
| */ |
| public boolean isComplete() { |
| return android.provider.Downloads.Impl.isStatusCompleted(statusCode); |
| } |
| |
| /** |
| * Returns whether the download is successful |
| * @return a boolean whether the download is successful. |
| */ |
| public boolean isSuccessful() { |
| return android.provider.Downloads.Impl.isStatusCompleted(statusCode); |
| } |
| } |
| |
| /** |
| * Class to access initiate and query download by server uri |
| */ |
| public static final class ByUri extends DownloadBase { |
| /** @hide */ |
| private ByUri() {} |
| |
| /** |
| * Query where clause by app data. |
| * @hide |
| */ |
| private static final String QUERY_WHERE_APP_DATA_CLAUSE = |
| android.provider.Downloads.Impl.COLUMN_APP_DATA + "=?"; |
| |
| /** |
| * Gets a Cursor pointing to the download(s) of the current system update. |
| * @hide |
| */ |
| private static final Cursor getCurrentOtaDownloads(Context context, String url) { |
| return context.getContentResolver().query( |
| android.provider.Downloads.Impl.CONTENT_URI, |
| DOWNLOADS_PROJECTION, |
| QUERY_WHERE_APP_DATA_CLAUSE, |
| new String[] {url}, |
| null); |
| } |
| |
| /** |
| * Returns a StatusInfo with the result of trying to download the |
| * given URL. Returns null if no attempts have been made. |
| */ |
| public static final StatusInfo getStatus( |
| Context context, |
| String url, |
| long redownload_threshold) { |
| StatusInfo result = null; |
| boolean hasFailedDownload = false; |
| long failedDownloadModificationTime = 0; |
| Cursor c = getCurrentOtaDownloads(context, url); |
| try { |
| while (c != null && c.moveToNext()) { |
| if (result == null) { |
| result = new StatusInfo(); |
| } |
| int status = getStatusOfDownload(c, redownload_threshold); |
| if (status == STATUS_DOWNLOADING_UPDATE || |
| status == STATUS_DOWNLOADED_UPDATE) { |
| result.completed = (status == STATUS_DOWNLOADED_UPDATE); |
| result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME); |
| result.id = c.getLong(DOWNLOADS_COLUMN_ID); |
| result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); |
| result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); |
| return result; |
| } |
| |
| long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); |
| if (hasFailedDownload && |
| modTime < failedDownloadModificationTime) { |
| // older than the one already in result; skip it. |
| continue; |
| } |
| |
| hasFailedDownload = true; |
| failedDownloadModificationTime = modTime; |
| result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); |
| result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); |
| } |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Query where clause for general querying. |
| */ |
| private static final String QUERY_WHERE_CLAUSE = |
| android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + "=? AND " + |
| android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS + "=?"; |
| |
| /** |
| * Delete all the downloads for a package/class pair. |
| */ |
| public static final void removeAllDownloadsByPackage( |
| Context context, |
| String notification_package, |
| String notification_class) { |
| context.getContentResolver().delete( |
| android.provider.Downloads.Impl.CONTENT_URI, |
| QUERY_WHERE_CLAUSE, |
| new String[] { notification_package, notification_class }); |
| } |
| |
| /** |
| * The column for the id in the Cursor returned by |
| * getProgressCursor() |
| */ |
| public static final int getProgressColumnId() { |
| return 0; |
| } |
| |
| /** |
| * The column for the current byte count in the Cursor returned by |
| * getProgressCursor() |
| */ |
| public static final int getProgressColumnCurrentBytes() { |
| return 1; |
| } |
| |
| /** |
| * The column for the total byte count in the Cursor returned by |
| * getProgressCursor() |
| */ |
| public static final int getProgressColumnTotalBytes() { |
| return 2; |
| } |
| |
| /** @hide */ |
| private static final String[] PROJECTION = { |
| BaseColumns._ID, |
| android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES, |
| android.provider.Downloads.Impl.COLUMN_TOTAL_BYTES |
| }; |
| |
| /** |
| * Returns a Cursor representing the progress of the download identified by the ID. |
| */ |
| public static final Cursor getProgressCursor(Context context, long id) { |
| Uri downloadUri = Uri.withAppendedPath(android.provider.Downloads.Impl.CONTENT_URI, |
| String.valueOf(id)); |
| return context.getContentResolver().query(downloadUri, PROJECTION, null, null, null); |
| } |
| } |
| |
| /** |
| * Class to access downloads by opaque download id |
| */ |
| public static final class ById extends DownloadBase { |
| /** @hide */ |
| private ById() {} |
| |
| /** |
| * Get the mime tupe of the download specified by the download id |
| */ |
| public static String getMimeTypeForId(Context context, long downloadId) { |
| ContentResolver cr = context.getContentResolver(); |
| |
| String mimeType = null; |
| Cursor downloadCursor = null; |
| |
| try { |
| Uri downloadUri = getDownloadUri(downloadId); |
| |
| downloadCursor = cr.query( |
| downloadUri, new String[]{android.provider.Downloads.Impl.COLUMN_MIME_TYPE}, |
| null, null, null); |
| if (downloadCursor.moveToNext()) { |
| mimeType = downloadCursor.getString(0); |
| } |
| } finally { |
| if (downloadCursor != null) downloadCursor.close(); |
| } |
| return mimeType; |
| } |
| |
| /** |
| * Delete a download by Id |
| */ |
| public static void deleteDownload(Context context, long downloadId) { |
| ContentResolver cr = context.getContentResolver(); |
| |
| String mimeType = null; |
| |
| Uri downloadUri = getDownloadUri(downloadId); |
| |
| cr.delete(downloadUri, null, null); |
| } |
| |
| /** |
| * Open a filedescriptor to a particular download |
| */ |
| public static ParcelFileDescriptor openDownload( |
| Context context, long downloadId, String mode) |
| throws FileNotFoundException |
| { |
| ContentResolver cr = context.getContentResolver(); |
| |
| String mimeType = null; |
| |
| Uri downloadUri = getDownloadUri(downloadId); |
| |
| return cr.openFileDescriptor(downloadUri, mode); |
| } |
| |
| /** |
| * Open a stream to a particular download |
| */ |
| public static InputStream openDownloadStream(Context context, long downloadId) |
| throws FileNotFoundException, IOException |
| { |
| ContentResolver cr = context.getContentResolver(); |
| |
| String mimeType = null; |
| |
| Uri downloadUri = getDownloadUri(downloadId); |
| |
| return cr.openInputStream(downloadUri); |
| } |
| |
| private static Uri getDownloadUri(long downloadId) { |
| return Uri.parse(android.provider.Downloads.Impl.CONTENT_URI + "/" + downloadId); |
| } |
| |
| /** |
| * Returns a StatusInfo with the result of trying to download the |
| * given URL. Returns null if no attempts have been made. |
| */ |
| public static final StatusInfo getStatus( |
| Context context, |
| long downloadId) { |
| StatusInfo result = null; |
| boolean hasFailedDownload = false; |
| long failedDownloadModificationTime = 0; |
| |
| Uri downloadUri = getDownloadUri(downloadId); |
| |
| ContentResolver cr = context.getContentResolver(); |
| |
| Cursor c = cr.query(downloadUri, DOWNLOADS_PROJECTION, null /* selection */, |
| null /* selection args */, null /* sort order */); |
| try { |
| if (c == null || !c.moveToNext()) { |
| return result; |
| } |
| |
| if (result == null) { |
| result = new StatusInfo(); |
| } |
| int status = getStatusOfDownload(c,0); |
| if (status == STATUS_DOWNLOADING_UPDATE || |
| status == STATUS_DOWNLOADED_UPDATE) { |
| result.completed = (status == STATUS_DOWNLOADED_UPDATE); |
| result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME); |
| result.id = c.getLong(DOWNLOADS_COLUMN_ID); |
| result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); |
| result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); |
| return result; |
| } |
| |
| long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); |
| |
| result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); |
| result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| return result; |
| } |
| } |
| |
| |
| /** |
| * Base class with common functionality for the various download classes |
| */ |
| public static class DownloadBase { |
| /** @hide */ |
| DownloadBase() {} |
| |
| /** |
| * Initiate a download where the download will be tracked by its URI. |
| */ |
| public static long startDownloadByUri( |
| Context context, |
| String url, |
| String cookieData, |
| boolean showDownload, |
| int downloadDestination, |
| boolean allowRoaming, |
| boolean skipIntegrityCheck, |
| String title, |
| String notification_package, |
| String notification_class, |
| String notification_extras) { |
| ContentResolver cr = context.getContentResolver(); |
| |
| // Tell download manager to start downloading update. |
| ContentValues values = new ContentValues(); |
| values.put(android.provider.Downloads.Impl.COLUMN_URI, url); |
| values.put(android.provider.Downloads.Impl.COLUMN_COOKIE_DATA, cookieData); |
| values.put(android.provider.Downloads.Impl.COLUMN_VISIBILITY, |
| showDownload ? android.provider.Downloads.Impl.VISIBILITY_VISIBLE |
| : android.provider.Downloads.Impl.VISIBILITY_HIDDEN); |
| if (title != null) { |
| values.put(android.provider.Downloads.Impl.COLUMN_TITLE, title); |
| } |
| values.put(android.provider.Downloads.Impl.COLUMN_APP_DATA, url); |
| |
| |
| // NOTE: destination should be seperated from whether the download |
| // can happen when roaming |
| int destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; |
| switch (downloadDestination) { |
| case DOWNLOAD_DESTINATION_EXTERNAL: |
| destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; |
| break; |
| case DOWNLOAD_DESTINATION_CACHE: |
| if (allowRoaming) { |
| destination = android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION; |
| } else { |
| destination = |
| android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING; |
| } |
| break; |
| case DOWNLOAD_DESTINATION_CACHE_PURGEABLE: |
| destination = |
| android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE; |
| break; |
| } |
| values.put(android.provider.Downloads.Impl.COLUMN_DESTINATION, destination); |
| values.put(android.provider.Downloads.Impl.COLUMN_NO_INTEGRITY, |
| skipIntegrityCheck); // Don't check ETag |
| if (notification_package != null && notification_class != null) { |
| values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, |
| notification_package); |
| values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS, |
| notification_class); |
| |
| if (notification_extras != null) { |
| values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, |
| notification_extras); |
| } |
| } |
| |
| Uri downloadUri = cr.insert(android.provider.Downloads.Impl.CONTENT_URI, values); |
| |
| long downloadId = DOWNLOAD_ID_INVALID; |
| if (downloadUri != null) { |
| downloadId = Long.parseLong(downloadUri.getLastPathSegment()); |
| } |
| return downloadId; |
| } |
| } |
| |
| /** @hide */ |
| private static final int STATUS_INVALID = 0; |
| /** @hide */ |
| private static final int STATUS_DOWNLOADING_UPDATE = 3; |
| /** @hide */ |
| private static final int STATUS_DOWNLOADED_UPDATE = 4; |
| |
| /** |
| * Column projection for the query to the download manager. This must match |
| * with the constants DOWNLOADS_COLUMN_*. |
| * @hide |
| */ |
| private static final String[] DOWNLOADS_PROJECTION = { |
| BaseColumns._ID, |
| android.provider.Downloads.Impl.COLUMN_APP_DATA, |
| android.provider.Downloads.Impl.COLUMN_STATUS, |
| android.provider.Downloads.Impl._DATA, |
| android.provider.Downloads.Impl.COLUMN_LAST_MODIFICATION, |
| android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES, |
| }; |
| |
| /** |
| * The column index for the ID. |
| * @hide |
| */ |
| private static final int DOWNLOADS_COLUMN_ID = 0; |
| /** |
| * The column index for the URI. |
| * @hide |
| */ |
| private static final int DOWNLOADS_COLUMN_URI = 1; |
| /** |
| * The column index for the status code. |
| * @hide |
| */ |
| private static final int DOWNLOADS_COLUMN_STATUS = 2; |
| /** |
| * The column index for the filename. |
| * @hide |
| */ |
| private static final int DOWNLOADS_COLUMN_FILENAME = 3; |
| /** |
| * The column index for the last modification time. |
| * @hide |
| */ |
| private static final int DOWNLOADS_COLUMN_LAST_MODIFICATION = 4; |
| /** |
| * The column index for the number of bytes downloaded so far. |
| * @hide |
| */ |
| private static final int DOWNLOADS_COLUMN_CURRENT_BYTES = 5; |
| |
| /** |
| * Gets the status of a download. |
| * |
| * @param c A Cursor pointing to a download. The URL column is assumed to be valid. |
| * @return The status of the download. |
| * @hide |
| */ |
| private static final int getStatusOfDownload( Cursor c, long redownload_threshold) { |
| int status = c.getInt(DOWNLOADS_COLUMN_STATUS); |
| long realtime = SystemClock.elapsedRealtime(); |
| |
| // TODO(dougz): special handling of 503, 404? (eg, special |
| // explanatory messages to user) |
| |
| if (!android.provider.Downloads.Impl.isStatusCompleted(status)) { |
| // Check if it's stuck |
| long modified = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); |
| long now = System.currentTimeMillis(); |
| if (now < modified || now - modified > redownload_threshold) { |
| return STATUS_INVALID; |
| } |
| |
| return STATUS_DOWNLOADING_UPDATE; |
| } |
| |
| if (android.provider.Downloads.Impl.isStatusError(status)) { |
| return STATUS_INVALID; |
| } |
| |
| String filename = c.getString(DOWNLOADS_COLUMN_FILENAME); |
| if (filename == null) { |
| return STATUS_INVALID; |
| } |
| |
| return STATUS_DOWNLOADED_UPDATE; |
| } |
| |
| |
| /** |
| * @hide |
| */ |
| private Downloads() {} |
| } |