646 lines
22 KiB
Java
646 lines
22 KiB
Java
/*
|
|
* 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.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() {}
|
|
}
|