/* * Copyright (C) 2010 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 com.novoda.downloadmanager.lib; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.text.TextUtils; import com.novoda.downloadmanager.lib.logger.LLog; import com.novoda.downloadmanager.notifications.NotificationVisibility; import java.io.File; import java.io.FileNotFoundException; import java.net.URI; /** * The download manager is a system service that handles long-running HTTP downloads. Clients may * request that a URI be downloaded to a particular destination file. The download manager will * conduct the download in the background, taking care of HTTP interactions and retrying downloads * after failures or across connectivity changes and system reboots. * <p/> * Instances of this class should be obtained through * {@link android.content.Context#getSystemService(String)} by passing * {@link android.content.Context#DOWNLOAD_SERVICE}. * <p/> * Apps that request downloads through this API should register a broadcast receiver for * {@link #ACTION_NOTIFICATION_CLICKED} to appropriately handle when the user clicks on a running * download in a notification or from the downloads UI. * <p/> * Note that the application must have the {@link android.Manifest.permission#INTERNET} * permission to use this class. */ public class DownloadManager { //CHECKSTYLE IGNORE MagicNumber /** * Extra information available when you register for notications of download status changes * see {@link Request#setNotificationExtra(String extra)` */ public static final String EXTRA_EXTRA = "com.novoda.download.lib.KEY_INTENT_EXTRA"; /** * An identifier for a particular download, unique across the system. Clients use this ID to * make subsequent calls related to the download. */ public static final String COLUMN_ID = DownloadContract.Downloads._ID; /** * The client-supplied title for this download. This will be displayed in system notifications. * Defaults to the empty string. */ public static final String COLUMN_TITLE = DownloadContract.Batches.COLUMN_TITLE; /** * The client-supplied description of this download. This will be displayed in system * notifications. Defaults to the empty string. */ public static final String COLUMN_DESCRIPTION = DownloadContract.Batches.COLUMN_DESCRIPTION; /** * The ID of the batch that contains this download. */ public static final String COLUMN_BATCH_ID = DownloadContract.Downloads.COLUMN_BATCH_ID; /** * The total size in bytes of the batch. */ public static final String COLUMN_BATCH_TOTAL_SIZE_BYTES = DownloadContract.BatchesWithSizes.COLUMN_TOTAL_BYTES; /** * The current size in bytes of the batch. */ public static final String COLUMN_BATCH_CURRENT_SIZE_BYTES = DownloadContract.BatchesWithSizes.COLUMN_CURRENT_BYTES; /** * The extra supplied information available to completion notifications for this download. */ public static final String COLUMN_NOTIFICATION_EXTRAS = DownloadContract.Downloads.COLUMN_NOTIFICATION_EXTRAS; /** * The extra supplied information available with any query for this download. */ public static final String COLUMN_EXTRA_DATA = DownloadContract.Downloads.COLUMN_EXTRA_DATA; /** * The status of the batch that contains this download. */ public static final String COLUMN_BATCH_STATUS = DownloadContract.Batches.COLUMN_STATUS; /** * URI to be downloaded. */ public static final String COLUMN_URI = DownloadContract.Downloads.COLUMN_URI; /** * Internet Media Type of the downloaded file. If no value is provided upon creation, this will * initially be null and will be filled in based on the server's response once the download has * started. * * @see <a href="http://www.ietf.org/rfc/rfc1590.txt">RFC 1590, defining Media Types</a> */ public static final String COLUMN_MEDIA_TYPE = "media_type"; /** * Total size of the download in bytes. This will initially be -1 and will be filled in once * the download starts. */ public static final String COLUMN_TOTAL_SIZE_BYTES = "total_size"; /** * Uri where downloaded file will be stored. If a destination is supplied by client, that URI * will be used here. Otherwise, the value will initially be null and will be filled in with a * generated URI once the download has started. */ public static final String COLUMN_LOCAL_URI = "local_uri"; /** * The pathname of the file where the download is stored. */ public static final String COLUMN_LOCAL_FILENAME = "local_filename"; /** * Current status of the download, as one of the STATUS_* constants. */ public static final String COLUMN_STATUS = DownloadContract.Downloads.COLUMN_STATUS; /** * Provides more detail on the status of the download. Its meaning depends on the value of * {@link #COLUMN_STATUS}. * <p/> * When {@link #COLUMN_STATUS} is {@link #STATUS_FAILED}, this indicates the type of error that * occurred. If an HTTP error occurred, this will hold the HTTP status code as defined in RFC * 2616. Otherwise, it will hold one of the ERROR_* constants. * <p/> * When {@link #COLUMN_STATUS} is {@link #STATUS_PAUSED}, this indicates why the download is * paused. It will hold one of the PAUSED_* constants. * <p/> * If {@link #COLUMN_STATUS} is neither {@link #STATUS_FAILED} nor {@link #STATUS_PAUSED}, this * column's value is undefined. * * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1">RFC 2616 * status codes</a> */ public static final String COLUMN_REASON = "reason"; /** * Number of bytes download so far. */ public static final String COLUMN_BYTES_DOWNLOADED_SO_FAR = "bytes_so_far"; /** * Timestamp when the download was last modified, in {@link System#currentTimeMillis * System.currentTimeMillis()} (wall clock time in UTC). */ public static final String COLUMN_LAST_MODIFIED_TIMESTAMP = DownloadContract.Downloads.COLUMN_LAST_MODIFICATION; /** * The URI to the corresponding entry in MediaProvider for this downloaded entry. It is * used to delete the entries from MediaProvider database when it is deleted from the * downloaded list. */ public static final String COLUMN_MEDIAPROVIDER_URI = DownloadContract.Downloads.COLUMN_MEDIAPROVIDER_URI; /** * Value of {@link #COLUMN_STATUS} when the download is waiting to start. */ public static final int STATUS_PENDING = 1 << 0; /** * Value of {@link #COLUMN_STATUS} when the download is currently running. */ public static final int STATUS_RUNNING = 1 << 1; /** * Value of {@link #COLUMN_STATUS} when the download is waiting to retry or resume. */ public static final int STATUS_PAUSED = 1 << 2; /** * Value of {@link #COLUMN_STATUS} when the download has successfully completed. */ public static final int STATUS_SUCCESSFUL = 1 << 3; /** * Value of {@link #COLUMN_STATUS} when the download has failed (and will not be retried). */ public static final int STATUS_FAILED = 1 << 4; /** * Value of {@link #COLUMN_STATUS} when the download is marked for deletion. */ public static final int STATUS_DELETING = 1 << 5; /** * Value of {@link #COLUMN_STATUS} when the download is marked for pausing. */ public static final int STATUS_PAUSING = 1 << 6; /** * Value of COLUMN_ERROR_CODE when the download has completed with an error that doesn't fit * under any other error code. */ public static final int ERROR_UNKNOWN = 1000; /** * Value of {@link #COLUMN_REASON} when a storage issue arises which doesn't fit under any * other error code. Use the more specific {@link #ERROR_INSUFFICIENT_SPACE} and * {@link #ERROR_DEVICE_NOT_FOUND} when appropriate. */ public static final int ERROR_FILE_ERROR = 1001; /** * Value of {@link #COLUMN_REASON} when an HTTP code was received that download manager * can't handle. */ public static final int ERROR_UNHANDLED_HTTP_CODE = 1002; /** * Value of {@link #COLUMN_REASON} when an error receiving or processing data occurred at * the HTTP level. */ public static final int ERROR_HTTP_DATA_ERROR = 1004; /** * Value of {@link #COLUMN_REASON} when there were too many redirects. */ public static final int ERROR_TOO_MANY_REDIRECTS = 1005; /** * Value of {@link #COLUMN_REASON} when there was insufficient storage space. Typically, * this is because the SD card is full. */ public static final int ERROR_INSUFFICIENT_SPACE = 1006; /** * Value of {@link #COLUMN_REASON} when no external storage device was found. Typically, * this is because the SD card is not mounted. */ public static final int ERROR_DEVICE_NOT_FOUND = 1007; /** * Value of {@link #COLUMN_REASON} when some possibly transient error occurred but we can't * resume the download. */ public static final int ERROR_CANNOT_RESUME = 1008; /** * Value of {@link #COLUMN_REASON} when the requested destination file already exists (the * download manager will not overwrite an existing file). */ public static final int ERROR_FILE_ALREADY_EXISTS = 1009; /** * Value of {@link #COLUMN_REASON} when the download is paused because some network error * occurred and the download manager is waiting before retrying the request. */ public static final int PAUSED_WAITING_TO_RETRY = 1; /** * Value of {@link #COLUMN_REASON} when the download is waiting for network connectivity to * proceed. */ public static final int PAUSED_WAITING_FOR_NETWORK = 2; /** * Value of {@link #COLUMN_REASON} when the download exceeds a size limit for downloads over * the mobile network and the download manager is waiting for a Wi-Fi connection to proceed. */ public static final int PAUSED_QUEUED_FOR_WIFI = 3; /** * Value of {@link #COLUMN_REASON} when the download is paused for some other reason. */ public static final int PAUSED_UNKNOWN = 4; /** * Value of {@link #COLUMN_REASON} when the download is paused due client download check permissions. */ public static final int PAUSED_QUEUED_DUE_CLIENT_RESTRICTIONS = 5; /** * Broadcast intent action sent by the download manager when a download completes. The * download's ID is specified in the intent's data. */ public static final String ACTION_DOWNLOAD_COMPLETE = "com.novoda.downloadmanager.DOWNLOAD_COMPLETE"; /** * Broadcast intent action sent by the download manager when a batch completes. The * batch's ID is specified in the intent's data. */ public static final String ACTION_BATCH_COMPLETE = BatchInformationBroadcaster.ACTION_BATCH_COMPLETE; /** * Broadcast intent action sent by the download manager when a batch failed. The * batch's ID is specified in the intent's data. */ public static final String ACTION_BATCH_FAILED = BatchInformationBroadcaster.ACTION_BATCH_FAILED; /** * Broadcast intent action sent by the download manager when a batch has started. The * batch's ID is specified in the intent's data. */ public static final String ACTION_BATCH_STARTED = BatchInformationBroadcaster.ACTION_BATCH_STARTED_FOR_FIRST_TIME; /** * Broadcast intent action sent by the download manager when a download wasn't started due to insufficient space */ public static final String ACTION_DOWNLOAD_INSUFFICIENT_SPACE = "com.novoda.downloadmanager.DOWNLOAD_INSUFFICIENT_SPACE"; /** * Broadcast intent action sent by the download manager when the user clicks on a running * download, either from a system notification. The download's content: uri is specified * in the intent's data if the click is associated with a single download, * or {@link DownloadsUriProvider#getContentUri()} if the notification is associated with * multiple downloads. */ public static final String ACTION_NOTIFICATION_CLICKED = "com.novoda.downloadmanager.DOWNLOAD_NOTIFICATION_CLICKED"; /** * Intent extra included with {@link #ACTION_DOWNLOAD_COMPLETE} intents, indicating the ID (as a * long) of the download that just completed. */ public static final String EXTRA_DOWNLOAD_ID = "extra_download_id"; /** * Intent extra included with {@link #ACTION_BATCH_COMPLETE} intents, indicating the ID (as a * long) of the batch that just completed. */ public static final String EXTRA_BATCH_ID = BatchInformationBroadcaster.EXTRA_BATCH_ID; /** * Intent extra included with {@link #ACTION_DOWNLOAD_COMPLETE} intents, indicating the status code of the download that just completed. */ public static final String EXTRA_DOWNLOAD_STATUS = "extra_download_status"; /** * When clicks on multiple notifications are received, the following * provides an array of download ids corresponding to the download notification that was * clicked. It can be retrieved by the receiver of this * Intent using {@link android.content.Intent#getLongArrayExtra(String)}. */ public static final String EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS = "extra_click_download_ids"; /** * When clicks on multiple notifications are received, the following * provides an int array of download statuses corresponding to the download notification that was * clicked (in the same order as @{link DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS}. * It can be retrieved by the receiver of this Intent using {@link android.content.Intent#getLongArrayExtra(String)}. */ public static final String EXTRA_NOTIFICATION_CLICK_DOWNLOAD_STATUSES = "download_statuses"; /** * columns to request from DownloadProvider. */ public static final String[] UNDERLYING_COLUMNS = new String[]{ DownloadContract.Downloads._ID, DownloadContract.Downloads.COLUMN_DATA + " AS " + COLUMN_LOCAL_FILENAME, DownloadContract.Downloads.COLUMN_MEDIAPROVIDER_URI, DownloadContract.Downloads.COLUMN_DESTINATION, DownloadContract.Downloads.COLUMN_URI, DownloadContract.Downloads.COLUMN_STATUS, DownloadContract.Downloads.COLUMN_DELETED, DownloadContract.Downloads.COLUMN_FILE_NAME_HINT, DownloadContract.Downloads.COLUMN_MIME_TYPE + " AS " + COLUMN_MEDIA_TYPE, DownloadContract.Downloads.COLUMN_TOTAL_BYTES + " AS " + COLUMN_TOTAL_SIZE_BYTES, DownloadContract.Downloads.COLUMN_LAST_MODIFICATION, DownloadContract.Downloads.COLUMN_CURRENT_BYTES + " AS " + COLUMN_BYTES_DOWNLOADED_SO_FAR, DownloadContract.Downloads.COLUMN_BATCH_ID, DownloadContract.Downloads.COLUMN_EXTRA_DATA, DownloadContract.Downloads.COLUMN_NOTIFICATION_EXTRAS, DownloadContract.Batches.COLUMN_TITLE, DownloadContract.Batches.COLUMN_DESCRIPTION, DownloadContract.Batches.COLUMN_BIG_PICTURE, DownloadContract.Batches.COLUMN_VISIBILITY, DownloadContract.Batches.COLUMN_STATUS, DownloadContract.Batches.COLUMN_EXTRA_DATA, DownloadContract.Batches.COLUMN_LAST_MODIFICATION, /* add the following 'computed' columns to the cursor. * they are not 'returned' by the database, but their inclusion * eliminates need to have lot of methods in CursorTranslator */ "'placeholder' AS " + COLUMN_LOCAL_URI, "'placeholder' AS " + COLUMN_REASON }; //CHECKSTYLE END IGNORE MagicNumber private final ContentResolver contentResolver; private final DownloadsUriProvider downloadsUriProvider; private final SystemFacade systemFacade; private final BatchPauseResumeController batchPauseResumeController; private Uri baseUri; public DownloadManager(Context context, ContentResolver contentResolver) { this( context, contentResolver, DownloadsUriProvider.getInstance(), new RealSystemFacade(context, new Clock()), new BatchPauseResumeController( contentResolver, DownloadsUriProvider.getInstance(), BatchRepository.from( contentResolver, new DownloadDeleter(contentResolver), DownloadsUriProvider.getInstance(), new RealSystemFacade(GlobalState.getContext(), new Clock()) ), new DownloadsRepository( new RealSystemFacade(GlobalState.getContext(), new Clock()), contentResolver, DownloadsRepository.DownloadInfoCreator.NON_FUNCTIONAL, DownloadsUriProvider.getInstance() ) ), false ); } public DownloadManager(Context context, ContentResolver contentResolver, boolean verboseLogging) { this( context, contentResolver, DownloadsUriProvider.getInstance(), new RealSystemFacade(context, new Clock()), new BatchPauseResumeController( contentResolver, DownloadsUriProvider.getInstance(), BatchRepository.from( contentResolver, new DownloadDeleter(contentResolver), DownloadsUriProvider.getInstance(), new RealSystemFacade(GlobalState.getContext(), new Clock()) ), new DownloadsRepository( new RealSystemFacade(GlobalState.getContext(), new Clock()), contentResolver, DownloadsRepository.DownloadInfoCreator.NON_FUNCTIONAL, DownloadsUriProvider.getInstance() ) ), verboseLogging ); } DownloadManager(Context context, ContentResolver contentResolver, DownloadsUriProvider downloadsUriProvider) { this( context, contentResolver, downloadsUriProvider, new RealSystemFacade(context, new Clock()), new BatchPauseResumeController( contentResolver, DownloadsUriProvider.getInstance(), BatchRepository.from( contentResolver, new DownloadDeleter(contentResolver), DownloadsUriProvider.getInstance(), new RealSystemFacade(GlobalState.getContext(), new Clock()) ), new DownloadsRepository( new RealSystemFacade(GlobalState.getContext(), new Clock()), contentResolver, DownloadsRepository.DownloadInfoCreator.NON_FUNCTIONAL, DownloadsUriProvider.getInstance() ) ), false ); } DownloadManager(Context context, ContentResolver contentResolver, DownloadsUriProvider downloadsUriProvider, SystemFacade systemFacade, BatchPauseResumeController batchPauseResumeController, boolean verboseLogging) { this.contentResolver = contentResolver; this.downloadsUriProvider = downloadsUriProvider; this.baseUri = downloadsUriProvider.getContentUri(); this.systemFacade = systemFacade; this.batchPauseResumeController = batchPauseResumeController; GlobalState.setContext(context); GlobalState.setVerboseLogging(verboseLogging); } /** * Create an intent which will cancel the batch with the supplied ID. Must be sent as a broadcast. * * @param batchId the ID of the batch to delete * @return an {@link Intent} that can be broadcast */ public static Intent createCancelBatchIntent(long batchId, Context context) { Intent cancelIntent = new Intent(Constants.ACTION_CANCEL, null, context, DownloadReceiver.class); cancelIntent.putExtra(DownloadReceiver.EXTRA_BATCH_ID, batchId); return cancelIntent; } /** * Makes this object access the download provider through /all_downloads URIs rather than * /my_downloads URIs, for clients that have permission to do so. */ void setAccessAllDownloads(boolean accessAllDownloads) { if (accessAllDownloads) { baseUri = downloadsUriProvider.getAllDownloadsUri(); } else { baseUri = downloadsUriProvider.getContentUri(); } } /** * Enqueue a new download. The download will start automatically once the download manager is * ready to execute it and connectivity is available. * * @param request the parameters specifying this download * @return an ID for the download, unique across the system. This ID is used to make future * calls related to this download. */ public long enqueue(Request request) { RequestBatch batch = request.asBatch(); long batchId = insert(batch); request.setBatchId(batchId); return insert(request); } private long insert(Request request) { ContentValues values = request.toContentValues(); Uri downloadUri = contentResolver.insert(downloadsUriProvider.getContentUri(), values); return ContentUris.parseId(downloadUri); } /** * {@link BatchPauseResumeController#pauseBatch(long)} */ public boolean pauseBatch(long batchId) { return batchPauseResumeController.pauseBatch(batchId); } /** * {@link BatchPauseResumeController#resumeBatch(long)}} */ public boolean resumeBatch(long batchId) { return batchPauseResumeController.resumeBatch(batchId); } public void removeDownload(URI uri) { Cursor cursor = null; try { cursor = contentResolver.query( downloadsUriProvider.getContentUri(), new String[]{"_id"}, DownloadContract.Downloads.COLUMN_FILE_NAME_HINT + "=?", new String[]{uri.toString()}, null ); if (cursor.moveToFirst()) { long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); removeDownloads(id); return; } LLog.e("Didn't delete anything for uri: " + uri); } finally { if (cursor != null) { cursor.close(); } } } /** * Cancel downloads and remove them from the download manager. Each download will be stopped if * it was running, and it will no longer be accessible through the download manager. * If there is a downloaded file, partial or complete, it is deleted. * * @param ids the IDs of the downloads to remove * @return the number of downloads actually removed */ public int removeDownloads(long... ids) { if (ids == null || ids.length == 0) { throw new IllegalArgumentException("called with nothing to remove. input param 'ids' can't be null"); } ContentValues values = new ContentValues(); values.put(DownloadContract.Downloads.COLUMN_DELETED, 1); // if only one id is passed in, then include it in the uri itself. // this will eliminate a full database scan in the download service. if (ids.length == 1) { return contentResolver.update(ContentUris.withAppendedId(baseUri, ids[0]), values, null, null); } return contentResolver.update(baseUri, values, getWhereClauseFor(ids, DownloadContract.Downloads._ID), longArrayToStringArray(ids)); } /** * Cancel batch downloads and remove them from the download manager. Each download will be stopped if * it was running, and it will no longer be accessible through the download manager. * If there are any downloaded files, partial or complete, they will be deleted. * * @param batchIds the IDs of the batches to remove * @return the number of batches actually removed */ public int removeBatches(long... batchIds) { if (batchIds == null || batchIds.length == 0) { throw new IllegalArgumentException("called with nothing to remove. input param 'batchIds' can't be null"); } setDeletingStatusFor(batchIds); return markBatchesToBeDeleted(batchIds); } private void setDeletingStatusFor(long[] batchesIds) { ContentValues values = new ContentValues(1); values.put(DownloadContract.Downloads.COLUMN_STATUS, DownloadStatus.DELETING); if (batchesIds.length == 1) { contentResolver.update(downloadsUriProvider.getContentUri(), values, COLUMN_BATCH_ID + "=?", new String[]{String.valueOf(batchesIds[0])}); } else { contentResolver.update(downloadsUriProvider.getContentUri(), values, getWhereClauseFor(batchesIds, COLUMN_BATCH_ID), longArrayToStringArray(batchesIds)); } } private int markBatchesToBeDeleted(long[] batchesIds) { ContentValues valuesDelete = new ContentValues(1); valuesDelete.put(DownloadContract.Batches.COLUMN_DELETED, 1); if (batchesIds.length == 1) { return contentResolver.update(ContentUris.withAppendedId(downloadsUriProvider.getBatchesUri(), batchesIds[0]), valuesDelete, null, null); } return contentResolver.update( downloadsUriProvider.getBatchesUri(), valuesDelete, getWhereClauseFor(batchesIds, DownloadContract.Downloads._ID), longArrayToStringArray(batchesIds) ); } /** * Query the download manager about downloads that have been requested. * * @param query parameters specifying filters for this query * @return a Cursor over the result set of downloads, with columns consisting of all the * COLUMN_* constants. */ public Cursor query(Query query) { Cursor underlyingCursor = query.runQuery(contentResolver, UNDERLYING_COLUMNS, downloadsUriProvider.getDownloadsByBatchUri()); if (underlyingCursor == null) { return null; } PublicFacingStatusTranslator statusTranslator = new PublicFacingStatusTranslator(); return new CursorTranslator(underlyingCursor, downloadsUriProvider.getDownloadsByBatchUri(), statusTranslator); } /** * Query the download manager about batches that have been requested. * * @param query parameters specifying filters for this query * @return a Cursor over the result set of batches */ public Cursor query(BatchQuery query) { DownloadDeleter downloadDeleter = new DownloadDeleter(contentResolver); RealSystemFacade systemFacade = new RealSystemFacade(GlobalState.getContext(), new Clock()); BatchRepository batchRepository = BatchRepository.from(contentResolver, downloadDeleter, downloadsUriProvider, systemFacade); Cursor cursor = batchRepository.retrieveFor(query); if (cursor == null) { return null; } PublicFacingStatusTranslator statusTranslator = new PublicFacingStatusTranslator(); return new CursorTranslator(cursor, downloadsUriProvider.getBatchesUri(), statusTranslator); } /** * Open a downloaded file for reading. The download must have completed. * * @param id the ID of the download * @return a read-only {@link ParcelFileDescriptor} * @throws FileNotFoundException if the destination file does not already exist */ public ParcelFileDescriptor openDownloadedFile(long id) throws FileNotFoundException { return contentResolver.openFileDescriptor(getDownloadUri(id), "r"); } /** * Returns {@link Uri} for the given downloaded file id, if the file is * downloaded successfully. otherwise, null is returned. * <p/> * If the specified downloaded file is in external storage (for example, /sdcard dir), * then it is assumed to be safe for anyone to read and the returned {@link Uri} corresponds * to the filepath on sdcard. * * @param id the id of the downloaded file. * @return the {@link Uri} for the given downloaded file id, if download was successful. null * otherwise. */ public Uri getUriForDownloadedFile(long id) { // to check if the file is in cache, get its destination from the database Query query = new Query().setFilterById(id); Cursor cursor = null; try { cursor = query(query); if (cursor == null) { return null; } if (cursor.moveToFirst()) { int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS)); if (DownloadManager.STATUS_SUCCESSFUL == status) { int indx = cursor.getColumnIndexOrThrow( DownloadContract.Downloads.COLUMN_DESTINATION ); int destination = cursor.getInt(indx); // TODO: if we ever add API to DownloadManager to let the caller specify // non-external storage for a downloaded file, then the following code // should also check for that destination. if (destination == DownloadsDestination.DESTINATION_CACHE_PARTITION || destination == DownloadsDestination.DESTINATION_SYSTEMCACHE_PARTITION || destination == DownloadsDestination.DESTINATION_CACHE_PARTITION_NOROAMING || destination == DownloadsDestination.DESTINATION_CACHE_PARTITION_PURGEABLE) { // return private uri return ContentUris.withAppendedId(downloadsUriProvider.getContentUri(), id); } else { // return public uri String path = cursor.getString( cursor.getColumnIndexOrThrow(COLUMN_LOCAL_FILENAME) ); return Uri.fromFile(new File(path)); } } } } finally { if (cursor != null) { cursor.close(); } } // downloaded file not found or its status is not 'successfully completed' return null; } /** * Returns {@link Uri} for the given downloaded file id, if the file is * downloaded successfully. otherwise, null is returned. * <p/> * If the specified downloaded file is in external storage (for example, /sdcard dir), * then it is assumed to be safe for anyone to read and the returned {@link Uri} corresponds * to the filepath on sdcard. * * @param id the id of the downloaded file. * @return the {@link Uri} for the given downloaded file id, if download was successful. null * otherwise. */ public String getMimeTypeForDownloadedFile(long id) { Query query = new Query().setFilterById(id); Cursor cursor = null; try { cursor = query(query); if (cursor == null) { return null; } if (cursor.moveToFirst()) { return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MEDIA_TYPE)); } } finally { if (cursor != null) { cursor.close(); } } // downloaded file not found or its status is not 'successfully completed' return null; } /** * Restart the given downloads, which must have already completed (successfully or not). This * method will only work when called from within the download manager's process. * * @param ids the IDs of the downloads */ public void restartDownload(long... ids) { Cursor cursor = query(new Query().setFilterById(ids)); try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { int status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS)); if (status != STATUS_SUCCESSFUL && status != STATUS_FAILED) { throw new IllegalArgumentException( "Cannot restart incomplete download: " + cursor.getLong(cursor.getColumnIndex(COLUMN_ID)) ); } } } finally { cursor.close(); } ContentValues values = new ContentValues(); values.put(DownloadContract.Downloads.COLUMN_CURRENT_BYTES, 0); values.put(DownloadContract.Downloads.COLUMN_TOTAL_BYTES, -1); values.putNull(DownloadContract.Downloads.COLUMN_DATA); values.put(DownloadContract.Downloads.COLUMN_STATUS, DownloadStatus.PENDING); values.put(DownloadContract.Downloads.COLUMN_FAILED_CONNECTIONS, 0); contentResolver.update(baseUri, values, getWhereClauseFor(ids, DownloadContract.Downloads._ID), longArrayToStringArray(ids)); } /** * Returns maximum size, in bytes, of downloads that may go over a mobile connection; or null if * there's no limit * * @param context the {@link Context} to use for accessing the {@link ContentResolver} * @return maximum size, in bytes, of downloads that may go over a mobile connection; or null if * there's no limit */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static Long getMaxBytesOverMobile(Context context) { try { //Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE return Settings.Global.getLong(context.getContentResolver(), "download_manager_max_bytes_over_mobile"); } catch (SettingNotFoundException exc) { return null; } } /** * Returns recommended maximum size, in bytes, of downloads that may go over a mobile * connection; or null if there's no recommended limit. The user will have the option to bypass * this limit. * * @param context the {@link Context} to use for accessing the {@link ContentResolver} * @return recommended maximum size, in bytes, of downloads that may go over a mobile * connection; or null if there's no recommended limit. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static Long getRecommendedMaxBytesOverMobile(Context context) { try { //Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE return Settings.Global.getLong(context.getContentResolver(), "download_manager_recommended_max_bytes_over_mobile"); } catch (SettingNotFoundException exc) { return null; } } /** * Adds a file to the downloads database system. * <p/> * It is helpful to make the file scannable by MediaScanner by setting the param * isMediaScannerScannable to true. It makes the file visible in media managing * applications such as Gallery App, which could be a useful purpose of using this API. * * @param title the title that would appear for this file in Downloads App. * @param description the description that would appear for this file in Downloads App. * @param isMediaScannerScannable true if the file is to be scanned by MediaScanner. Files * scanned by MediaScanner appear in the applications used to view media (for example, * Gallery app). * @param mimeType mimetype of the file. * @param path absolute pathname to the file. The file should be world-readable, so that it can * be managed by the Downloads App and any other app that is used to read it (for example, * Gallery app to display the file, if the file contents represent a video/image). * @param length length of the downloaded file * @param showNotification true if a notification is to be sent, false otherwise * @return an ID for the download entry added to the downloads app, unique across the system * This ID is used to make future calls related to this download. */ public long addCompletedDownload(String title, String description, boolean isMediaScannerScannable, String mimeType, String path, long length, boolean showNotification) { // make sure the input args are non-null/non-zero validateArgumentIsNonEmpty("title", title); validateArgumentIsNonEmpty("description", description); validateArgumentIsNonEmpty("path", path); validateArgumentIsNonEmpty("mimeType", mimeType); if (length < 0) { throw new IllegalArgumentException(" invalid value for param: totalBytes"); } // if there is already an entry with the given path name in downloads.db, return its id Request request = new Request(NON_DOWNLOADMANAGER_DOWNLOAD) .setTitle(title) .setDescription(description) .setMimeType(mimeType) .setNotificationVisibility(showNotification ? NotificationVisibility.ONLY_WHEN_COMPLETE : NotificationVisibility.HIDDEN); if (isMediaScannerScannable) { request.allowScanningByMediaScanner(); } return addCompletedBatch(request.asBatch()); } /** * Adds this batch to the downloads system. * * @param requestBatch A batch of completed downloads which have been downloaded not using this DownloadManager * @return the id of the batch which can be used to query the download manager */ public long addCompletedBatch(RequestBatch requestBatch) { long completedBatchId = insertBatchAsCompleted(requestBatch); for (Request request : requestBatch.getRequests()) { request.setBatchId(completedBatchId); File file = new File(request.getDestinationPath()); long length = file.exists() ? file.length() : 0; insertRequestAsCompletedDownload(request.getDestinationPath(), length, request); } return completedBatchId; } private long insertBatchAsCompleted(RequestBatch batch) { ContentValues values = batch.toContentValues(); values.put(DownloadContract.Batches.COLUMN_STATUS, DownloadStatus.SUCCESS); values.put(DownloadContract.Batches.COLUMN_LAST_MODIFICATION, systemFacade.currentTimeMillis()); Uri batchUri = contentResolver.insert(downloadsUriProvider.getBatchesUri(), values); return ContentUris.parseId(batchUri); } private long insertRequestAsCompletedDownload(String path, long length, Request request) { ContentValues values = request.toContentValues(); values.put(DownloadContract.Downloads.COLUMN_DESTINATION, DownloadsDestination.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD); values.put(DownloadContract.Downloads.COLUMN_DATA, path); values.put(DownloadContract.Downloads.COLUMN_STATUS, DownloadStatus.SUCCESS); values.put(DownloadContract.Downloads.COLUMN_TOTAL_BYTES, length); values.put(DownloadContract.Downloads.COLUMN_CURRENT_BYTES, length); Uri downloadUri = contentResolver.insert(downloadsUriProvider.getContentUri(), values); if (downloadUri == null) { return -1; } return ContentUris.parseId(downloadUri); } private static final String NON_DOWNLOADMANAGER_DOWNLOAD = "non-dwnldmngr-download-dont-retry2download"; private static void validateArgumentIsNonEmpty(String paramName, String val) { if (TextUtils.isEmpty(val)) { throw new IllegalArgumentException(paramName + " can't be null"); } } /** * Get the DownloadProvider URI for the download with the given ID. */ private Uri getDownloadUri(long id) { return ContentUris.withAppendedId(baseUri, id); } /** * This is the uri for the underlying table * use this at your own risk as many of the constants defined here will not return you what you expect for raw cursor data */ public Uri getContentUri() { return downloadsUriProvider.getContentUri(); } /** * Uri for the batches table */ public Uri getBatchesUri() { return downloadsUriProvider.getBatchesUri(); } static String getWhereClauseFor(long[] ids, String column) { StringBuilder whereClause = new StringBuilder(); whereClause.append("("); for (int i = 0; i < ids.length; i++) { if (i > 0) { whereClause.append("OR "); } whereClause.append(column); whereClause.append(" = ? "); } whereClause.append(")"); return whereClause.toString(); } private static String[] longArrayToStringArray(long[] ids) { String[] whereArgs = new String[ids.length]; for (int i = 0; i < ids.length; i++) { whereArgs[i] = Long.toString(ids[i]); } return whereArgs; } /** * Enqueue a new download batch. * * @param batch the parameters specifying this batch * @return an ID for the batch, unique across the system. This ID is used to make future * calls related to this batch. */ public long enqueue(RequestBatch batch) { long batchId = insert(batch); for (Request request : batch.getRequests()) { request.setBatchId(batchId); insert(request); } notifyBatchesHaveChanged(); return batchId; } private void notifyBatchesHaveChanged() { contentResolver.notifyChange(getBatchesUri(), null); contentResolver.notifyChange(getBatchesWithoutProgressUri(), null); } private long insert(RequestBatch batch) { ContentValues values = batch.toContentValues(); values.put(DownloadContract.Batches.COLUMN_STATUS, DownloadStatus.PENDING); values.put(DownloadContract.Batches.COLUMN_LAST_MODIFICATION, systemFacade.currentTimeMillis()); Uri batchUri = contentResolver.insert(downloadsUriProvider.getBatchesUri(), values); return ContentUris.parseId(batchUri); } public Uri getDownloadsWithoutProgressUri() { return downloadsUriProvider.getDownloadsWithoutProgressUri(); } public Uri getBatchesWithoutProgressUri() { return downloadsUriProvider.getBatchesWithoutProgressUri(); } /** * Restart will activate the downloads workflow in case that it was not already active * <p/> * A possible scenario: A client denies a download for a particular business rule and that * rule does not apply any more. Calling this method will reactivate the downloads workflow, * check the client rules and proceed if necessary * <p/> * This method can be called as many times as desired as the system will take care that only * one instance is running, ignoring further calls if is currently active */ public void forceStart() { Context context = GlobalState.getContext(); context.startService(new Intent(context, DownloadService.class)); } }