Downloads.java revision 3af80dc6d937fb95cab544417e75f5aadc595690
1/*
2 * Copyright (C) 2008 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.net;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.net.Uri;
25import android.os.ParcelFileDescriptor;
26import android.os.SystemClock;
27import android.provider.BaseColumns;
28import android.util.Log;
29
30import java.io.File;
31import java.io.FileNotFoundException;
32import java.io.IOException;
33import java.io.File;
34import java.io.InputStream;
35
36/**
37 * The Download Manager
38 *
39 *
40 */
41public final class Downloads {
42
43
44    /**
45     * Download status codes
46     */
47
48    /**
49     * This download hasn't started yet
50     */
51    public static final int STATUS_PENDING = 190;
52
53    /**
54     * This download has started
55     */
56    public static final int STATUS_RUNNING = 192;
57
58    /**
59     * This download has successfully completed.
60     * Warning: there might be other status values that indicate success
61     * in the future.
62     * Use isSucccess() to capture the entire category.
63     */
64    public static final int STATUS_SUCCESS = 200;
65
66    /**
67     * This download can't be performed because the content type cannot be
68     * handled.
69     */
70    public static final int STATUS_NOT_ACCEPTABLE = 406;
71
72    /**
73     * This download has completed with an error.
74     * Warning: there will be other status values that indicate errors in
75     * the future. Use isStatusError() to capture the entire category.
76     */
77    public static final int STATUS_UNKNOWN_ERROR = 491;
78
79    /**
80     * This download couldn't be completed because of an HTTP
81     * redirect response that the download manager couldn't
82     * handle.
83     */
84    public static final int STATUS_UNHANDLED_REDIRECT = 493;
85
86    /**
87     * This download couldn't be completed due to insufficient storage
88     * space.  Typically, this is because the SD card is full.
89     */
90    public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
91
92    /**
93     * This download couldn't be completed because no external storage
94     * device was found.  Typically, this is because the SD card is not
95     * mounted.
96     */
97    public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
98
99    /**
100     * Returns whether the status is a success (i.e. 2xx).
101     */
102    public static boolean isStatusSuccess(int status) {
103        return (status >= 200 && status < 300);
104    }
105
106    /**
107     * Returns whether the status is an error (i.e. 4xx or 5xx).
108     */
109    public static boolean isStatusError(int status) {
110        return (status >= 400 && status < 600);
111    }
112
113    /**
114     * Download destinations
115     */
116
117    /**
118     * This download will be saved to the external storage. This is the
119     * default behavior, and should be used for any file that the user
120     * can freely access, copy, delete. Even with that destination,
121     * unencrypted DRM files are saved in secure internal storage.
122     * Downloads to the external destination only write files for which
123     * there is a registered handler. The resulting files are accessible
124     * by filename to all applications.
125     */
126    public static final int DOWNLOAD_DESTINATION_EXTERNAL = 1;
127
128    /**
129     * This download will be saved to the download manager's private
130     * partition. This is the behavior used by applications that want to
131     * download private files that are used and deleted soon after they
132     * get downloaded. All file types are allowed, and only the initiating
133     * application can access the file (indirectly through a content
134     * provider). This requires the
135     * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission.
136     */
137    public static final int DOWNLOAD_DESTINATION_CACHE = 2;
138
139    /**
140     * This download will be saved to the download manager's private
141     * partition and will be purged as necessary to make space. This is
142     * for private files (similar to CACHE_PARTITION) that aren't deleted
143     * immediately after they are used, and are kept around by the download
144     * manager as long as space is available.
145     */
146    public static final int DOWNLOAD_DESTINATION_CACHE_PURGEABLE = 3;
147
148
149    /**
150     * An invalid download id
151     */
152    public static final long DOWNLOAD_ID_INVALID = -1;
153
154
155    /**
156     * Broadcast Action: this is sent by the download manager to the app
157     * that had initiated a download when that download completes. The
158     * download's content: uri is specified in the intent's data.
159     */
160    public static final String ACTION_DOWNLOAD_COMPLETED =
161            "android.intent.action.DOWNLOAD_COMPLETED";
162
163    /**
164     * If extras are specified when requesting a download they will be provided in the intent that
165     * is sent to the specified class and package when a download has finished.
166     * <P>Type: TEXT</P>
167     * <P>Owner can Init</P>
168     */
169    public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras";
170
171
172    /**
173     * Status class for a download
174     */
175    public static final class StatusInfo {
176        public boolean completed = false;
177        /** The filename of the active download. */
178        public String filename = null;
179        /** An opaque id for the download */
180        public long id = DOWNLOAD_ID_INVALID;
181        /** An opaque status code for the download */
182        public int statusCode = -1;
183        /** Approximate number of bytes downloaded so far, for debugging purposes. */
184        public long bytesSoFar = -1;
185
186        /**
187         * Returns whether the download is completed
188         * @return a boolean whether the download is complete.
189         */
190        public boolean isComplete() {
191            return android.provider.Downloads.Impl.isStatusCompleted(statusCode);
192        }
193
194        /**
195         * Returns whether the download is successful
196         * @return a boolean whether the download is successful.
197         */
198        public boolean isSuccessful() {
199            return android.provider.Downloads.Impl.isStatusCompleted(statusCode);
200        }
201    }
202
203    /**
204     * Class to access initiate and query download by server uri
205     */
206    public static final class ByUri extends DownloadBase {
207        /** @hide */
208        private ByUri() {}
209
210        /**
211         * Query where clause by app data.
212         * @hide
213         */
214        private static final String QUERY_WHERE_APP_DATA_CLAUSE =
215                android.provider.Downloads.Impl.COLUMN_APP_DATA + "=?";
216
217        /**
218         * Gets a Cursor pointing to the download(s) of the current system update.
219         * @hide
220         */
221        private static final Cursor getCurrentOtaDownloads(Context context, String url) {
222            return context.getContentResolver().query(
223                    android.provider.Downloads.Impl.CONTENT_URI,
224                    DOWNLOADS_PROJECTION,
225                    QUERY_WHERE_APP_DATA_CLAUSE,
226                    new String[] {url},
227                    null);
228        }
229
230        /**
231         * Returns a StatusInfo with the result of trying to download the
232         * given URL.  Returns null if no attempts have been made.
233         */
234        public static final StatusInfo getStatus(
235                Context context,
236                String url,
237                long redownload_threshold) {
238            StatusInfo result = null;
239            boolean hasFailedDownload = false;
240            long failedDownloadModificationTime = 0;
241            Cursor c = getCurrentOtaDownloads(context, url);
242            try {
243                while (c != null && c.moveToNext()) {
244                    if (result == null) {
245                        result = new StatusInfo();
246                    }
247                    int status = getStatusOfDownload(c, redownload_threshold);
248                    if (status == STATUS_DOWNLOADING_UPDATE ||
249                        status == STATUS_DOWNLOADED_UPDATE) {
250                        result.completed = (status == STATUS_DOWNLOADED_UPDATE);
251                        result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME);
252                        result.id = c.getLong(DOWNLOADS_COLUMN_ID);
253                        result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS);
254                        result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES);
255                        return result;
256                    }
257
258                    long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION);
259                    if (hasFailedDownload &&
260                        modTime < failedDownloadModificationTime) {
261                        // older than the one already in result; skip it.
262                        continue;
263                    }
264
265                    hasFailedDownload = true;
266                    failedDownloadModificationTime = modTime;
267                    result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS);
268                    result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES);
269                }
270            } finally {
271                if (c != null) {
272                    c.close();
273                }
274            }
275            return result;
276        }
277
278        /**
279         * Query where clause for general querying.
280         */
281        private static final String QUERY_WHERE_CLAUSE =
282                android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + "=? AND " +
283                android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS + "=?";
284
285        /**
286         * Delete all the downloads for a package/class pair.
287         */
288        public static final void removeAllDownloadsByPackage(
289                Context context,
290                String notification_package,
291                String notification_class) {
292            context.getContentResolver().delete(
293                    android.provider.Downloads.Impl.CONTENT_URI,
294                    QUERY_WHERE_CLAUSE,
295                    new String[] { notification_package, notification_class });
296        }
297
298        /**
299         * The column for the id in the Cursor returned by
300         * {@link #getProgressCursor()}
301         */
302        public static final int getProgressColumnId() {
303            return 0;
304        }
305
306        /**
307         * The column for the current byte count in the Cursor returned by
308         * {@link #getProgressCursor()}
309         */
310        public static final int getProgressColumnCurrentBytes() {
311            return 1;
312        }
313
314        /**
315         * The column for the total byte count in the Cursor returned by
316         * {@link #getProgressCursor()}
317         */
318        public static final int getProgressColumnTotalBytes() {
319            return 2;
320        }
321
322        /** @hide */
323        private static final String[] PROJECTION = {
324            BaseColumns._ID,
325            android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES,
326            android.provider.Downloads.Impl.COLUMN_TOTAL_BYTES
327        };
328
329        /**
330         * Returns a Cursor representing the progress of the download identified by the ID.
331         */
332        public static final Cursor getProgressCursor(Context context, long id) {
333            Uri downloadUri = Uri.withAppendedPath(android.provider.Downloads.Impl.CONTENT_URI,
334                    String.valueOf(id));
335            return context.getContentResolver().query(downloadUri, PROJECTION, null, null, null);
336        }
337    }
338
339    /**
340     * Class to access downloads by opaque download id
341     */
342    public static final class ById extends DownloadBase {
343        /** @hide */
344        private ById() {}
345
346        /**
347         * Get the mime tupe of the download specified by the download id
348         */
349        public static String getMimeTypeForId(Context context, long downloadId) {
350            ContentResolver cr = context.getContentResolver();
351
352            String mimeType = null;
353            Cursor downloadCursor = null;
354
355            try {
356                Uri downloadUri = getDownloadUri(downloadId);
357
358                downloadCursor = cr.query(
359                        downloadUri, new String[]{android.provider.Downloads.Impl.COLUMN_MIME_TYPE},
360                        null, null, null);
361                if (downloadCursor.moveToNext()) {
362                    mimeType = downloadCursor.getString(0);
363                }
364            } finally {
365                if (downloadCursor != null) downloadCursor.close();
366            }
367            return mimeType;
368        }
369
370        /**
371         * Delete a download by Id
372         */
373        public static void deleteDownload(Context context, long downloadId) {
374            ContentResolver cr = context.getContentResolver();
375
376            String mimeType = null;
377
378            Uri downloadUri = getDownloadUri(downloadId);
379
380            cr.delete(downloadUri, null, null);
381        }
382
383        /**
384         * Open a filedescriptor to a particular download
385         */
386        public static ParcelFileDescriptor openDownload(
387                Context context, long downloadId, String mode)
388            throws FileNotFoundException
389        {
390            ContentResolver cr = context.getContentResolver();
391
392            String mimeType = null;
393
394            Uri downloadUri = getDownloadUri(downloadId);
395
396            return cr.openFileDescriptor(downloadUri, mode);
397        }
398
399        /**
400         * Open a stream to a particular download
401         */
402        public static InputStream openDownloadStream(Context context, long downloadId)
403                throws FileNotFoundException, IOException
404        {
405            ContentResolver cr = context.getContentResolver();
406
407            String mimeType = null;
408
409            Uri downloadUri = getDownloadUri(downloadId);
410
411            return cr.openInputStream(downloadUri);
412        }
413
414        private static Uri getDownloadUri(long downloadId) {
415            return Uri.parse(android.provider.Downloads.Impl.CONTENT_URI + "/" + downloadId);
416        }
417
418        /**
419         * Returns a StatusInfo with the result of trying to download the
420         * given URL.  Returns null if no attempts have been made.
421         */
422        public static final StatusInfo getStatus(
423                Context context,
424                long downloadId) {
425            StatusInfo result = null;
426            boolean hasFailedDownload = false;
427            long failedDownloadModificationTime = 0;
428
429            Uri downloadUri = getDownloadUri(downloadId);
430
431            ContentResolver cr = context.getContentResolver();
432
433            Cursor c = cr.query(
434                    downloadUri, DOWNLOADS_PROJECTION, null /* selection */, null /* selection args */,
435                    null /* sort order */);
436            try {
437                if (!c.moveToNext()) {
438                    return result;
439                }
440
441                if (result == null) {
442                    result = new StatusInfo();
443                }
444                int status = getStatusOfDownload(c,0);
445                if (status == STATUS_DOWNLOADING_UPDATE ||
446                        status == STATUS_DOWNLOADED_UPDATE) {
447                    result.completed = (status == STATUS_DOWNLOADED_UPDATE);
448                    result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME);
449                    result.id = c.getLong(DOWNLOADS_COLUMN_ID);
450                    result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS);
451                    result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES);
452                    return result;
453                }
454
455                long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION);
456
457                result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS);
458                result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES);
459            } finally {
460                if (c != null) {
461                    c.close();
462                }
463            }
464            return result;
465        }
466    }
467
468
469    /**
470     * Base class with common functionality for the various download classes
471     */
472    private static class DownloadBase {
473        /** @hide */
474        DownloadBase() {}
475
476        /**
477          * Initiate a download where the download will be tracked by its URI.
478          */
479        public static long startDownloadByUri(
480                Context context,
481                String url,
482                String cookieData,
483                boolean showDownload,
484                int downloadDestination,
485                boolean allowRoaming,
486                boolean skipIntegrityCheck,
487                String title,
488                String notification_package,
489                String notification_class,
490                String notification_extras) {
491            ContentResolver cr = context.getContentResolver();
492
493            // Tell download manager to start downloading update.
494            ContentValues values = new ContentValues();
495            values.put(android.provider.Downloads.Impl.COLUMN_URI, url);
496            values.put(android.provider.Downloads.Impl.COLUMN_COOKIE_DATA, cookieData);
497            values.put(android.provider.Downloads.Impl.COLUMN_VISIBILITY,
498                       showDownload ? android.provider.Downloads.Impl.VISIBILITY_VISIBLE
499                       : android.provider.Downloads.Impl.VISIBILITY_HIDDEN);
500            if (title != null) {
501                values.put(android.provider.Downloads.Impl.COLUMN_TITLE, title);
502            }
503            values.put(android.provider.Downloads.Impl.COLUMN_APP_DATA, url);
504
505
506            // NOTE:  destination should be seperated from whether the download
507            // can happen when roaming
508            int destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
509            switch (downloadDestination) {
510                case DOWNLOAD_DESTINATION_EXTERNAL:
511                    destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
512                    break;
513                case DOWNLOAD_DESTINATION_CACHE:
514                    if (allowRoaming) {
515                        destination = android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION;
516                    } else {
517                        destination =
518                                android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING;
519                    }
520                    break;
521                case DOWNLOAD_DESTINATION_CACHE_PURGEABLE:
522                    destination =
523                            android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE;
524                    break;
525            }
526            values.put(android.provider.Downloads.Impl.COLUMN_DESTINATION, destination);
527            values.put(android.provider.Downloads.Impl.COLUMN_NO_INTEGRITY,
528                    skipIntegrityCheck);  // Don't check ETag
529            if (notification_package != null && notification_class != null) {
530                values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
531                        notification_package);
532                values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
533                        notification_class);
534
535                if (notification_extras != null) {
536                    values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS,
537                            notification_extras);
538                }
539            }
540
541            Uri downloadUri = cr.insert(android.provider.Downloads.Impl.CONTENT_URI, values);
542
543            long downloadId = DOWNLOAD_ID_INVALID;
544            if (downloadUri != null) {
545                downloadId = Long.parseLong(downloadUri.getLastPathSegment());
546            }
547            return downloadId;
548        }
549    }
550
551    /** @hide */
552    private static final int STATUS_INVALID = 0;
553    /** @hide */
554    private static final int STATUS_DOWNLOADING_UPDATE = 3;
555    /** @hide */
556    private static final int STATUS_DOWNLOADED_UPDATE = 4;
557
558    /**
559     * Column projection for the query to the download manager. This must match
560     * with the constants DOWNLOADS_COLUMN_*.
561     * @hide
562     */
563    private static final String[] DOWNLOADS_PROJECTION = {
564            BaseColumns._ID,
565            android.provider.Downloads.Impl.COLUMN_APP_DATA,
566            android.provider.Downloads.Impl.COLUMN_STATUS,
567            android.provider.Downloads.Impl._DATA,
568            android.provider.Downloads.Impl.COLUMN_LAST_MODIFICATION,
569            android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES,
570    };
571
572    /**
573     * The column index for the ID.
574     * @hide
575     */
576    private static final int DOWNLOADS_COLUMN_ID = 0;
577    /**
578     * The column index for the URI.
579     * @hide
580     */
581    private static final int DOWNLOADS_COLUMN_URI = 1;
582    /**
583     * The column index for the status code.
584     * @hide
585     */
586    private static final int DOWNLOADS_COLUMN_STATUS = 2;
587    /**
588     * The column index for the filename.
589     * @hide
590     */
591    private static final int DOWNLOADS_COLUMN_FILENAME = 3;
592    /**
593     * The column index for the last modification time.
594     * @hide
595     */
596    private static final int DOWNLOADS_COLUMN_LAST_MODIFICATION = 4;
597    /**
598     * The column index for the number of bytes downloaded so far.
599     * @hide
600     */
601    private static final int DOWNLOADS_COLUMN_CURRENT_BYTES = 5;
602
603    /**
604     * Gets the status of a download.
605     *
606     * @param c A Cursor pointing to a download.  The URL column is assumed to be valid.
607     * @return The status of the download.
608     * @hide
609     */
610    private static final int getStatusOfDownload( Cursor c, long redownload_threshold) {
611        int status = c.getInt(DOWNLOADS_COLUMN_STATUS);
612        long realtime = SystemClock.elapsedRealtime();
613
614        // TODO(dougz): special handling of 503, 404?  (eg, special
615        // explanatory messages to user)
616
617        if (!android.provider.Downloads.Impl.isStatusCompleted(status)) {
618            // Check if it's stuck
619            long modified = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION);
620            long now = System.currentTimeMillis();
621            if (now < modified || now - modified > redownload_threshold) {
622                return STATUS_INVALID;
623            }
624
625            return STATUS_DOWNLOADING_UPDATE;
626        }
627
628        if (android.provider.Downloads.Impl.isStatusError(status)) {
629            return STATUS_INVALID;
630        }
631
632        String filename = c.getString(DOWNLOADS_COLUMN_FILENAME);
633        if (filename == null) {
634            return STATUS_INVALID;
635        }
636
637        return STATUS_DOWNLOADED_UPDATE;
638    }
639
640
641    /**
642     * @hide
643     */
644    private Downloads() {}
645}
646