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