• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.database.CursorWrapper;
25 import android.net.ConnectivityManager;
26 import android.net.Uri;
27 import android.os.Environment;
28 import android.os.ParcelFileDescriptor;
29 import android.provider.BaseColumns;
30 import android.provider.Downloads;
31 import android.util.Pair;
32 
33 import java.io.File;
34 import java.io.FileNotFoundException;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Set;
40 
41 /**
42  * The download manager is a system service that handles long-running HTTP downloads. Clients may
43  * request that a URI be downloaded to a particular destination file. The download manager will
44  * conduct the download in the background, taking care of HTTP interactions and retrying downloads
45  * after failures or across connectivity changes and system reboots.
46  *
47  * Instances of this class should be obtained through
48  * {@link android.content.Context#getSystemService(String)} by passing
49  * {@link android.content.Context#DOWNLOAD_SERVICE}.
50  *
51  * Apps that request downloads through this API should register a broadcast receiver for
52  * {@link #ACTION_NOTIFICATION_CLICKED} to appropriately handle when the user clicks on a running
53  * download in a notification or from the downloads UI.
54  */
55 public class DownloadManager {
56     private static final String TAG = "DownloadManager";
57 
58     /**
59      * An identifier for a particular download, unique across the system.  Clients use this ID to
60      * make subsequent calls related to the download.
61      */
62     public final static String COLUMN_ID = BaseColumns._ID;
63 
64     /**
65      * The client-supplied title for this download.  This will be displayed in system notifications.
66      * Defaults to the empty string.
67      */
68     public final static String COLUMN_TITLE = "title";
69 
70     /**
71      * The client-supplied description of this download.  This will be displayed in system
72      * notifications.  Defaults to the empty string.
73      */
74     public final static String COLUMN_DESCRIPTION = "description";
75 
76     /**
77      * URI to be downloaded.
78      */
79     public final static String COLUMN_URI = "uri";
80 
81     /**
82      * Internet Media Type of the downloaded file.  If no value is provided upon creation, this will
83      * initially be null and will be filled in based on the server's response once the download has
84      * started.
85      *
86      * @see <a href="http://www.ietf.org/rfc/rfc1590.txt">RFC 1590, defining Media Types</a>
87      */
88     public final static String COLUMN_MEDIA_TYPE = "media_type";
89 
90     /**
91      * Total size of the download in bytes.  This will initially be -1 and will be filled in once
92      * the download starts.
93      */
94     public final static String COLUMN_TOTAL_SIZE_BYTES = "total_size";
95 
96     /**
97      * Uri where downloaded file will be stored.  If a destination is supplied by client, that URI
98      * will be used here.  Otherwise, the value will initially be null and will be filled in with a
99      * generated URI once the download has started.
100      */
101     public final static String COLUMN_LOCAL_URI = "local_uri";
102 
103     /**
104      * Current status of the download, as one of the STATUS_* constants.
105      */
106     public final static String COLUMN_STATUS = "status";
107 
108     /**
109      * Provides more detail on the status of the download.  Its meaning depends on the value of
110      * {@link #COLUMN_STATUS}.
111      *
112      * When {@link #COLUMN_STATUS} is {@link #STATUS_FAILED}, this indicates the type of error that
113      * occurred.  If an HTTP error occurred, this will hold the HTTP status code as defined in RFC
114      * 2616.  Otherwise, it will hold one of the ERROR_* constants.
115      *
116      * When {@link #COLUMN_STATUS} is {@link #STATUS_PAUSED}, this indicates why the download is
117      * paused.  It will hold one of the PAUSED_* constants.
118      *
119      * If {@link #COLUMN_STATUS} is neither {@link #STATUS_FAILED} nor {@link #STATUS_PAUSED}, this
120      * column's value is undefined.
121      *
122      * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1">RFC 2616
123      * status codes</a>
124      */
125     public final static String COLUMN_REASON = "reason";
126 
127     /**
128      * Number of bytes download so far.
129      */
130     public final static String COLUMN_BYTES_DOWNLOADED_SO_FAR = "bytes_so_far";
131 
132     /**
133      * Timestamp when the download was last modified, in {@link System#currentTimeMillis
134      * System.currentTimeMillis()} (wall clock time in UTC).
135      */
136     public final static String COLUMN_LAST_MODIFIED_TIMESTAMP = "last_modified_timestamp";
137 
138     /**
139      * The URI to the corresponding entry in MediaProvider for this downloaded entry. It is
140      * used to delete the entries from MediaProvider database when it is deleted from the
141      * downloaded list.
142      */
143     public static final String COLUMN_MEDIAPROVIDER_URI = "mediaprovider_uri";
144 
145     /**
146      * Value of {@link #COLUMN_STATUS} when the download is waiting to start.
147      */
148     public final static int STATUS_PENDING = 1 << 0;
149 
150     /**
151      * Value of {@link #COLUMN_STATUS} when the download is currently running.
152      */
153     public final static int STATUS_RUNNING = 1 << 1;
154 
155     /**
156      * Value of {@link #COLUMN_STATUS} when the download is waiting to retry or resume.
157      */
158     public final static int STATUS_PAUSED = 1 << 2;
159 
160     /**
161      * Value of {@link #COLUMN_STATUS} when the download has successfully completed.
162      */
163     public final static int STATUS_SUCCESSFUL = 1 << 3;
164 
165     /**
166      * Value of {@link #COLUMN_STATUS} when the download has failed (and will not be retried).
167      */
168     public final static int STATUS_FAILED = 1 << 4;
169 
170 
171     /**
172      * Value of COLUMN_ERROR_CODE when the download has completed with an error that doesn't fit
173      * under any other error code.
174      */
175     public final static int ERROR_UNKNOWN = 1000;
176 
177     /**
178      * Value of {@link #COLUMN_REASON} when a storage issue arises which doesn't fit under any
179      * other error code. Use the more specific {@link #ERROR_INSUFFICIENT_SPACE} and
180      * {@link #ERROR_DEVICE_NOT_FOUND} when appropriate.
181      */
182     public final static int ERROR_FILE_ERROR = 1001;
183 
184     /**
185      * Value of {@link #COLUMN_REASON} when an HTTP code was received that download manager
186      * can't handle.
187      */
188     public final static int ERROR_UNHANDLED_HTTP_CODE = 1002;
189 
190     /**
191      * Value of {@link #COLUMN_REASON} when an error receiving or processing data occurred at
192      * the HTTP level.
193      */
194     public final static int ERROR_HTTP_DATA_ERROR = 1004;
195 
196     /**
197      * Value of {@link #COLUMN_REASON} when there were too many redirects.
198      */
199     public final static int ERROR_TOO_MANY_REDIRECTS = 1005;
200 
201     /**
202      * Value of {@link #COLUMN_REASON} when there was insufficient storage space. Typically,
203      * this is because the SD card is full.
204      */
205     public final static int ERROR_INSUFFICIENT_SPACE = 1006;
206 
207     /**
208      * Value of {@link #COLUMN_REASON} when no external storage device was found. Typically,
209      * this is because the SD card is not mounted.
210      */
211     public final static int ERROR_DEVICE_NOT_FOUND = 1007;
212 
213     /**
214      * Value of {@link #COLUMN_REASON} when some possibly transient error occurred but we can't
215      * resume the download.
216      */
217     public final static int ERROR_CANNOT_RESUME = 1008;
218 
219     /**
220      * Value of {@link #COLUMN_REASON} when the requested destination file already exists (the
221      * download manager will not overwrite an existing file).
222      */
223     public final static int ERROR_FILE_ALREADY_EXISTS = 1009;
224 
225     /**
226      * Value of {@link #COLUMN_REASON} when the download is paused because some network error
227      * occurred and the download manager is waiting before retrying the request.
228      */
229     public final static int PAUSED_WAITING_TO_RETRY = 1;
230 
231     /**
232      * Value of {@link #COLUMN_REASON} when the download is waiting for network connectivity to
233      * proceed.
234      */
235     public final static int PAUSED_WAITING_FOR_NETWORK = 2;
236 
237     /**
238      * Value of {@link #COLUMN_REASON} when the download exceeds a size limit for downloads over
239      * the mobile network and the download manager is waiting for a Wi-Fi connection to proceed.
240      */
241     public final static int PAUSED_QUEUED_FOR_WIFI = 3;
242 
243     /**
244      * Value of {@link #COLUMN_REASON} when the download is paused for some other reason.
245      */
246     public final static int PAUSED_UNKNOWN = 4;
247 
248     /**
249      * Broadcast intent action sent by the download manager when a download completes.
250      */
251     public final static String ACTION_DOWNLOAD_COMPLETE = "android.intent.action.DOWNLOAD_COMPLETE";
252 
253     /**
254      * Broadcast intent action sent by the download manager when the user clicks on a running
255      * download, either from a system notification or from the downloads UI.
256      */
257     public final static String ACTION_NOTIFICATION_CLICKED =
258             "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";
259 
260     /**
261      * Intent action to launch an activity to display all downloads.
262      */
263     public final static String ACTION_VIEW_DOWNLOADS = "android.intent.action.VIEW_DOWNLOADS";
264 
265     /**
266      * Intent extra included with {@link #ACTION_DOWNLOAD_COMPLETE} intents, indicating the ID (as a
267      * long) of the download that just completed.
268      */
269     public static final String EXTRA_DOWNLOAD_ID = "extra_download_id";
270 
271     // this array must contain all public columns
272     private static final String[] COLUMNS = new String[] {
273         COLUMN_ID,
274         COLUMN_MEDIAPROVIDER_URI,
275         COLUMN_TITLE,
276         COLUMN_DESCRIPTION,
277         COLUMN_URI,
278         COLUMN_MEDIA_TYPE,
279         COLUMN_TOTAL_SIZE_BYTES,
280         COLUMN_LOCAL_URI,
281         COLUMN_STATUS,
282         COLUMN_REASON,
283         COLUMN_BYTES_DOWNLOADED_SO_FAR,
284         COLUMN_LAST_MODIFIED_TIMESTAMP
285     };
286 
287     // columns to request from DownloadProvider
288     private static final String[] UNDERLYING_COLUMNS = new String[] {
289         Downloads.Impl._ID,
290         Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
291         Downloads.COLUMN_TITLE,
292         Downloads.COLUMN_DESCRIPTION,
293         Downloads.COLUMN_URI,
294         Downloads.COLUMN_MIME_TYPE,
295         Downloads.COLUMN_TOTAL_BYTES,
296         Downloads.COLUMN_STATUS,
297         Downloads.COLUMN_CURRENT_BYTES,
298         Downloads.COLUMN_LAST_MODIFICATION,
299         Downloads.COLUMN_DESTINATION,
300         Downloads.Impl.COLUMN_FILE_NAME_HINT,
301         Downloads.Impl._DATA,
302     };
303 
304     private static final Set<String> LONG_COLUMNS = new HashSet<String>(
305             Arrays.asList(COLUMN_ID, COLUMN_TOTAL_SIZE_BYTES, COLUMN_STATUS, COLUMN_REASON,
306                           COLUMN_BYTES_DOWNLOADED_SO_FAR, COLUMN_LAST_MODIFIED_TIMESTAMP));
307 
308     /**
309      * This class contains all the information necessary to request a new download. The URI is the
310      * only required parameter.
311      *
312      * Note that the default download destination is a shared volume where the system might delete
313      * your file if it needs to reclaim space for system use. If this is a problem, use a location
314      * on external storage (see {@link #setDestinationUri(Uri)}.
315      */
316     public static class Request {
317         /**
318          * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
319          * {@link ConnectivityManager#TYPE_MOBILE}.
320          */
321         public static final int NETWORK_MOBILE = 1 << 0;
322 
323         /**
324          * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
325          * {@link ConnectivityManager#TYPE_WIFI}.
326          */
327         public static final int NETWORK_WIFI = 1 << 1;
328 
329         private Uri mUri;
330         private Uri mDestinationUri;
331         private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
332         private CharSequence mTitle;
333         private CharSequence mDescription;
334         private boolean mShowNotification = true;
335         private String mMimeType;
336         private boolean mRoamingAllowed = true;
337         private int mAllowedNetworkTypes = ~0; // default to all network types allowed
338         private boolean mIsVisibleInDownloadsUi = true;
339 
340         /**
341          * @param uri the HTTP URI to download.
342          */
Request(Uri uri)343         public Request(Uri uri) {
344             if (uri == null) {
345                 throw new NullPointerException();
346             }
347             String scheme = uri.getScheme();
348             if (scheme == null || !scheme.equals("http")) {
349                 throw new IllegalArgumentException("Can only download HTTP URIs: " + uri);
350             }
351             mUri = uri;
352         }
353 
354         /**
355          * Set the local destination for the downloaded file. Must be a file URI to a path on
356          * external storage, and the calling application must have the WRITE_EXTERNAL_STORAGE
357          * permission.
358          *
359          * By default, downloads are saved to a generated filename in the shared download cache and
360          * may be deleted by the system at any time to reclaim space.
361          *
362          * @return this object
363          */
setDestinationUri(Uri uri)364         public Request setDestinationUri(Uri uri) {
365             mDestinationUri = uri;
366             return this;
367         }
368 
369         /**
370          * Set the local destination for the downloaded file to a path within the application's
371          * external files directory (as returned by {@link Context#getExternalFilesDir(String)}.
372          *
373          * @param context the {@link Context} to use in determining the external files directory
374          * @param dirType the directory type to pass to {@link Context#getExternalFilesDir(String)}
375          * @param subPath the path within the external directory, including the destination filename
376          * @return this object
377          */
setDestinationInExternalFilesDir(Context context, String dirType, String subPath)378         public Request setDestinationInExternalFilesDir(Context context, String dirType,
379                 String subPath) {
380             setDestinationFromBase(context.getExternalFilesDir(dirType), subPath);
381             return this;
382         }
383 
384         /**
385          * Set the local destination for the downloaded file to a path within the public external
386          * storage directory (as returned by
387          * {@link Environment#getExternalStoragePublicDirectory(String)}.
388          *
389          * @param dirType the directory type to pass to
390          *        {@link Environment#getExternalStoragePublicDirectory(String)}
391          * @param subPath the path within the external directory, including the destination filename
392          * @return this object
393          */
setDestinationInExternalPublicDir(String dirType, String subPath)394         public Request setDestinationInExternalPublicDir(String dirType, String subPath) {
395             setDestinationFromBase(Environment.getExternalStoragePublicDirectory(dirType), subPath);
396             return this;
397         }
398 
setDestinationFromBase(File base, String subPath)399         private void setDestinationFromBase(File base, String subPath) {
400             if (subPath == null) {
401                 throw new NullPointerException("subPath cannot be null");
402             }
403             mDestinationUri = Uri.withAppendedPath(Uri.fromFile(base), subPath);
404         }
405 
406         /**
407          * Add an HTTP header to be included with the download request.  The header will be added to
408          * the end of the list.
409          * @param header HTTP header name
410          * @param value header value
411          * @return this object
412          * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2">HTTP/1.1
413          *      Message Headers</a>
414          */
addRequestHeader(String header, String value)415         public Request addRequestHeader(String header, String value) {
416             if (header == null) {
417                 throw new NullPointerException("header cannot be null");
418             }
419             if (header.contains(":")) {
420                 throw new IllegalArgumentException("header may not contain ':'");
421             }
422             if (value == null) {
423                 value = "";
424             }
425             mRequestHeaders.add(Pair.create(header, value));
426             return this;
427         }
428 
429         /**
430          * Set the title of this download, to be displayed in notifications (if enabled).  If no
431          * title is given, a default one will be assigned based on the download filename, once the
432          * download starts.
433          * @return this object
434          */
setTitle(CharSequence title)435         public Request setTitle(CharSequence title) {
436             mTitle = title;
437             return this;
438         }
439 
440         /**
441          * Set a description of this download, to be displayed in notifications (if enabled)
442          * @return this object
443          */
setDescription(CharSequence description)444         public Request setDescription(CharSequence description) {
445             mDescription = description;
446             return this;
447         }
448 
449         /**
450          * Set the MIME content type of this download.  This will override the content type declared
451          * in the server's response.
452          * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7">HTTP/1.1
453          *      Media Types</a>
454          * @return this object
455          */
setMimeType(String mimeType)456         public Request setMimeType(String mimeType) {
457             mMimeType = mimeType;
458             return this;
459         }
460 
461         /**
462          * Control whether a system notification is posted by the download manager while this
463          * download is running. If enabled, the download manager posts notifications about downloads
464          * through the system {@link android.app.NotificationManager}. By default, a notification is
465          * shown.
466          *
467          * If set to false, this requires the permission
468          * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
469          *
470          * @param show whether the download manager should show a notification for this download.
471          * @return this object
472          */
setShowRunningNotification(boolean show)473         public Request setShowRunningNotification(boolean show) {
474             mShowNotification = show;
475             return this;
476         }
477 
478         /**
479          * Restrict the types of networks over which this download may proceed.  By default, all
480          * network types are allowed.
481          * @param flags any combination of the NETWORK_* bit flags.
482          * @return this object
483          */
setAllowedNetworkTypes(int flags)484         public Request setAllowedNetworkTypes(int flags) {
485             mAllowedNetworkTypes = flags;
486             return this;
487         }
488 
489         /**
490          * Set whether this download may proceed over a roaming connection.  By default, roaming is
491          * allowed.
492          * @param allowed whether to allow a roaming connection to be used
493          * @return this object
494          */
setAllowedOverRoaming(boolean allowed)495         public Request setAllowedOverRoaming(boolean allowed) {
496             mRoamingAllowed = allowed;
497             return this;
498         }
499 
500         /**
501          * Set whether this download should be displayed in the system's Downloads UI. True by
502          * default.
503          * @param isVisible whether to display this download in the Downloads UI
504          * @return this object
505          */
setVisibleInDownloadsUi(boolean isVisible)506         public Request setVisibleInDownloadsUi(boolean isVisible) {
507             mIsVisibleInDownloadsUi = isVisible;
508             return this;
509         }
510 
511         /**
512          * @return ContentValues to be passed to DownloadProvider.insert()
513          */
toContentValues(String packageName)514         ContentValues toContentValues(String packageName) {
515             ContentValues values = new ContentValues();
516             assert mUri != null;
517             values.put(Downloads.COLUMN_URI, mUri.toString());
518             values.put(Downloads.Impl.COLUMN_IS_PUBLIC_API, true);
519             values.put(Downloads.COLUMN_NOTIFICATION_PACKAGE, packageName);
520 
521             if (mDestinationUri != null) {
522                 values.put(Downloads.COLUMN_DESTINATION, Downloads.Impl.DESTINATION_FILE_URI);
523                 values.put(Downloads.COLUMN_FILE_NAME_HINT, mDestinationUri.toString());
524             } else {
525                 values.put(Downloads.COLUMN_DESTINATION,
526                            Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE);
527             }
528 
529             if (!mRequestHeaders.isEmpty()) {
530                 encodeHttpHeaders(values);
531             }
532 
533             putIfNonNull(values, Downloads.COLUMN_TITLE, mTitle);
534             putIfNonNull(values, Downloads.COLUMN_DESCRIPTION, mDescription);
535             putIfNonNull(values, Downloads.COLUMN_MIME_TYPE, mMimeType);
536 
537             values.put(Downloads.COLUMN_VISIBILITY,
538                     mShowNotification ? Downloads.VISIBILITY_VISIBLE
539                             : Downloads.VISIBILITY_HIDDEN);
540 
541             values.put(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, mAllowedNetworkTypes);
542             values.put(Downloads.Impl.COLUMN_ALLOW_ROAMING, mRoamingAllowed);
543             values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, mIsVisibleInDownloadsUi);
544 
545             return values;
546         }
547 
encodeHttpHeaders(ContentValues values)548         private void encodeHttpHeaders(ContentValues values) {
549             int index = 0;
550             for (Pair<String, String> header : mRequestHeaders) {
551                 String headerString = header.first + ": " + header.second;
552                 values.put(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX + index, headerString);
553                 index++;
554             }
555         }
556 
putIfNonNull(ContentValues contentValues, String key, Object value)557         private void putIfNonNull(ContentValues contentValues, String key, Object value) {
558             if (value != null) {
559                 contentValues.put(key, value.toString());
560             }
561         }
562     }
563 
564     /**
565      * This class may be used to filter download manager queries.
566      */
567     public static class Query {
568         /**
569          * Constant for use with {@link #orderBy}
570          * @hide
571          */
572         public static final int ORDER_ASCENDING = 1;
573 
574         /**
575          * Constant for use with {@link #orderBy}
576          * @hide
577          */
578         public static final int ORDER_DESCENDING = 2;
579 
580         private long[] mIds = null;
581         private Integer mStatusFlags = null;
582         private String mOrderByColumn = Downloads.COLUMN_LAST_MODIFICATION;
583         private int mOrderDirection = ORDER_DESCENDING;
584         private boolean mOnlyIncludeVisibleInDownloadsUi = false;
585 
586         /**
587          * Include only the downloads with the given IDs.
588          * @return this object
589          */
setFilterById(long... ids)590         public Query setFilterById(long... ids) {
591             mIds = ids;
592             return this;
593         }
594 
595         /**
596          * Include only downloads with status matching any the given status flags.
597          * @param flags any combination of the STATUS_* bit flags
598          * @return this object
599          */
setFilterByStatus(int flags)600         public Query setFilterByStatus(int flags) {
601             mStatusFlags = flags;
602             return this;
603         }
604 
605         /**
606          * Controls whether this query includes downloads not visible in the system's Downloads UI.
607          * @param value if true, this query will only include downloads that should be displayed in
608          *            the system's Downloads UI; if false (the default), this query will include
609          *            both visible and invisible downloads.
610          * @return this object
611          * @hide
612          */
setOnlyIncludeVisibleInDownloadsUi(boolean value)613         public Query setOnlyIncludeVisibleInDownloadsUi(boolean value) {
614             mOnlyIncludeVisibleInDownloadsUi = value;
615             return this;
616         }
617 
618         /**
619          * Change the sort order of the returned Cursor.
620          *
621          * @param column one of the COLUMN_* constants; currently, only
622          *         {@link #COLUMN_LAST_MODIFIED_TIMESTAMP} and {@link #COLUMN_TOTAL_SIZE_BYTES} are
623          *         supported.
624          * @param direction either {@link #ORDER_ASCENDING} or {@link #ORDER_DESCENDING}
625          * @return this object
626          * @hide
627          */
orderBy(String column, int direction)628         public Query orderBy(String column, int direction) {
629             if (direction != ORDER_ASCENDING && direction != ORDER_DESCENDING) {
630                 throw new IllegalArgumentException("Invalid direction: " + direction);
631             }
632 
633             if (column.equals(COLUMN_LAST_MODIFIED_TIMESTAMP)) {
634                 mOrderByColumn = Downloads.COLUMN_LAST_MODIFICATION;
635             } else if (column.equals(COLUMN_TOTAL_SIZE_BYTES)) {
636                 mOrderByColumn = Downloads.COLUMN_TOTAL_BYTES;
637             } else {
638                 throw new IllegalArgumentException("Cannot order by " + column);
639             }
640             mOrderDirection = direction;
641             return this;
642         }
643 
644         /**
645          * Run this query using the given ContentResolver.
646          * @param projection the projection to pass to ContentResolver.query()
647          * @return the Cursor returned by ContentResolver.query()
648          */
runQuery(ContentResolver resolver, String[] projection, Uri baseUri)649         Cursor runQuery(ContentResolver resolver, String[] projection, Uri baseUri) {
650             Uri uri = baseUri;
651             List<String> selectionParts = new ArrayList<String>();
652             String[] selectionArgs = null;
653 
654             if (mIds != null) {
655                 selectionParts.add(getWhereClauseForIds(mIds));
656                 selectionArgs = getWhereArgsForIds(mIds);
657             }
658 
659             if (mStatusFlags != null) {
660                 List<String> parts = new ArrayList<String>();
661                 if ((mStatusFlags & STATUS_PENDING) != 0) {
662                     parts.add(statusClause("=", Downloads.STATUS_PENDING));
663                 }
664                 if ((mStatusFlags & STATUS_RUNNING) != 0) {
665                     parts.add(statusClause("=", Downloads.STATUS_RUNNING));
666                 }
667                 if ((mStatusFlags & STATUS_PAUSED) != 0) {
668                     parts.add(statusClause("=", Downloads.Impl.STATUS_PAUSED_BY_APP));
669                     parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_TO_RETRY));
670                     parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_FOR_NETWORK));
671                     parts.add(statusClause("=", Downloads.Impl.STATUS_QUEUED_FOR_WIFI));
672                 }
673                 if ((mStatusFlags & STATUS_SUCCESSFUL) != 0) {
674                     parts.add(statusClause("=", Downloads.STATUS_SUCCESS));
675                 }
676                 if ((mStatusFlags & STATUS_FAILED) != 0) {
677                     parts.add("(" + statusClause(">=", 400)
678                               + " AND " + statusClause("<", 600) + ")");
679                 }
680                 selectionParts.add(joinStrings(" OR ", parts));
681             }
682 
683             if (mOnlyIncludeVisibleInDownloadsUi) {
684                 selectionParts.add(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + " != '0'");
685             }
686 
687             // only return rows which are not marked 'deleted = 1'
688             selectionParts.add(Downloads.Impl.COLUMN_DELETED + " != '1'");
689 
690             String selection = joinStrings(" AND ", selectionParts);
691             String orderDirection = (mOrderDirection == ORDER_ASCENDING ? "ASC" : "DESC");
692             String orderBy = mOrderByColumn + " " + orderDirection;
693 
694             return resolver.query(uri, projection, selection, selectionArgs, orderBy);
695         }
696 
joinStrings(String joiner, Iterable<String> parts)697         private String joinStrings(String joiner, Iterable<String> parts) {
698             StringBuilder builder = new StringBuilder();
699             boolean first = true;
700             for (String part : parts) {
701                 if (!first) {
702                     builder.append(joiner);
703                 }
704                 builder.append(part);
705                 first = false;
706             }
707             return builder.toString();
708         }
709 
statusClause(String operator, int value)710         private String statusClause(String operator, int value) {
711             return Downloads.COLUMN_STATUS + operator + "'" + value + "'";
712         }
713     }
714 
715     private ContentResolver mResolver;
716     private String mPackageName;
717     private Uri mBaseUri = Downloads.Impl.CONTENT_URI;
718 
719     /**
720      * @hide
721      */
DownloadManager(ContentResolver resolver, String packageName)722     public DownloadManager(ContentResolver resolver, String packageName) {
723         mResolver = resolver;
724         mPackageName = packageName;
725     }
726 
727     /**
728      * Makes this object access the download provider through /all_downloads URIs rather than
729      * /my_downloads URIs, for clients that have permission to do so.
730      * @hide
731      */
setAccessAllDownloads(boolean accessAllDownloads)732     public void setAccessAllDownloads(boolean accessAllDownloads) {
733         if (accessAllDownloads) {
734             mBaseUri = Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
735         } else {
736             mBaseUri = Downloads.Impl.CONTENT_URI;
737         }
738     }
739 
740     /**
741      * Enqueue a new download.  The download will start automatically once the download manager is
742      * ready to execute it and connectivity is available.
743      *
744      * @param request the parameters specifying this download
745      * @return an ID for the download, unique across the system.  This ID is used to make future
746      * calls related to this download.
747      */
enqueue(Request request)748     public long enqueue(Request request) {
749         ContentValues values = request.toContentValues(mPackageName);
750         Uri downloadUri = mResolver.insert(Downloads.CONTENT_URI, values);
751         long id = Long.parseLong(downloadUri.getLastPathSegment());
752         return id;
753     }
754 
755     /**
756      * Marks the specified download as 'to be deleted'. This is done when a completed download
757      * is to be removed but the row was stored without enough info to delete the corresponding
758      * metadata from Mediaprovider database. Actual cleanup of this row is done in DownloadService.
759      *
760      * @param ids the IDs of the downloads to be marked 'deleted'
761      * @return the number of downloads actually updated
762      * @hide
763      */
markRowDeleted(long... ids)764     public int markRowDeleted(long... ids) {
765         if (ids == null || ids.length == 0) {
766             // called with nothing to remove!
767             throw new IllegalArgumentException("input param 'ids' can't be null");
768         }
769         ContentValues values = new ContentValues();
770         values.put(Downloads.Impl.COLUMN_DELETED, 1);
771         return mResolver.update(mBaseUri, values, getWhereClauseForIds(ids),
772                 getWhereArgsForIds(ids));
773     }
774 
775     /**
776      * Cancel downloads and remove them from the download manager.  Each download will be stopped if
777      * it was running, and it will no longer be accessible through the download manager.  If a file
778      * was already downloaded to external storage, it will not be deleted.
779      *
780      * @param ids the IDs of the downloads to remove
781      * @return the number of downloads actually removed
782      */
remove(long... ids)783     public int remove(long... ids) {
784         if (ids == null || ids.length == 0) {
785             // called with nothing to remove!
786             throw new IllegalArgumentException("input param 'ids' can't be null");
787         }
788         return mResolver.delete(mBaseUri, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
789     }
790 
791     /**
792      * Query the download manager about downloads that have been requested.
793      * @param query parameters specifying filters for this query
794      * @return a Cursor over the result set of downloads, with columns consisting of all the
795      * COLUMN_* constants.
796      */
query(Query query)797     public Cursor query(Query query) {
798         Cursor underlyingCursor = query.runQuery(mResolver, UNDERLYING_COLUMNS, mBaseUri);
799         if (underlyingCursor == null) {
800             return null;
801         }
802         return new CursorTranslator(underlyingCursor, mBaseUri);
803     }
804 
805     /**
806      * Open a downloaded file for reading.  The download must have completed.
807      * @param id the ID of the download
808      * @return a read-only {@link ParcelFileDescriptor}
809      * @throws FileNotFoundException if the destination file does not already exist
810      */
openDownloadedFile(long id)811     public ParcelFileDescriptor openDownloadedFile(long id) throws FileNotFoundException {
812         return mResolver.openFileDescriptor(getDownloadUri(id), "r");
813     }
814 
815     /**
816      * Restart the given downloads, which must have already completed (successfully or not).  This
817      * method will only work when called from within the download manager's process.
818      * @param ids the IDs of the downloads
819      * @hide
820      */
restartDownload(long... ids)821     public void restartDownload(long... ids) {
822         Cursor cursor = query(new Query().setFilterById(ids));
823         try {
824             for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
825                 int status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS));
826                 if (status != STATUS_SUCCESSFUL && status != STATUS_FAILED) {
827                     throw new IllegalArgumentException("Cannot restart incomplete download: "
828                             + cursor.getLong(cursor.getColumnIndex(COLUMN_ID)));
829                 }
830             }
831         } finally {
832             cursor.close();
833         }
834 
835         ContentValues values = new ContentValues();
836         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
837         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
838         values.putNull(Downloads.Impl._DATA);
839         values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
840         mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
841     }
842 
843     /**
844      * Get the DownloadProvider URI for the download with the given ID.
845      */
getDownloadUri(long id)846     Uri getDownloadUri(long id) {
847         return ContentUris.withAppendedId(mBaseUri, id);
848     }
849 
850     /**
851      * Get a parameterized SQL WHERE clause to select a bunch of IDs.
852      */
getWhereClauseForIds(long[] ids)853     static String getWhereClauseForIds(long[] ids) {
854         StringBuilder whereClause = new StringBuilder();
855         whereClause.append("(");
856         for (int i = 0; i < ids.length; i++) {
857             if (i > 0) {
858                 whereClause.append("OR ");
859             }
860             whereClause.append(Downloads.Impl._ID);
861             whereClause.append(" = ? ");
862         }
863         whereClause.append(")");
864         return whereClause.toString();
865     }
866 
867     /**
868      * Get the selection args for a clause returned by {@link #getWhereClauseForIds(long[])}.
869      */
getWhereArgsForIds(long[] ids)870     static String[] getWhereArgsForIds(long[] ids) {
871         String[] whereArgs = new String[ids.length];
872         for (int i = 0; i < ids.length; i++) {
873             whereArgs[i] = Long.toString(ids[i]);
874         }
875         return whereArgs;
876     }
877 
878     /**
879      * This class wraps a cursor returned by DownloadProvider -- the "underlying cursor" -- and
880      * presents a different set of columns, those defined in the DownloadManager.COLUMN_* constants.
881      * Some columns correspond directly to underlying values while others are computed from
882      * underlying data.
883      */
884     private static class CursorTranslator extends CursorWrapper {
885         private Uri mBaseUri;
886 
CursorTranslator(Cursor cursor, Uri baseUri)887         public CursorTranslator(Cursor cursor, Uri baseUri) {
888             super(cursor);
889             mBaseUri = baseUri;
890         }
891 
892         @Override
getColumnIndex(String columnName)893         public int getColumnIndex(String columnName) {
894             return Arrays.asList(COLUMNS).indexOf(columnName);
895         }
896 
897         @Override
getColumnIndexOrThrow(String columnName)898         public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
899             int index = getColumnIndex(columnName);
900             if (index == -1) {
901                 throw new IllegalArgumentException("No such column: " + columnName);
902             }
903             return index;
904         }
905 
906         @Override
getColumnName(int columnIndex)907         public String getColumnName(int columnIndex) {
908             int numColumns = COLUMNS.length;
909             if (columnIndex < 0 || columnIndex >= numColumns) {
910                 throw new IllegalArgumentException("Invalid column index " + columnIndex + ", "
911                                                    + numColumns + " columns exist");
912             }
913             return COLUMNS[columnIndex];
914         }
915 
916         @Override
getColumnNames()917         public String[] getColumnNames() {
918             String[] returnColumns = new String[COLUMNS.length];
919             System.arraycopy(COLUMNS, 0, returnColumns, 0, COLUMNS.length);
920             return returnColumns;
921         }
922 
923         @Override
getColumnCount()924         public int getColumnCount() {
925             return COLUMNS.length;
926         }
927 
928         @Override
getBlob(int columnIndex)929         public byte[] getBlob(int columnIndex) {
930             throw new UnsupportedOperationException();
931         }
932 
933         @Override
getDouble(int columnIndex)934         public double getDouble(int columnIndex) {
935             return getLong(columnIndex);
936         }
937 
isLongColumn(String column)938         private boolean isLongColumn(String column) {
939             return LONG_COLUMNS.contains(column);
940         }
941 
942         @Override
getFloat(int columnIndex)943         public float getFloat(int columnIndex) {
944             return (float) getDouble(columnIndex);
945         }
946 
947         @Override
getInt(int columnIndex)948         public int getInt(int columnIndex) {
949             return (int) getLong(columnIndex);
950         }
951 
952         @Override
getLong(int columnIndex)953         public long getLong(int columnIndex) {
954             return translateLong(getColumnName(columnIndex));
955         }
956 
957         @Override
getShort(int columnIndex)958         public short getShort(int columnIndex) {
959             return (short) getLong(columnIndex);
960         }
961 
962         @Override
getString(int columnIndex)963         public String getString(int columnIndex) {
964             return translateString(getColumnName(columnIndex));
965         }
966 
translateString(String column)967         private String translateString(String column) {
968             if (isLongColumn(column)) {
969                 return Long.toString(translateLong(column));
970             }
971             if (column.equals(COLUMN_TITLE)) {
972                 return getUnderlyingString(Downloads.COLUMN_TITLE);
973             }
974             if (column.equals(COLUMN_DESCRIPTION)) {
975                 return getUnderlyingString(Downloads.COLUMN_DESCRIPTION);
976             }
977             if (column.equals(COLUMN_URI)) {
978                 return getUnderlyingString(Downloads.COLUMN_URI);
979             }
980             if (column.equals(COLUMN_MEDIA_TYPE)) {
981                 return getUnderlyingString(Downloads.COLUMN_MIME_TYPE);
982             }
983             if (column.equals(COLUMN_MEDIAPROVIDER_URI)) {
984                 return getUnderlyingString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
985             }
986 
987             assert column.equals(COLUMN_LOCAL_URI);
988             return getLocalUri();
989         }
990 
getLocalUri()991         private String getLocalUri() {
992             long destinationType = getUnderlyingLong(Downloads.Impl.COLUMN_DESTINATION);
993             if (destinationType == Downloads.Impl.DESTINATION_FILE_URI) {
994                 // return client-provided file URI for external download
995                 return getUnderlyingString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
996             }
997 
998             if (destinationType == Downloads.Impl.DESTINATION_EXTERNAL) {
999                 // return stored destination for legacy external download
1000                 String localPath = getUnderlyingString(Downloads.Impl._DATA);
1001                 if (localPath == null) {
1002                     return null;
1003                 }
1004                 return Uri.fromFile(new File(localPath)).toString();
1005             }
1006 
1007             // return content URI for cache download
1008             long downloadId = getUnderlyingLong(Downloads.Impl._ID);
1009             return ContentUris.withAppendedId(mBaseUri, downloadId).toString();
1010         }
1011 
translateLong(String column)1012         private long translateLong(String column) {
1013             if (!isLongColumn(column)) {
1014                 // mimic behavior of underlying cursor -- most likely, throw NumberFormatException
1015                 return Long.valueOf(translateString(column));
1016             }
1017 
1018             if (column.equals(COLUMN_ID)) {
1019                 return getUnderlyingLong(Downloads.Impl._ID);
1020             }
1021             if (column.equals(COLUMN_TOTAL_SIZE_BYTES)) {
1022                 return getUnderlyingLong(Downloads.COLUMN_TOTAL_BYTES);
1023             }
1024             if (column.equals(COLUMN_STATUS)) {
1025                 return translateStatus((int) getUnderlyingLong(Downloads.COLUMN_STATUS));
1026             }
1027             if (column.equals(COLUMN_REASON)) {
1028                 return getReason((int) getUnderlyingLong(Downloads.COLUMN_STATUS));
1029             }
1030             if (column.equals(COLUMN_BYTES_DOWNLOADED_SO_FAR)) {
1031                 return getUnderlyingLong(Downloads.COLUMN_CURRENT_BYTES);
1032             }
1033             assert column.equals(COLUMN_LAST_MODIFIED_TIMESTAMP);
1034             return getUnderlyingLong(Downloads.COLUMN_LAST_MODIFICATION);
1035         }
1036 
getReason(int status)1037         private long getReason(int status) {
1038             switch (translateStatus(status)) {
1039                 case STATUS_FAILED:
1040                     return getErrorCode(status);
1041 
1042                 case STATUS_PAUSED:
1043                     return getPausedReason(status);
1044 
1045                 default:
1046                     return 0; // arbitrary value when status is not an error
1047             }
1048         }
1049 
getPausedReason(int status)1050         private long getPausedReason(int status) {
1051             switch (status) {
1052                 case Downloads.Impl.STATUS_WAITING_TO_RETRY:
1053                     return PAUSED_WAITING_TO_RETRY;
1054 
1055                 case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
1056                     return PAUSED_WAITING_FOR_NETWORK;
1057 
1058                 case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
1059                     return PAUSED_QUEUED_FOR_WIFI;
1060 
1061                 default:
1062                     return PAUSED_UNKNOWN;
1063             }
1064         }
1065 
getErrorCode(int status)1066         private long getErrorCode(int status) {
1067             if ((400 <= status && status < Downloads.Impl.MIN_ARTIFICIAL_ERROR_STATUS)
1068                     || (500 <= status && status < 600)) {
1069                 // HTTP status code
1070                 return status;
1071             }
1072 
1073             switch (status) {
1074                 case Downloads.STATUS_FILE_ERROR:
1075                     return ERROR_FILE_ERROR;
1076 
1077                 case Downloads.STATUS_UNHANDLED_HTTP_CODE:
1078                 case Downloads.STATUS_UNHANDLED_REDIRECT:
1079                     return ERROR_UNHANDLED_HTTP_CODE;
1080 
1081                 case Downloads.STATUS_HTTP_DATA_ERROR:
1082                     return ERROR_HTTP_DATA_ERROR;
1083 
1084                 case Downloads.STATUS_TOO_MANY_REDIRECTS:
1085                     return ERROR_TOO_MANY_REDIRECTS;
1086 
1087                 case Downloads.STATUS_INSUFFICIENT_SPACE_ERROR:
1088                     return ERROR_INSUFFICIENT_SPACE;
1089 
1090                 case Downloads.STATUS_DEVICE_NOT_FOUND_ERROR:
1091                     return ERROR_DEVICE_NOT_FOUND;
1092 
1093                 case Downloads.Impl.STATUS_CANNOT_RESUME:
1094                     return ERROR_CANNOT_RESUME;
1095 
1096                 case Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR:
1097                     return ERROR_FILE_ALREADY_EXISTS;
1098 
1099                 default:
1100                     return ERROR_UNKNOWN;
1101             }
1102         }
1103 
getUnderlyingLong(String column)1104         private long getUnderlyingLong(String column) {
1105             return super.getLong(super.getColumnIndex(column));
1106         }
1107 
getUnderlyingString(String column)1108         private String getUnderlyingString(String column) {
1109             return super.getString(super.getColumnIndex(column));
1110         }
1111 
translateStatus(int status)1112         private int translateStatus(int status) {
1113             switch (status) {
1114                 case Downloads.STATUS_PENDING:
1115                     return STATUS_PENDING;
1116 
1117                 case Downloads.STATUS_RUNNING:
1118                     return STATUS_RUNNING;
1119 
1120                 case Downloads.Impl.STATUS_PAUSED_BY_APP:
1121                 case Downloads.Impl.STATUS_WAITING_TO_RETRY:
1122                 case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
1123                 case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
1124                     return STATUS_PAUSED;
1125 
1126                 case Downloads.STATUS_SUCCESS:
1127                     return STATUS_SUCCESSFUL;
1128 
1129                 default:
1130                     assert Downloads.isStatusError(status);
1131                     return STATUS_FAILED;
1132             }
1133         }
1134     }
1135 }
1136