package com.novoda.downloadmanager.lib; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import android.os.Environment; import android.util.Pair; import com.novoda.downloadmanager.notifications.NotificationVisibility; import java.io.File; import java.net.URI; import java.util.ArrayList; import java.util.List; /** * This class contains all the information necessary to request a new download. The URI is the * only required parameter. * <p/> * Note that the default download destination is a shared volume where the system might delete * your file if it needs to reclaim space for system use. If this is a problem, use a location * on external storage (see {@link #setDestinationUri(java.net.URI)}. */ public class Request { /** * Bit flag for {@link #setAllowedNetworkTypes} corresponding to * {@link android.net.ConnectivityManager#TYPE_MOBILE}. */ public static final int NETWORK_MOBILE = 1 << 0; /** * Bit flag for {@link #setAllowedNetworkTypes} corresponding to * {@link android.net.ConnectivityManager#TYPE_WIFI}. */ public static final int NETWORK_WIFI = 1 << 1; /** * Bit flag for {@link #setAllowedNetworkTypes} corresponding to * {@link android.net.ConnectivityManager#TYPE_BLUETOOTH}. */ public static final int NETWORK_BLUETOOTH = 1 << 2; private final List<Pair<String, String>> requestHeaders = new ArrayList<>(); private URI uri; private URI destinationUri; private CharSequence title = ""; private CharSequence description = ""; private String mimeType; private int allowedNetworkTypes = ~0; // default to all network types allowed private boolean roamingAllowed = true; private boolean meteredAllowed = true; private boolean isVisibleInDownloadsUi = true; private boolean scannable = false; private String notificationExtras; private String bigPictureUrl = ""; private long batchId = -1L; private String extraData; private boolean alwaysResume; private boolean allowTarUpdates; private boolean noIntegrity; /** * if a file is designated as a MediaScanner scannable file, the following value is * stored in the database column {@link DownloadContract.Downloads#COLUMN_MEDIA_SCANNED}. */ static final int SCANNABLE_VALUE_YES = 0; // value of 1 is stored in the above column by DownloadProvider after it is scanned by // MediaScanner /** * if a file is designated as a file that should not be scanned by MediaScanner, * the following value is stored in the database column * {@link DownloadContract.Downloads#COLUMN_MEDIA_SCANNED}. */ static final int SCANNABLE_VALUE_NO = 2; /** * can take any of the following values: {@link NotificationVisibility#HIDDEN} * {@link NotificationVisibility#ACTIVE_OR_COMPLETE}, {@link NotificationVisibility#ONLY_WHEN_ACTIVE}, * {@link NotificationVisibility#ONLY_WHEN_COMPLETE} */ private int notificationVisibility = NotificationVisibility.ONLY_WHEN_ACTIVE; /** * @param uri the HTTP Uri to download. * @deprecated use {@link #Request(java.net.URI)} */ @Deprecated public Request(Uri uri) { this(uri.toString()); } /** * @param uri the HTTP URI to download. */ public Request(URI uri) { validateUriScheme(uri.toString(), uri.getScheme()); this.uri = uri; } Request(String uriString) { URI uri = URI.create(uriString); validateUriScheme(uri.toString(), uri.getScheme()); this.uri = uri; } private void validateUriScheme(String uri, String scheme) { if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) { throw new IllegalArgumentException("Can only download HTTP/HTTPS URIs: " + uri); } } /** * Set the local destination for the downloaded file. Must be a file Uri to a path on * external storage, and the calling application must have the WRITE_EXTERNAL_STORAGE * permission. * <p/> * The downloaded file is not scanned by MediaScanner. * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}. * <p/> * By default, downloads are saved to a generated filename in the shared download cache and * may be deleted by the system at any time to reclaim space. * * @return this object * * @deprecated use {@link #setDestinationUri(java.net.URI)} */ @Deprecated public Request setDestinationUri(Uri uri) { destinationUri = URI.create(uri.toString()); return this; } /** * Set the local destination for the downloaded file. Must be a file URI to a path on * external storage, and the calling application must have the WRITE_EXTERNAL_STORAGE * permission. * <p/> * The downloaded file is not scanned by MediaScanner. * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}. * <p/> * By default, downloads are saved to a generated filename in the shared download cache and * may be deleted by the system at any time to reclaim space. * * @return this object */ public Request setDestinationUri(URI uri) { destinationUri = uri; return this; } /** * Set the local destination for the downloaded file to a path within * the application's external files directory (as returned by * {@link android.content.Context#getExternalFilesDir(String)}. * <p/> * The downloaded file is not scanned by MediaScanner. But it can be * made scannable by calling {@link #allowScanningByMediaScanner()}. * * @param dirType the directory type to pass to * {@link Context#getExternalFilesDir(String)} * @param subPath the path within the external directory, including the * destination filename * @return this object * @throws IllegalStateException If the external storage directory * cannot be found or created. */ public Request setDestinationInExternalFilesDir(String dirType, String subPath) { final File file = GlobalState.getContext().getExternalFilesDir(dirType); if (file == null) { throw new IllegalStateException("Failed to get external storage files directory"); } else if (file.exists()) { if (!file.isDirectory()) { throw new IllegalStateException(file.getAbsolutePath() + " already exists and is not a directory"); } } else { if (!file.mkdirs()) { throw new IllegalStateException("Unable to create directory: " + file.getAbsolutePath()); } } setDestinationFromBase(file, subPath); return this; } /** * Set the local destination for the downloaded file to a path within * the application's internal files directory (as returned by * {@link new File(context.getFilesDir(), dirType)}. * <p/> * The downloaded file is not scanned by MediaScanner. But it can be * made scannable by calling {@link #allowScanningByMediaScanner()}. * * @param dirType the directory type to pass to * {@link Context#getExternalFilesDir(String)} * @param subPath the path within the external directory, including the * destination filename * @return this object * @throws IllegalStateException If the external storage directory * cannot be found or created. */ public Request setDestinationInInternalFilesDir(String dirType, String subPath) { final File file = new File(GlobalState.getContext().getFilesDir(), dirType); if (file.exists()) { if (!file.isDirectory()) { throw new IllegalStateException(file.getAbsolutePath() + " already exists and is not a directory"); } } else { if (!file.mkdirs()) { throw new IllegalStateException("Unable to create directory: " + file.getAbsolutePath()); } } setDestinationFromBase(file, subPath); return this; } /** * Set the local destination for the downloaded file to a path within * the public external storage directory (as returned by * {@link android.os.Environment#getExternalStoragePublicDirectory(String)}). * <p/> * The downloaded file is not scanned by MediaScanner. But it can be * made scannable by calling {@link #allowScanningByMediaScanner()}. * * @param dirType the directory type to pass to {@link android.os.Environment#getExternalStoragePublicDirectory(String)} * @param subPath the path within the external directory, including the * destination filename * @return this object * @throws IllegalStateException If the external storage directory * cannot be found or created. */ public Request setDestinationInExternalPublicDir(String dirType, String subPath) { File file = Environment.getExternalStoragePublicDirectory(dirType); if (file == null) { throw new IllegalStateException("Failed to get external storage public directory"); } else if (file.exists()) { if (!file.isDirectory()) { throw new IllegalStateException(file.getAbsolutePath() + " already exists and is not a directory"); } } else { if (!file.mkdirs()) { throw new IllegalStateException("Unable to create directory: " + file.getAbsolutePath()); } } setDestinationFromBase(file, subPath); return this; } private void setDestinationFromBase(File base, String subPath) { if (subPath == null) { throw new NullPointerException("subPath cannot be null"); } destinationUri = new File(base, subPath).toURI(); } /** * If the file to be downloaded is to be scanned by MediaScanner, this method * should be called before {@link DownloadManager#enqueue(Request)} is called. */ public void allowScanningByMediaScanner() { scannable = true; } /** * Always attempt to resume the download, regardless of whether the server returns * a Etag header or not. **CAUTION** if the file has changed then this flag will * result in undefined behaviour. */ public Request alwaysAttemptResume() { alwaysResume = true; return this; } /** * Automatically pause the download when reaching the end of the file instead of writing the last bytes to disk. * This allows for a resume of the download against an updated tar file. * If used this also sets {@code alwaysAttemptResume()} */ public Request allowTarUpdates() { allowTarUpdates = true; alwaysAttemptResume(); return this; } /** * When a ETag header is present, the application should check the integrity of the * downloaded file, otherwise the current download won't be able to be resumed */ public Request applicationChecksFileIntegrity() { noIntegrity = true; return this; } /** * Add an HTTP header to be included with the download request. The header will be added to * the end of the list. * * @param header HTTP header name * @param value header value * @return this object * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2">HTTP/1.1 * Message Headers</a> */ public Request addRequestHeader(String header, String value) { if (header == null) { throw new NullPointerException("header cannot be null"); } if (header.contains(":")) { throw new IllegalArgumentException("header may not contain ':'"); } if (value == null) { value = ""; } requestHeaders.add(Pair.create(header, value)); return this; } /** * Set the title of this download, to be displayed in notifications (if enabled). If no * title is given, a default one will be assigned based on the download filename, once the * download starts. * * @return this object */ public Request setTitle(CharSequence title) { this.title = title; return this; } /** * Set a description of this download, to be displayed in notifications (if enabled) * * @return this object */ public Request setDescription(CharSequence description) { this.description = description; return this; } /** * Set a drawable url that will be used for the Big Picture Style * * @param bigPictureUrl the drawable resource id * @return this object */ public Request setBigPictureUrl(String bigPictureUrl) { this.bigPictureUrl = bigPictureUrl; return this; } /** * Set the ID of the batch that this request belongs to * * @param batchId the batch id * @return this object */ Request setBatchId(long batchId) { this.batchId = batchId; return this; } /** * Set the MIME content type of this download. This will override the content type declared * in the server's response. * * @return this object * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7">HTTP/1.1 * Media Types</a> */ public Request setMimeType(String mimeType) { this.mimeType = mimeType; return this; } /** * Control whether a system notification is posted by the download manager while this * download is running. If enabled, the download manager posts notifications about downloads * through the system {@link android.app.NotificationManager}. By default, a notification is * shown. * <p/> * If set to false, this requires the permission * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION. * * @param show whether the download manager should show a notification for this download. * @return this object * @deprecated use {@link #setNotificationVisibility(int)} */ @Deprecated public Request setShowRunningNotification(boolean show) { return (show) ? setNotificationVisibility(NotificationVisibility.ONLY_WHEN_ACTIVE) : setNotificationVisibility(NotificationVisibility.HIDDEN); } /** * Control whether a system notification is posted by the download manager while this * download is running or when it is completed. * If enabled, the download manager posts notifications about downloads * through the system {@link android.app.NotificationManager}. * By default, a notification is shown only when the download is in progress. * <p/> * It can take the following values: {@link NotificationVisibility#HIDDEN}, * {@link NotificationVisibility#ONLY_WHEN_ACTIVE}, * {@link NotificationVisibility#ONLY_WHEN_COMPLETE}. * <p/> * If set to {@link NotificationVisibility#HIDDEN}, this requires the permission * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION. * * @param visibility the visibility setting value * @return this object */ public Request setNotificationVisibility(int visibility) { notificationVisibility = visibility; return this; } /** * Restrict the types of networks over which this download may proceed. * By default, all network types are allowed. Consider using * {@link #setAllowedOverMetered(boolean)} instead, since it's more * flexible. * * @param flags any combination of the NETWORK_* bit flags. * @return this object */ public Request setAllowedNetworkTypes(int flags) { allowedNetworkTypes = flags; return this; } /** * Set whether this download may proceed over a roaming connection. By default, roaming is * allowed. * * @param allowed whether to allow a roaming connection to be used * @return this object */ public Request setAllowedOverRoaming(boolean allowed) { roamingAllowed = allowed; return this; } /** * Set whether this download may proceed over a metered network * connection. By default, metered networks are allowed. * * @see android.net.ConnectivityManager#isActiveNetworkMetered() */ public Request setAllowedOverMetered(boolean allow) { meteredAllowed = allow; return this; } /** * Set whether this download should be displayed in the system's Downloads UI. True by * default. * * @param isVisible whether to display this download in the Downloads UI * @return this object */ public Request setVisibleInDownloadsUi(boolean isVisible) { isVisibleInDownloadsUi = isVisible; return this; } /** * @param extra data that will be passed to you in the Intent on download completion. */ public Request setNotificationExtra(String extra) { notificationExtras = extra; return this; } /** * @param extra data you want to save alongside your download so you can query it later. */ public Request setExtraData(String extra) { extraData = extra; return this; } long getBatchId() { return batchId; } /** * @return ContentValues to be passed to DownloadProvider.insert() */ ContentValues toContentValues() { ContentValues values = new ContentValues(); assert uri != null; values.put(DownloadContract.Downloads.COLUMN_URI, uri.toString()); if (destinationUri != null) { values.put(DownloadContract.Downloads.COLUMN_DESTINATION, DownloadsDestination.DESTINATION_FILE_URI); values.put(DownloadContract.Downloads.COLUMN_FILE_NAME_HINT, destinationUri.toString()); } else { values.put( DownloadContract.Downloads.COLUMN_DESTINATION, DownloadsDestination.DESTINATION_CACHE_PARTITION_PURGEABLE ); } // is the file supposed to be media-scannable? values.put(DownloadContract.Downloads.COLUMN_MEDIA_SCANNED, (scannable) ? SCANNABLE_VALUE_YES : SCANNABLE_VALUE_NO); if (!requestHeaders.isEmpty()) { encodeHttpHeaders(values); } putIfNonNull(values, DownloadContract.Downloads.COLUMN_MIME_TYPE, mimeType); values.put(DownloadContract.Downloads.COLUMN_ALLOWED_NETWORK_TYPES, allowedNetworkTypes); values.put(DownloadContract.Downloads.COLUMN_ALLOW_ROAMING, roamingAllowed); values.put(DownloadContract.Downloads.COLUMN_ALLOW_METERED, meteredAllowed); values.put(DownloadContract.Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isVisibleInDownloadsUi); values.put(DownloadContract.Downloads.COLUMN_NOTIFICATION_EXTRAS, notificationExtras); values.put(DownloadContract.Downloads.COLUMN_BATCH_ID, batchId); values.put(DownloadContract.Downloads.COLUMN_EXTRA_DATA, extraData); values.put(DownloadContract.Downloads.COLUMN_ALWAYS_RESUME, alwaysResume); values.put(DownloadContract.Downloads.COLUMN_ALLOW_TAR_UPDATES, allowTarUpdates); values.put(DownloadContract.Downloads.COLUMN_NO_INTEGRITY, noIntegrity); return values; } private void encodeHttpHeaders(ContentValues values) { int index = 0; for (Pair<String, String> header : requestHeaders) { String headerString = header.first + ": " + header.second; values.put(DownloadContract.RequestHeaders.INSERT_KEY_PREFIX + index, headerString); index++; } } private void putIfNonNull(ContentValues contentValues, String key, Object value) { if (value != null) { contentValues.put(key, value.toString()); } } RequestBatch asBatch() { RequestBatch requestBatch = new RequestBatch.Builder() .withTitle(title.toString()) .withDescription(description.toString()) .withBigPictureUrl(bigPictureUrl) .withVisibility(notificationVisibility) .build(); requestBatch.addRequest(this); return requestBatch; } String getDestinationPath() { return destinationUri.getPath(); } }