blob: fd33781981216237657263e46c15d9c9a7933604 [file] [log] [blame]
/*
* 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() {}
}