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
17package android.app;
18
19import android.annotation.SdkConstant;
20import android.annotation.SdkConstant.SdkConstantType;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.database.Cursor;
26import android.database.CursorWrapper;
27import android.net.ConnectivityManager;
28import android.net.NetworkPolicyManager;
29import android.net.Uri;
30import android.os.Environment;
31import android.os.ParcelFileDescriptor;
32import android.provider.Downloads;
33import android.provider.Settings;
34import android.provider.Settings.SettingNotFoundException;
35import android.text.TextUtils;
36import android.util.Pair;
37
38import java.io.File;
39import java.io.FileNotFoundException;
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * The download manager is a system service that handles long-running HTTP downloads. Clients may
45 * request that a URI be downloaded to a particular destination file. The download manager will
46 * conduct the download in the background, taking care of HTTP interactions and retrying downloads
47 * after failures or across connectivity changes and system reboots.
48 *
49 * Instances of this class should be obtained through
50 * {@link android.content.Context#getSystemService(String)} by passing
51 * {@link android.content.Context#DOWNLOAD_SERVICE}.
52 *
53 * Apps that request downloads through this API should register a broadcast receiver for
54 * {@link #ACTION_NOTIFICATION_CLICKED} to appropriately handle when the user clicks on a running
55 * download in a notification or from the downloads UI.
56 *
57 * Note that the application must have the {@link android.Manifest.permission#INTERNET}
58 * permission to use this class.
59 */
60public class DownloadManager {
61
62    /**
63     * An identifier for a particular download, unique across the system.  Clients use this ID to
64     * make subsequent calls related to the download.
65     */
66    public final static String COLUMN_ID = Downloads.Impl._ID;
67
68    /**
69     * The client-supplied title for this download.  This will be displayed in system notifications.
70     * Defaults to the empty string.
71     */
72    public final static String COLUMN_TITLE = Downloads.Impl.COLUMN_TITLE;
73
74    /**
75     * The client-supplied description of this download.  This will be displayed in system
76     * notifications.  Defaults to the empty string.
77     */
78    public final static String COLUMN_DESCRIPTION = Downloads.Impl.COLUMN_DESCRIPTION;
79
80    /**
81     * URI to be downloaded.
82     */
83    public final static String COLUMN_URI = Downloads.Impl.COLUMN_URI;
84
85    /**
86     * Internet Media Type of the downloaded file.  If no value is provided upon creation, this will
87     * initially be null and will be filled in based on the server's response once the download has
88     * started.
89     *
90     * @see <a href="http://www.ietf.org/rfc/rfc1590.txt">RFC 1590, defining Media Types</a>
91     */
92    public final static String COLUMN_MEDIA_TYPE = "media_type";
93
94    /**
95     * Total size of the download in bytes.  This will initially be -1 and will be filled in once
96     * the download starts.
97     */
98    public final static String COLUMN_TOTAL_SIZE_BYTES = "total_size";
99
100    /**
101     * Uri where downloaded file will be stored.  If a destination is supplied by client, that URI
102     * will be used here.  Otherwise, the value will initially be null and will be filled in with a
103     * generated URI once the download has started.
104     */
105    public final static String COLUMN_LOCAL_URI = "local_uri";
106
107    /**
108     * The pathname of the file where the download is stored.
109     */
110    public final static String COLUMN_LOCAL_FILENAME = "local_filename";
111
112    /**
113     * Current status of the download, as one of the STATUS_* constants.
114     */
115    public final static String COLUMN_STATUS = Downloads.Impl.COLUMN_STATUS;
116
117    /**
118     * Provides more detail on the status of the download.  Its meaning depends on the value of
119     * {@link #COLUMN_STATUS}.
120     *
121     * When {@link #COLUMN_STATUS} is {@link #STATUS_FAILED}, this indicates the type of error that
122     * occurred.  If an HTTP error occurred, this will hold the HTTP status code as defined in RFC
123     * 2616.  Otherwise, it will hold one of the ERROR_* constants.
124     *
125     * When {@link #COLUMN_STATUS} is {@link #STATUS_PAUSED}, this indicates why the download is
126     * paused.  It will hold one of the PAUSED_* constants.
127     *
128     * If {@link #COLUMN_STATUS} is neither {@link #STATUS_FAILED} nor {@link #STATUS_PAUSED}, this
129     * column's value is undefined.
130     *
131     * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1">RFC 2616
132     * status codes</a>
133     */
134    public final static String COLUMN_REASON = "reason";
135
136    /**
137     * Number of bytes download so far.
138     */
139    public final static String COLUMN_BYTES_DOWNLOADED_SO_FAR = "bytes_so_far";
140
141    /**
142     * Timestamp when the download was last modified, in {@link System#currentTimeMillis
143     * System.currentTimeMillis()} (wall clock time in UTC).
144     */
145    public final static String COLUMN_LAST_MODIFIED_TIMESTAMP = "last_modified_timestamp";
146
147    /**
148     * The URI to the corresponding entry in MediaProvider for this downloaded entry. It is
149     * used to delete the entries from MediaProvider database when it is deleted from the
150     * downloaded list.
151     */
152    public static final String COLUMN_MEDIAPROVIDER_URI = Downloads.Impl.COLUMN_MEDIAPROVIDER_URI;
153
154    /**
155     * @hide
156     */
157    public final static String COLUMN_ALLOW_WRITE = Downloads.Impl.COLUMN_ALLOW_WRITE;
158
159    /**
160     * Value of {@link #COLUMN_STATUS} when the download is waiting to start.
161     */
162    public final static int STATUS_PENDING = 1 << 0;
163
164    /**
165     * Value of {@link #COLUMN_STATUS} when the download is currently running.
166     */
167    public final static int STATUS_RUNNING = 1 << 1;
168
169    /**
170     * Value of {@link #COLUMN_STATUS} when the download is waiting to retry or resume.
171     */
172    public final static int STATUS_PAUSED = 1 << 2;
173
174    /**
175     * Value of {@link #COLUMN_STATUS} when the download has successfully completed.
176     */
177    public final static int STATUS_SUCCESSFUL = 1 << 3;
178
179    /**
180     * Value of {@link #COLUMN_STATUS} when the download has failed (and will not be retried).
181     */
182    public final static int STATUS_FAILED = 1 << 4;
183
184    /**
185     * Value of COLUMN_ERROR_CODE when the download has completed with an error that doesn't fit
186     * under any other error code.
187     */
188    public final static int ERROR_UNKNOWN = 1000;
189
190    /**
191     * Value of {@link #COLUMN_REASON} when a storage issue arises which doesn't fit under any
192     * other error code. Use the more specific {@link #ERROR_INSUFFICIENT_SPACE} and
193     * {@link #ERROR_DEVICE_NOT_FOUND} when appropriate.
194     */
195    public final static int ERROR_FILE_ERROR = 1001;
196
197    /**
198     * Value of {@link #COLUMN_REASON} when an HTTP code was received that download manager
199     * can't handle.
200     */
201    public final static int ERROR_UNHANDLED_HTTP_CODE = 1002;
202
203    /**
204     * Value of {@link #COLUMN_REASON} when an error receiving or processing data occurred at
205     * the HTTP level.
206     */
207    public final static int ERROR_HTTP_DATA_ERROR = 1004;
208
209    /**
210     * Value of {@link #COLUMN_REASON} when there were too many redirects.
211     */
212    public final static int ERROR_TOO_MANY_REDIRECTS = 1005;
213
214    /**
215     * Value of {@link #COLUMN_REASON} when there was insufficient storage space. Typically,
216     * this is because the SD card is full.
217     */
218    public final static int ERROR_INSUFFICIENT_SPACE = 1006;
219
220    /**
221     * Value of {@link #COLUMN_REASON} when no external storage device was found. Typically,
222     * this is because the SD card is not mounted.
223     */
224    public final static int ERROR_DEVICE_NOT_FOUND = 1007;
225
226    /**
227     * Value of {@link #COLUMN_REASON} when some possibly transient error occurred but we can't
228     * resume the download.
229     */
230    public final static int ERROR_CANNOT_RESUME = 1008;
231
232    /**
233     * Value of {@link #COLUMN_REASON} when the requested destination file already exists (the
234     * download manager will not overwrite an existing file).
235     */
236    public final static int ERROR_FILE_ALREADY_EXISTS = 1009;
237
238    /**
239     * Value of {@link #COLUMN_REASON} when the download has failed because of
240     * {@link NetworkPolicyManager} controls on the requesting application.
241     *
242     * @hide
243     */
244    public final static int ERROR_BLOCKED = 1010;
245
246    /**
247     * Value of {@link #COLUMN_REASON} when the download is paused because some network error
248     * occurred and the download manager is waiting before retrying the request.
249     */
250    public final static int PAUSED_WAITING_TO_RETRY = 1;
251
252    /**
253     * Value of {@link #COLUMN_REASON} when the download is waiting for network connectivity to
254     * proceed.
255     */
256    public final static int PAUSED_WAITING_FOR_NETWORK = 2;
257
258    /**
259     * Value of {@link #COLUMN_REASON} when the download exceeds a size limit for downloads over
260     * the mobile network and the download manager is waiting for a Wi-Fi connection to proceed.
261     */
262    public final static int PAUSED_QUEUED_FOR_WIFI = 3;
263
264    /**
265     * Value of {@link #COLUMN_REASON} when the download is paused for some other reason.
266     */
267    public final static int PAUSED_UNKNOWN = 4;
268
269    /**
270     * Broadcast intent action sent by the download manager when a download completes.
271     */
272    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
273    public final static String ACTION_DOWNLOAD_COMPLETE = "android.intent.action.DOWNLOAD_COMPLETE";
274
275    /**
276     * Broadcast intent action sent by the download manager when the user clicks on a running
277     * download, either from a system notification or from the downloads UI.
278     */
279    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
280    public final static String ACTION_NOTIFICATION_CLICKED =
281            "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";
282
283    /**
284     * Intent action to launch an activity to display all downloads.
285     */
286    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
287    public final static String ACTION_VIEW_DOWNLOADS = "android.intent.action.VIEW_DOWNLOADS";
288
289    /**
290     * Intent extra included with {@link #ACTION_VIEW_DOWNLOADS} to start DownloadApp in
291     * sort-by-size mode.
292     */
293    public final static String INTENT_EXTRAS_SORT_BY_SIZE =
294            "android.app.DownloadManager.extra_sortBySize";
295
296    /**
297     * Intent extra included with {@link #ACTION_DOWNLOAD_COMPLETE} intents, indicating the ID (as a
298     * long) of the download that just completed.
299     */
300    public static final String EXTRA_DOWNLOAD_ID = "extra_download_id";
301
302    /**
303     * When clicks on multiple notifications are received, the following
304     * provides an array of download ids corresponding to the download notification that was
305     * clicked. It can be retrieved by the receiver of this
306     * Intent using {@link android.content.Intent#getLongArrayExtra(String)}.
307     */
308    public static final String EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS = "extra_click_download_ids";
309
310    /**
311     * columns to request from DownloadProvider.
312     * @hide
313     */
314    public static final String[] UNDERLYING_COLUMNS = new String[] {
315        Downloads.Impl._ID,
316        Downloads.Impl._DATA + " AS " + COLUMN_LOCAL_FILENAME,
317        Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
318        Downloads.Impl.COLUMN_DESTINATION,
319        Downloads.Impl.COLUMN_TITLE,
320        Downloads.Impl.COLUMN_DESCRIPTION,
321        Downloads.Impl.COLUMN_URI,
322        Downloads.Impl.COLUMN_STATUS,
323        Downloads.Impl.COLUMN_FILE_NAME_HINT,
324        Downloads.Impl.COLUMN_MIME_TYPE + " AS " + COLUMN_MEDIA_TYPE,
325        Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + COLUMN_TOTAL_SIZE_BYTES,
326        Downloads.Impl.COLUMN_LAST_MODIFICATION + " AS " + COLUMN_LAST_MODIFIED_TIMESTAMP,
327        Downloads.Impl.COLUMN_CURRENT_BYTES + " AS " + COLUMN_BYTES_DOWNLOADED_SO_FAR,
328        Downloads.Impl.COLUMN_ALLOW_WRITE,
329        /* add the following 'computed' columns to the cursor.
330         * they are not 'returned' by the database, but their inclusion
331         * eliminates need to have lot of methods in CursorTranslator
332         */
333        "'placeholder' AS " + COLUMN_LOCAL_URI,
334        "'placeholder' AS " + COLUMN_REASON
335    };
336
337    /**
338     * This class contains all the information necessary to request a new download. The URI is the
339     * only required parameter.
340     *
341     * Note that the default download destination is a shared volume where the system might delete
342     * your file if it needs to reclaim space for system use. If this is a problem, use a location
343     * on external storage (see {@link #setDestinationUri(Uri)}.
344     */
345    public static class Request {
346        /**
347         * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
348         * {@link ConnectivityManager#TYPE_MOBILE}.
349         */
350        public static final int NETWORK_MOBILE = 1 << 0;
351
352        /**
353         * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
354         * {@link ConnectivityManager#TYPE_WIFI}.
355         */
356        public static final int NETWORK_WIFI = 1 << 1;
357
358        /**
359         * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
360         * {@link ConnectivityManager#TYPE_BLUETOOTH}.
361         * @hide
362         */
363        public static final int NETWORK_BLUETOOTH = 1 << 2;
364
365        private Uri mUri;
366        private Uri mDestinationUri;
367        private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
368        private CharSequence mTitle;
369        private CharSequence mDescription;
370        private String mMimeType;
371        private int mAllowedNetworkTypes = ~0; // default to all network types allowed
372        private boolean mRoamingAllowed = true;
373        private boolean mMeteredAllowed = true;
374        private boolean mIsVisibleInDownloadsUi = true;
375        private boolean mScannable = false;
376        private boolean mUseSystemCache = false;
377        /** if a file is designated as a MediaScanner scannable file, the following value is
378         * stored in the database column {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
379         */
380        private static final int SCANNABLE_VALUE_YES = 0;
381        // value of 1 is stored in the above column by DownloadProvider after it is scanned by
382        // MediaScanner
383        /** if a file is designated as a file that should not be scanned by MediaScanner,
384         * the following value is stored in the database column
385         * {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
386         */
387        private static final int SCANNABLE_VALUE_NO = 2;
388
389        /**
390         * This download is visible but only shows in the notifications
391         * while it's in progress.
392         */
393        public static final int VISIBILITY_VISIBLE = 0;
394
395        /**
396         * This download is visible and shows in the notifications while
397         * in progress and after completion.
398         */
399        public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
400
401        /**
402         * This download doesn't show in the UI or in the notifications.
403         */
404        public static final int VISIBILITY_HIDDEN = 2;
405
406        /**
407         * This download shows in the notifications after completion ONLY.
408         * It is usuable only with
409         * {@link DownloadManager#addCompletedDownload(String, String,
410         * boolean, String, String, long, boolean)}.
411         */
412        public static final int VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION = 3;
413
414        /** can take any of the following values: {@link #VISIBILITY_HIDDEN}
415         * {@link #VISIBILITY_VISIBLE_NOTIFY_COMPLETED}, {@link #VISIBILITY_VISIBLE},
416         * {@link #VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION}
417         */
418        private int mNotificationVisibility = VISIBILITY_VISIBLE;
419
420        /**
421         * @param uri the HTTP URI to download.
422         */
423        public Request(Uri uri) {
424            if (uri == null) {
425                throw new NullPointerException();
426            }
427            String scheme = uri.getScheme();
428            if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
429                throw new IllegalArgumentException("Can only download HTTP/HTTPS URIs: " + uri);
430            }
431            mUri = uri;
432        }
433
434        Request(String uriString) {
435            mUri = Uri.parse(uriString);
436        }
437
438        /**
439         * Set the local destination for the downloaded file. Must be a file URI to a path on
440         * external storage, and the calling application must have the WRITE_EXTERNAL_STORAGE
441         * permission.
442         * <p>
443         * The downloaded file is not scanned by MediaScanner.
444         * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}.
445         * <p>
446         * By default, downloads are saved to a generated filename in the shared download cache and
447         * may be deleted by the system at any time to reclaim space.
448         *
449         * @return this object
450         */
451        public Request setDestinationUri(Uri uri) {
452            mDestinationUri = uri;
453            return this;
454        }
455
456        /**
457         * Set the local destination for the downloaded file to the system cache dir (/cache).
458         * This is only available to System apps with the permission
459         * {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM}.
460         * <p>
461         * The downloaded file is not scanned by MediaScanner.
462         * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}.
463         * <p>
464         * Files downloaded to /cache may be deleted by the system at any time to reclaim space.
465         *
466         * @return this object
467         * @hide
468         */
469        public Request setDestinationToSystemCache() {
470            mUseSystemCache = true;
471            return this;
472        }
473
474        /**
475         * Set the local destination for the downloaded file to a path within
476         * the application's external files directory (as returned by
477         * {@link Context#getExternalFilesDir(String)}.
478         * <p>
479         * The downloaded file is not scanned by MediaScanner. But it can be
480         * made scannable by calling {@link #allowScanningByMediaScanner()}.
481         *
482         * @param context the {@link Context} to use in determining the external
483         *            files directory
484         * @param dirType the directory type to pass to
485         *            {@link Context#getExternalFilesDir(String)}
486         * @param subPath the path within the external directory, including the
487         *            destination filename
488         * @return this object
489         * @throws IllegalStateException If the external storage directory
490         *             cannot be found or created.
491         */
492        public Request setDestinationInExternalFilesDir(Context context, String dirType,
493                String subPath) {
494            final File file = context.getExternalFilesDir(dirType);
495            if (file == null) {
496                throw new IllegalStateException("Failed to get external storage files directory");
497            } else if (file.exists()) {
498                if (!file.isDirectory()) {
499                    throw new IllegalStateException(file.getAbsolutePath() +
500                            " already exists and is not a directory");
501                }
502            } else {
503                if (!file.mkdirs()) {
504                    throw new IllegalStateException("Unable to create directory: "+
505                            file.getAbsolutePath());
506                }
507            }
508            setDestinationFromBase(file, subPath);
509            return this;
510        }
511
512        /**
513         * Set the local destination for the downloaded file to a path within
514         * the public external storage directory (as returned by
515         * {@link Environment#getExternalStoragePublicDirectory(String)}).
516         * <p>
517         * The downloaded file is not scanned by MediaScanner. But it can be
518         * made scannable by calling {@link #allowScanningByMediaScanner()}.
519         *
520         * @param dirType the directory type to pass to {@link Environment#getExternalStoragePublicDirectory(String)}
521         * @param subPath the path within the external directory, including the
522         *            destination filename
523         * @return this object
524         * @throws IllegalStateException If the external storage directory
525         *             cannot be found or created.
526         */
527        public Request setDestinationInExternalPublicDir(String dirType, String subPath) {
528            File file = Environment.getExternalStoragePublicDirectory(dirType);
529            if (file == null) {
530                throw new IllegalStateException("Failed to get external storage public directory");
531            } else if (file.exists()) {
532                if (!file.isDirectory()) {
533                    throw new IllegalStateException(file.getAbsolutePath() +
534                            " already exists and is not a directory");
535                }
536            } else {
537                if (!file.mkdirs()) {
538                    throw new IllegalStateException("Unable to create directory: "+
539                            file.getAbsolutePath());
540                }
541            }
542            setDestinationFromBase(file, subPath);
543            return this;
544        }
545
546        private void setDestinationFromBase(File base, String subPath) {
547            if (subPath == null) {
548                throw new NullPointerException("subPath cannot be null");
549            }
550            mDestinationUri = Uri.withAppendedPath(Uri.fromFile(base), subPath);
551        }
552
553        /**
554         * If the file to be downloaded is to be scanned by MediaScanner, this method
555         * should be called before {@link DownloadManager#enqueue(Request)} is called.
556         */
557        public void allowScanningByMediaScanner() {
558            mScannable = true;
559        }
560
561        /**
562         * Add an HTTP header to be included with the download request.  The header will be added to
563         * the end of the list.
564         * @param header HTTP header name
565         * @param value header value
566         * @return this object
567         * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2">HTTP/1.1
568         *      Message Headers</a>
569         */
570        public Request addRequestHeader(String header, String value) {
571            if (header == null) {
572                throw new NullPointerException("header cannot be null");
573            }
574            if (header.contains(":")) {
575                throw new IllegalArgumentException("header may not contain ':'");
576            }
577            if (value == null) {
578                value = "";
579            }
580            mRequestHeaders.add(Pair.create(header, value));
581            return this;
582        }
583
584        /**
585         * Set the title of this download, to be displayed in notifications (if enabled).  If no
586         * title is given, a default one will be assigned based on the download filename, once the
587         * download starts.
588         * @return this object
589         */
590        public Request setTitle(CharSequence title) {
591            mTitle = title;
592            return this;
593        }
594
595        /**
596         * Set a description of this download, to be displayed in notifications (if enabled)
597         * @return this object
598         */
599        public Request setDescription(CharSequence description) {
600            mDescription = description;
601            return this;
602        }
603
604        /**
605         * Set the MIME content type of this download.  This will override the content type declared
606         * in the server's response.
607         * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7">HTTP/1.1
608         *      Media Types</a>
609         * @return this object
610         */
611        public Request setMimeType(String mimeType) {
612            mMimeType = mimeType;
613            return this;
614        }
615
616        /**
617         * Control whether a system notification is posted by the download manager while this
618         * download is running. If enabled, the download manager posts notifications about downloads
619         * through the system {@link android.app.NotificationManager}. By default, a notification is
620         * shown.
621         *
622         * If set to false, this requires the permission
623         * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
624         *
625         * @param show whether the download manager should show a notification for this download.
626         * @return this object
627         * @deprecated use {@link #setNotificationVisibility(int)}
628         */
629        @Deprecated
630        public Request setShowRunningNotification(boolean show) {
631            return (show) ? setNotificationVisibility(VISIBILITY_VISIBLE) :
632                    setNotificationVisibility(VISIBILITY_HIDDEN);
633        }
634
635        /**
636         * Control whether a system notification is posted by the download manager while this
637         * download is running or when it is completed.
638         * If enabled, the download manager posts notifications about downloads
639         * through the system {@link android.app.NotificationManager}.
640         * By default, a notification is shown only when the download is in progress.
641         *<p>
642         * It can take the following values: {@link #VISIBILITY_HIDDEN},
643         * {@link #VISIBILITY_VISIBLE},
644         * {@link #VISIBILITY_VISIBLE_NOTIFY_COMPLETED}.
645         *<p>
646         * If set to {@link #VISIBILITY_HIDDEN}, this requires the permission
647         * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
648         *
649         * @param visibility the visibility setting value
650         * @return this object
651         */
652        public Request setNotificationVisibility(int visibility) {
653            mNotificationVisibility = visibility;
654            return this;
655        }
656
657        /**
658         * Restrict the types of networks over which this download may proceed.
659         * By default, all network types are allowed. Consider using
660         * {@link #setAllowedOverMetered(boolean)} instead, since it's more
661         * flexible.
662         *
663         * @param flags any combination of the NETWORK_* bit flags.
664         * @return this object
665         */
666        public Request setAllowedNetworkTypes(int flags) {
667            mAllowedNetworkTypes = flags;
668            return this;
669        }
670
671        /**
672         * Set whether this download may proceed over a roaming connection.  By default, roaming is
673         * allowed.
674         * @param allowed whether to allow a roaming connection to be used
675         * @return this object
676         */
677        public Request setAllowedOverRoaming(boolean allowed) {
678            mRoamingAllowed = allowed;
679            return this;
680        }
681
682        /**
683         * Set whether this download may proceed over a metered network
684         * connection. By default, metered networks are allowed.
685         *
686         * @see ConnectivityManager#isActiveNetworkMetered()
687         */
688        public Request setAllowedOverMetered(boolean allow) {
689            mMeteredAllowed = allow;
690            return this;
691        }
692
693        /**
694         * Set whether this download should be displayed in the system's Downloads UI. True by
695         * default.
696         * @param isVisible whether to display this download in the Downloads UI
697         * @return this object
698         */
699        public Request setVisibleInDownloadsUi(boolean isVisible) {
700            mIsVisibleInDownloadsUi = isVisible;
701            return this;
702        }
703
704        /**
705         * @return ContentValues to be passed to DownloadProvider.insert()
706         */
707        ContentValues toContentValues(String packageName) {
708            ContentValues values = new ContentValues();
709            assert mUri != null;
710            values.put(Downloads.Impl.COLUMN_URI, mUri.toString());
711            values.put(Downloads.Impl.COLUMN_IS_PUBLIC_API, true);
712            values.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, packageName);
713
714            if (mDestinationUri != null) {
715                values.put(Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.DESTINATION_FILE_URI);
716                values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, mDestinationUri.toString());
717            } else {
718                values.put(Downloads.Impl.COLUMN_DESTINATION,
719                           (this.mUseSystemCache) ?
720                                   Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION :
721                                   Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
722            }
723            // is the file supposed to be media-scannable?
724            values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, (mScannable) ? SCANNABLE_VALUE_YES :
725                    SCANNABLE_VALUE_NO);
726
727            if (!mRequestHeaders.isEmpty()) {
728                encodeHttpHeaders(values);
729            }
730
731            putIfNonNull(values, Downloads.Impl.COLUMN_TITLE, mTitle);
732            putIfNonNull(values, Downloads.Impl.COLUMN_DESCRIPTION, mDescription);
733            putIfNonNull(values, Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
734
735            values.put(Downloads.Impl.COLUMN_VISIBILITY, mNotificationVisibility);
736            values.put(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, mAllowedNetworkTypes);
737            values.put(Downloads.Impl.COLUMN_ALLOW_ROAMING, mRoamingAllowed);
738            values.put(Downloads.Impl.COLUMN_ALLOW_METERED, mMeteredAllowed);
739            values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, mIsVisibleInDownloadsUi);
740
741            return values;
742        }
743
744        private void encodeHttpHeaders(ContentValues values) {
745            int index = 0;
746            for (Pair<String, String> header : mRequestHeaders) {
747                String headerString = header.first + ": " + header.second;
748                values.put(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX + index, headerString);
749                index++;
750            }
751        }
752
753        private void putIfNonNull(ContentValues contentValues, String key, Object value) {
754            if (value != null) {
755                contentValues.put(key, value.toString());
756            }
757        }
758    }
759
760    /**
761     * This class may be used to filter download manager queries.
762     */
763    public static class Query {
764        /**
765         * Constant for use with {@link #orderBy}
766         * @hide
767         */
768        public static final int ORDER_ASCENDING = 1;
769
770        /**
771         * Constant for use with {@link #orderBy}
772         * @hide
773         */
774        public static final int ORDER_DESCENDING = 2;
775
776        private long[] mIds = null;
777        private Integer mStatusFlags = null;
778        private String mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
779        private int mOrderDirection = ORDER_DESCENDING;
780        private boolean mOnlyIncludeVisibleInDownloadsUi = false;
781
782        /**
783         * Include only the downloads with the given IDs.
784         * @return this object
785         */
786        public Query setFilterById(long... ids) {
787            mIds = ids;
788            return this;
789        }
790
791        /**
792         * Include only downloads with status matching any the given status flags.
793         * @param flags any combination of the STATUS_* bit flags
794         * @return this object
795         */
796        public Query setFilterByStatus(int flags) {
797            mStatusFlags = flags;
798            return this;
799        }
800
801        /**
802         * Controls whether this query includes downloads not visible in the system's Downloads UI.
803         * @param value if true, this query will only include downloads that should be displayed in
804         *            the system's Downloads UI; if false (the default), this query will include
805         *            both visible and invisible downloads.
806         * @return this object
807         * @hide
808         */
809        public Query setOnlyIncludeVisibleInDownloadsUi(boolean value) {
810            mOnlyIncludeVisibleInDownloadsUi = value;
811            return this;
812        }
813
814        /**
815         * Change the sort order of the returned Cursor.
816         *
817         * @param column one of the COLUMN_* constants; currently, only
818         *         {@link #COLUMN_LAST_MODIFIED_TIMESTAMP} and {@link #COLUMN_TOTAL_SIZE_BYTES} are
819         *         supported.
820         * @param direction either {@link #ORDER_ASCENDING} or {@link #ORDER_DESCENDING}
821         * @return this object
822         * @hide
823         */
824        public Query orderBy(String column, int direction) {
825            if (direction != ORDER_ASCENDING && direction != ORDER_DESCENDING) {
826                throw new IllegalArgumentException("Invalid direction: " + direction);
827            }
828
829            if (column.equals(COLUMN_LAST_MODIFIED_TIMESTAMP)) {
830                mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
831            } else if (column.equals(COLUMN_TOTAL_SIZE_BYTES)) {
832                mOrderByColumn = Downloads.Impl.COLUMN_TOTAL_BYTES;
833            } else {
834                throw new IllegalArgumentException("Cannot order by " + column);
835            }
836            mOrderDirection = direction;
837            return this;
838        }
839
840        /**
841         * Run this query using the given ContentResolver.
842         * @param projection the projection to pass to ContentResolver.query()
843         * @return the Cursor returned by ContentResolver.query()
844         */
845        Cursor runQuery(ContentResolver resolver, String[] projection, Uri baseUri) {
846            Uri uri = baseUri;
847            List<String> selectionParts = new ArrayList<String>();
848            String[] selectionArgs = null;
849
850            if (mIds != null) {
851                selectionParts.add(getWhereClauseForIds(mIds));
852                selectionArgs = getWhereArgsForIds(mIds);
853            }
854
855            if (mStatusFlags != null) {
856                List<String> parts = new ArrayList<String>();
857                if ((mStatusFlags & STATUS_PENDING) != 0) {
858                    parts.add(statusClause("=", Downloads.Impl.STATUS_PENDING));
859                }
860                if ((mStatusFlags & STATUS_RUNNING) != 0) {
861                    parts.add(statusClause("=", Downloads.Impl.STATUS_RUNNING));
862                }
863                if ((mStatusFlags & STATUS_PAUSED) != 0) {
864                    parts.add(statusClause("=", Downloads.Impl.STATUS_PAUSED_BY_APP));
865                    parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_TO_RETRY));
866                    parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_FOR_NETWORK));
867                    parts.add(statusClause("=", Downloads.Impl.STATUS_QUEUED_FOR_WIFI));
868                }
869                if ((mStatusFlags & STATUS_SUCCESSFUL) != 0) {
870                    parts.add(statusClause("=", Downloads.Impl.STATUS_SUCCESS));
871                }
872                if ((mStatusFlags & STATUS_FAILED) != 0) {
873                    parts.add("(" + statusClause(">=", 400)
874                              + " AND " + statusClause("<", 600) + ")");
875                }
876                selectionParts.add(joinStrings(" OR ", parts));
877            }
878
879            if (mOnlyIncludeVisibleInDownloadsUi) {
880                selectionParts.add(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + " != '0'");
881            }
882
883            // only return rows which are not marked 'deleted = 1'
884            selectionParts.add(Downloads.Impl.COLUMN_DELETED + " != '1'");
885
886            String selection = joinStrings(" AND ", selectionParts);
887            String orderDirection = (mOrderDirection == ORDER_ASCENDING ? "ASC" : "DESC");
888            String orderBy = mOrderByColumn + " " + orderDirection;
889
890            return resolver.query(uri, projection, selection, selectionArgs, orderBy);
891        }
892
893        private String joinStrings(String joiner, Iterable<String> parts) {
894            StringBuilder builder = new StringBuilder();
895            boolean first = true;
896            for (String part : parts) {
897                if (!first) {
898                    builder.append(joiner);
899                }
900                builder.append(part);
901                first = false;
902            }
903            return builder.toString();
904        }
905
906        private String statusClause(String operator, int value) {
907            return Downloads.Impl.COLUMN_STATUS + operator + "'" + value + "'";
908        }
909    }
910
911    private ContentResolver mResolver;
912    private String mPackageName;
913    private Uri mBaseUri = Downloads.Impl.CONTENT_URI;
914
915    /**
916     * @hide
917     */
918    public DownloadManager(ContentResolver resolver, String packageName) {
919        mResolver = resolver;
920        mPackageName = packageName;
921    }
922
923    /**
924     * Makes this object access the download provider through /all_downloads URIs rather than
925     * /my_downloads URIs, for clients that have permission to do so.
926     * @hide
927     */
928    public void setAccessAllDownloads(boolean accessAllDownloads) {
929        if (accessAllDownloads) {
930            mBaseUri = Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
931        } else {
932            mBaseUri = Downloads.Impl.CONTENT_URI;
933        }
934    }
935
936    /**
937     * Enqueue a new download.  The download will start automatically once the download manager is
938     * ready to execute it and connectivity is available.
939     *
940     * @param request the parameters specifying this download
941     * @return an ID for the download, unique across the system.  This ID is used to make future
942     * calls related to this download.
943     */
944    public long enqueue(Request request) {
945        ContentValues values = request.toContentValues(mPackageName);
946        Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
947        long id = Long.parseLong(downloadUri.getLastPathSegment());
948        return id;
949    }
950
951    /**
952     * Marks the specified download as 'to be deleted'. This is done when a completed download
953     * is to be removed but the row was stored without enough info to delete the corresponding
954     * metadata from Mediaprovider database. Actual cleanup of this row is done in DownloadService.
955     *
956     * @param ids the IDs of the downloads to be marked 'deleted'
957     * @return the number of downloads actually updated
958     * @hide
959     */
960    public int markRowDeleted(long... ids) {
961        if (ids == null || ids.length == 0) {
962            // called with nothing to remove!
963            throw new IllegalArgumentException("input param 'ids' can't be null");
964        }
965        ContentValues values = new ContentValues();
966        values.put(Downloads.Impl.COLUMN_DELETED, 1);
967        // if only one id is passed in, then include it in the uri itself.
968        // this will eliminate a full database scan in the download service.
969        if (ids.length == 1) {
970            return mResolver.update(ContentUris.withAppendedId(mBaseUri, ids[0]), values,
971                    null, null);
972        }
973        return mResolver.update(mBaseUri, values, getWhereClauseForIds(ids),
974                getWhereArgsForIds(ids));
975    }
976
977    /**
978     * Cancel downloads and remove them from the download manager.  Each download will be stopped if
979     * it was running, and it will no longer be accessible through the download manager.
980     * If there is a downloaded file, partial or complete, it is deleted.
981     *
982     * @param ids the IDs of the downloads to remove
983     * @return the number of downloads actually removed
984     */
985    public int remove(long... ids) {
986        return markRowDeleted(ids);
987    }
988
989    /**
990     * Query the download manager about downloads that have been requested.
991     * @param query parameters specifying filters for this query
992     * @return a Cursor over the result set of downloads, with columns consisting of all the
993     * COLUMN_* constants.
994     */
995    public Cursor query(Query query) {
996        Cursor underlyingCursor = query.runQuery(mResolver, UNDERLYING_COLUMNS, mBaseUri);
997        if (underlyingCursor == null) {
998            return null;
999        }
1000        return new CursorTranslator(underlyingCursor, mBaseUri);
1001    }
1002
1003    /**
1004     * Open a downloaded file for reading.  The download must have completed.
1005     * @param id the ID of the download
1006     * @return a read-only {@link ParcelFileDescriptor}
1007     * @throws FileNotFoundException if the destination file does not already exist
1008     */
1009    public ParcelFileDescriptor openDownloadedFile(long id) throws FileNotFoundException {
1010        return mResolver.openFileDescriptor(getDownloadUri(id), "r");
1011    }
1012
1013    /**
1014     * Returns the {@link Uri} of the given downloaded file id, if the file is
1015     * downloaded successfully. Otherwise, null is returned.
1016     *<p>
1017     * If the specified downloaded file is in external storage (for example, /sdcard dir),
1018     * then it is assumed to be safe for anyone to read and the returned {@link Uri} corresponds
1019     * to the filepath on sdcard.
1020     *
1021     * @param id the id of the downloaded file.
1022     * @return the {@link Uri} of the given downloaded file id, if download was successful. null
1023     * otherwise.
1024     */
1025    public Uri getUriForDownloadedFile(long id) {
1026        // to check if the file is in cache, get its destination from the database
1027        Query query = new Query().setFilterById(id);
1028        Cursor cursor = null;
1029        try {
1030            cursor = query(query);
1031            if (cursor == null) {
1032                return null;
1033            }
1034            if (cursor.moveToFirst()) {
1035                int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS));
1036                if (DownloadManager.STATUS_SUCCESSFUL == status) {
1037                    int indx = cursor.getColumnIndexOrThrow(
1038                            Downloads.Impl.COLUMN_DESTINATION);
1039                    int destination = cursor.getInt(indx);
1040                    // TODO: if we ever add API to DownloadManager to let the caller specify
1041                    // non-external storage for a downloaded file, then the following code
1042                    // should also check for that destination.
1043                    if (destination == Downloads.Impl.DESTINATION_CACHE_PARTITION ||
1044                            destination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION ||
1045                            destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING ||
1046                            destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
1047                        // return private uri
1048                        return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, id);
1049                    } else {
1050                        // return public uri
1051                        String path = cursor.getString(
1052                                cursor.getColumnIndexOrThrow(COLUMN_LOCAL_FILENAME));
1053                        return Uri.fromFile(new File(path));
1054                    }
1055                }
1056            }
1057        } finally {
1058            if (cursor != null) {
1059                cursor.close();
1060            }
1061        }
1062        // downloaded file not found or its status is not 'successfully completed'
1063        return null;
1064    }
1065
1066    /**
1067     * Returns the media type of the given downloaded file id, if the file was
1068     * downloaded successfully. Otherwise, null is returned.
1069     *
1070     * @param id the id of the downloaded file.
1071     * @return the media type of the given downloaded file id, if download was successful. null
1072     * otherwise.
1073     */
1074    public String getMimeTypeForDownloadedFile(long id) {
1075        Query query = new Query().setFilterById(id);
1076        Cursor cursor = null;
1077        try {
1078            cursor = query(query);
1079            if (cursor == null) {
1080                return null;
1081            }
1082            while (cursor.moveToFirst()) {
1083                return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MEDIA_TYPE));
1084            }
1085        } finally {
1086            if (cursor != null) {
1087                cursor.close();
1088            }
1089        }
1090        // downloaded file not found or its status is not 'successfully completed'
1091        return null;
1092    }
1093
1094    /**
1095     * Restart the given downloads, which must have already completed (successfully or not).  This
1096     * method will only work when called from within the download manager's process.
1097     * @param ids the IDs of the downloads
1098     * @hide
1099     */
1100    public void restartDownload(long... ids) {
1101        Cursor cursor = query(new Query().setFilterById(ids));
1102        try {
1103            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1104                int status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS));
1105                if (status != STATUS_SUCCESSFUL && status != STATUS_FAILED) {
1106                    throw new IllegalArgumentException("Cannot restart incomplete download: "
1107                            + cursor.getLong(cursor.getColumnIndex(COLUMN_ID)));
1108                }
1109            }
1110        } finally {
1111            cursor.close();
1112        }
1113
1114        ContentValues values = new ContentValues();
1115        values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
1116        values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
1117        values.putNull(Downloads.Impl._DATA);
1118        values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
1119        values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, 0);
1120        mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
1121    }
1122
1123    /**
1124     * Returns maximum size, in bytes, of downloads that may go over a mobile connection; or null if
1125     * there's no limit
1126     *
1127     * @param context the {@link Context} to use for accessing the {@link ContentResolver}
1128     * @return maximum size, in bytes, of downloads that may go over a mobile connection; or null if
1129     * there's no limit
1130     */
1131    public static Long getMaxBytesOverMobile(Context context) {
1132        try {
1133            return Settings.Global.getLong(context.getContentResolver(),
1134                    Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE);
1135        } catch (SettingNotFoundException exc) {
1136            return null;
1137        }
1138    }
1139
1140    /**
1141     * Returns recommended maximum size, in bytes, of downloads that may go over a mobile
1142     * connection; or null if there's no recommended limit.  The user will have the option to bypass
1143     * this limit.
1144     *
1145     * @param context the {@link Context} to use for accessing the {@link ContentResolver}
1146     * @return recommended maximum size, in bytes, of downloads that may go over a mobile
1147     * connection; or null if there's no recommended limit.
1148     */
1149    public static Long getRecommendedMaxBytesOverMobile(Context context) {
1150        try {
1151            return Settings.Global.getLong(context.getContentResolver(),
1152                    Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE);
1153        } catch (SettingNotFoundException exc) {
1154            return null;
1155        }
1156    }
1157
1158    /** {@hide} */
1159    public static boolean isActiveNetworkExpensive(Context context) {
1160        // TODO: connect to NetworkPolicyManager
1161        return false;
1162    }
1163
1164    /** {@hide} */
1165    public static long getActiveNetworkWarningBytes(Context context) {
1166        // TODO: connect to NetworkPolicyManager
1167        return -1;
1168    }
1169
1170    /**
1171     * Adds a file to the downloads database system, so it could appear in Downloads App
1172     * (and thus become eligible for management by the Downloads App).
1173     * <p>
1174     * It is helpful to make the file scannable by MediaScanner by setting the param
1175     * isMediaScannerScannable to true. It makes the file visible in media managing
1176     * applications such as Gallery App, which could be a useful purpose of using this API.
1177     *
1178     * @param title the title that would appear for this file in Downloads App.
1179     * @param description the description that would appear for this file in Downloads App.
1180     * @param isMediaScannerScannable true if the file is to be scanned by MediaScanner. Files
1181     * scanned by MediaScanner appear in the applications used to view media (for example,
1182     * Gallery app).
1183     * @param mimeType mimetype of the file.
1184     * @param path absolute pathname to the file. The file should be world-readable, so that it can
1185     * be managed by the Downloads App and any other app that is used to read it (for example,
1186     * Gallery app to display the file, if the file contents represent a video/image).
1187     * @param length length of the downloaded file
1188     * @param showNotification true if a notification is to be sent, false otherwise
1189     * @return  an ID for the download entry added to the downloads app, unique across the system
1190     * This ID is used to make future calls related to this download.
1191     */
1192    public long addCompletedDownload(String title, String description,
1193            boolean isMediaScannerScannable, String mimeType, String path, long length,
1194            boolean showNotification) {
1195        return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
1196                length, showNotification, false);
1197    }
1198
1199    /** {@hide} */
1200    public long addCompletedDownload(String title, String description,
1201            boolean isMediaScannerScannable, String mimeType, String path, long length,
1202            boolean showNotification, boolean allowWrite) {
1203        // make sure the input args are non-null/non-zero
1204        validateArgumentIsNonEmpty("title", title);
1205        validateArgumentIsNonEmpty("description", description);
1206        validateArgumentIsNonEmpty("path", path);
1207        validateArgumentIsNonEmpty("mimeType", mimeType);
1208        if (length < 0) {
1209            throw new IllegalArgumentException(" invalid value for param: totalBytes");
1210        }
1211
1212        // if there is already an entry with the given path name in downloads.db, return its id
1213        Request request = new Request(NON_DOWNLOADMANAGER_DOWNLOAD)
1214                .setTitle(title)
1215                .setDescription(description)
1216                .setMimeType(mimeType);
1217        ContentValues values = request.toContentValues(null);
1218        values.put(Downloads.Impl.COLUMN_DESTINATION,
1219                Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
1220        values.put(Downloads.Impl._DATA, path);
1221        values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
1222        values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, length);
1223        values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED,
1224                (isMediaScannerScannable) ? Request.SCANNABLE_VALUE_YES :
1225                        Request.SCANNABLE_VALUE_NO);
1226        values.put(Downloads.Impl.COLUMN_VISIBILITY, (showNotification) ?
1227                Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION : Request.VISIBILITY_HIDDEN);
1228        values.put(Downloads.Impl.COLUMN_ALLOW_WRITE, allowWrite ? 1 : 0);
1229        Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
1230        if (downloadUri == null) {
1231            return -1;
1232        }
1233        return Long.parseLong(downloadUri.getLastPathSegment());
1234    }
1235
1236    private static final String NON_DOWNLOADMANAGER_DOWNLOAD =
1237            "non-dwnldmngr-download-dont-retry2download";
1238
1239    private static void validateArgumentIsNonEmpty(String paramName, String val) {
1240        if (TextUtils.isEmpty(val)) {
1241            throw new IllegalArgumentException(paramName + " can't be null");
1242        }
1243    }
1244
1245    /**
1246     * Get the DownloadProvider URI for the download with the given ID.
1247     *
1248     * @hide
1249     */
1250    public Uri getDownloadUri(long id) {
1251        return ContentUris.withAppendedId(mBaseUri, id);
1252    }
1253
1254    /**
1255     * Get a parameterized SQL WHERE clause to select a bunch of IDs.
1256     */
1257    static String getWhereClauseForIds(long[] ids) {
1258        StringBuilder whereClause = new StringBuilder();
1259        whereClause.append("(");
1260        for (int i = 0; i < ids.length; i++) {
1261            if (i > 0) {
1262                whereClause.append("OR ");
1263            }
1264            whereClause.append(Downloads.Impl._ID);
1265            whereClause.append(" = ? ");
1266        }
1267        whereClause.append(")");
1268        return whereClause.toString();
1269    }
1270
1271    /**
1272     * Get the selection args for a clause returned by {@link #getWhereClauseForIds(long[])}.
1273     */
1274    static String[] getWhereArgsForIds(long[] ids) {
1275        String[] whereArgs = new String[ids.length];
1276        for (int i = 0; i < ids.length; i++) {
1277            whereArgs[i] = Long.toString(ids[i]);
1278        }
1279        return whereArgs;
1280    }
1281
1282    /**
1283     * This class wraps a cursor returned by DownloadProvider -- the "underlying cursor" -- and
1284     * presents a different set of columns, those defined in the DownloadManager.COLUMN_* constants.
1285     * Some columns correspond directly to underlying values while others are computed from
1286     * underlying data.
1287     */
1288    private static class CursorTranslator extends CursorWrapper {
1289        private Uri mBaseUri;
1290
1291        public CursorTranslator(Cursor cursor, Uri baseUri) {
1292            super(cursor);
1293            mBaseUri = baseUri;
1294        }
1295
1296        @Override
1297        public int getInt(int columnIndex) {
1298            return (int) getLong(columnIndex);
1299        }
1300
1301        @Override
1302        public long getLong(int columnIndex) {
1303            if (getColumnName(columnIndex).equals(COLUMN_REASON)) {
1304                return getReason(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
1305            } else if (getColumnName(columnIndex).equals(COLUMN_STATUS)) {
1306                return translateStatus(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
1307            } else {
1308                return super.getLong(columnIndex);
1309            }
1310        }
1311
1312        @Override
1313        public String getString(int columnIndex) {
1314            return (getColumnName(columnIndex).equals(COLUMN_LOCAL_URI)) ? getLocalUri() :
1315                    super.getString(columnIndex);
1316        }
1317
1318        private String getLocalUri() {
1319            long destinationType = getLong(getColumnIndex(Downloads.Impl.COLUMN_DESTINATION));
1320            if (destinationType == Downloads.Impl.DESTINATION_FILE_URI ||
1321                    destinationType == Downloads.Impl.DESTINATION_EXTERNAL ||
1322                    destinationType == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
1323                String localPath = getString(getColumnIndex(COLUMN_LOCAL_FILENAME));
1324                if (localPath == null) {
1325                    return null;
1326                }
1327                return Uri.fromFile(new File(localPath)).toString();
1328            }
1329
1330            // return content URI for cache download
1331            long downloadId = getLong(getColumnIndex(Downloads.Impl._ID));
1332            return ContentUris.withAppendedId(mBaseUri, downloadId).toString();
1333        }
1334
1335        private long getReason(int status) {
1336            switch (translateStatus(status)) {
1337                case STATUS_FAILED:
1338                    return getErrorCode(status);
1339
1340                case STATUS_PAUSED:
1341                    return getPausedReason(status);
1342
1343                default:
1344                    return 0; // arbitrary value when status is not an error
1345            }
1346        }
1347
1348        private long getPausedReason(int status) {
1349            switch (status) {
1350                case Downloads.Impl.STATUS_WAITING_TO_RETRY:
1351                    return PAUSED_WAITING_TO_RETRY;
1352
1353                case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
1354                    return PAUSED_WAITING_FOR_NETWORK;
1355
1356                case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
1357                    return PAUSED_QUEUED_FOR_WIFI;
1358
1359                default:
1360                    return PAUSED_UNKNOWN;
1361            }
1362        }
1363
1364        private long getErrorCode(int status) {
1365            if ((400 <= status && status < Downloads.Impl.MIN_ARTIFICIAL_ERROR_STATUS)
1366                    || (500 <= status && status < 600)) {
1367                // HTTP status code
1368                return status;
1369            }
1370
1371            switch (status) {
1372                case Downloads.Impl.STATUS_FILE_ERROR:
1373                    return ERROR_FILE_ERROR;
1374
1375                case Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE:
1376                case Downloads.Impl.STATUS_UNHANDLED_REDIRECT:
1377                    return ERROR_UNHANDLED_HTTP_CODE;
1378
1379                case Downloads.Impl.STATUS_HTTP_DATA_ERROR:
1380                    return ERROR_HTTP_DATA_ERROR;
1381
1382                case Downloads.Impl.STATUS_TOO_MANY_REDIRECTS:
1383                    return ERROR_TOO_MANY_REDIRECTS;
1384
1385                case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
1386                    return ERROR_INSUFFICIENT_SPACE;
1387
1388                case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
1389                    return ERROR_DEVICE_NOT_FOUND;
1390
1391                case Downloads.Impl.STATUS_CANNOT_RESUME:
1392                    return ERROR_CANNOT_RESUME;
1393
1394                case Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR:
1395                    return ERROR_FILE_ALREADY_EXISTS;
1396
1397                default:
1398                    return ERROR_UNKNOWN;
1399            }
1400        }
1401
1402        private int translateStatus(int status) {
1403            switch (status) {
1404                case Downloads.Impl.STATUS_PENDING:
1405                    return STATUS_PENDING;
1406
1407                case Downloads.Impl.STATUS_RUNNING:
1408                    return STATUS_RUNNING;
1409
1410                case Downloads.Impl.STATUS_PAUSED_BY_APP:
1411                case Downloads.Impl.STATUS_WAITING_TO_RETRY:
1412                case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
1413                case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
1414                    return STATUS_PAUSED;
1415
1416                case Downloads.Impl.STATUS_SUCCESS:
1417                    return STATUS_SUCCESSFUL;
1418
1419                default:
1420                    assert Downloads.Impl.isStatusError(status);
1421                    return STATUS_FAILED;
1422            }
1423        }
1424    }
1425}
1426