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 com.android.providers.downloads;
18
19import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE;
20import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
21
22import static com.android.providers.downloads.Constants.TAG;
23
24import android.app.DownloadManager;
25import android.app.job.JobInfo;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.Context;
29import android.content.Intent;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.Environment;
33import android.provider.Downloads;
34import android.text.TextUtils;
35import android.text.format.DateUtils;
36import android.util.Log;
37import android.util.Pair;
38
39import com.android.internal.util.IndentingPrintWriter;
40
41import java.io.CharArrayWriter;
42import java.io.File;
43import java.util.ArrayList;
44import java.util.Collection;
45import java.util.Collections;
46import java.util.List;
47
48/**
49 * Details about a specific download. Fields should only be mutated by updating
50 * from database query.
51 */
52public class DownloadInfo {
53    // TODO: move towards these in-memory objects being sources of truth, and
54    // periodically pushing to provider.
55
56    public static class Reader {
57        private ContentResolver mResolver;
58        private Cursor mCursor;
59
60        public Reader(ContentResolver resolver, Cursor cursor) {
61            mResolver = resolver;
62            mCursor = cursor;
63        }
64
65        public void updateFromDatabase(DownloadInfo info) {
66            info.mId = getLong(Downloads.Impl._ID);
67            info.mUri = getString(Downloads.Impl.COLUMN_URI);
68            info.mNoIntegrity = getInt(Downloads.Impl.COLUMN_NO_INTEGRITY) == 1;
69            info.mHint = getString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
70            info.mFileName = getString(Downloads.Impl._DATA);
71            info.mMimeType = Intent.normalizeMimeType(getString(Downloads.Impl.COLUMN_MIME_TYPE));
72            info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION);
73            info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY);
74            info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS);
75            info.mNumFailed = getInt(Downloads.Impl.COLUMN_FAILED_CONNECTIONS);
76            int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT);
77            info.mRetryAfter = retryRedirect & 0xfffffff;
78            info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION);
79            info.mPackage = getString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
80            info.mClass = getString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
81            info.mExtras = getString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
82            info.mCookies = getString(Downloads.Impl.COLUMN_COOKIE_DATA);
83            info.mUserAgent = getString(Downloads.Impl.COLUMN_USER_AGENT);
84            info.mReferer = getString(Downloads.Impl.COLUMN_REFERER);
85            info.mTotalBytes = getLong(Downloads.Impl.COLUMN_TOTAL_BYTES);
86            info.mCurrentBytes = getLong(Downloads.Impl.COLUMN_CURRENT_BYTES);
87            info.mETag = getString(Constants.ETAG);
88            info.mUid = getInt(Constants.UID);
89            info.mMediaScanned = getInt(Downloads.Impl.COLUMN_MEDIA_SCANNED);
90            info.mDeleted = getInt(Downloads.Impl.COLUMN_DELETED) == 1;
91            info.mMediaProviderUri = getString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
92            info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
93            info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
94            info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
95            info.mAllowMetered = getInt(Downloads.Impl.COLUMN_ALLOW_METERED) != 0;
96            info.mFlags = getInt(Downloads.Impl.COLUMN_FLAGS);
97            info.mTitle = getString(Downloads.Impl.COLUMN_TITLE);
98            info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION);
99            info.mBypassRecommendedSizeLimit =
100                    getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
101
102            synchronized (this) {
103                info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL);
104            }
105        }
106
107        public void readRequestHeaders(DownloadInfo info) {
108            info.mRequestHeaders.clear();
109            Uri headerUri = Uri.withAppendedPath(
110                    info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT);
111            Cursor cursor = mResolver.query(headerUri, null, null, null, null);
112            try {
113                int headerIndex =
114                        cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER);
115                int valueIndex =
116                        cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE);
117                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
118                    addHeader(info, cursor.getString(headerIndex), cursor.getString(valueIndex));
119                }
120            } finally {
121                cursor.close();
122            }
123
124            if (info.mCookies != null) {
125                addHeader(info, "Cookie", info.mCookies);
126            }
127            if (info.mReferer != null) {
128                addHeader(info, "Referer", info.mReferer);
129            }
130        }
131
132        private void addHeader(DownloadInfo info, String header, String value) {
133            info.mRequestHeaders.add(Pair.create(header, value));
134        }
135
136        private String getString(String column) {
137            int index = mCursor.getColumnIndexOrThrow(column);
138            String s = mCursor.getString(index);
139            return (TextUtils.isEmpty(s)) ? null : s;
140        }
141
142        private Integer getInt(String column) {
143            return mCursor.getInt(mCursor.getColumnIndexOrThrow(column));
144        }
145
146        private Long getLong(String column) {
147            return mCursor.getLong(mCursor.getColumnIndexOrThrow(column));
148        }
149    }
150
151    public long mId;
152    public String mUri;
153    @Deprecated
154    public boolean mNoIntegrity;
155    public String mHint;
156    public String mFileName;
157    public String mMimeType;
158    public int mDestination;
159    public int mVisibility;
160    public int mControl;
161    public int mStatus;
162    public int mNumFailed;
163    public int mRetryAfter;
164    public long mLastMod;
165    public String mPackage;
166    public String mClass;
167    public String mExtras;
168    public String mCookies;
169    public String mUserAgent;
170    public String mReferer;
171    public long mTotalBytes;
172    public long mCurrentBytes;
173    public String mETag;
174    public int mUid;
175    public int mMediaScanned;
176    public boolean mDeleted;
177    public String mMediaProviderUri;
178    public boolean mIsPublicApi;
179    public int mAllowedNetworkTypes;
180    public boolean mAllowRoaming;
181    public boolean mAllowMetered;
182    public int mFlags;
183    public String mTitle;
184    public String mDescription;
185    public int mBypassRecommendedSizeLimit;
186
187    private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
188
189    private final Context mContext;
190    private final SystemFacade mSystemFacade;
191
192    public DownloadInfo(Context context) {
193        mContext = context;
194        mSystemFacade = Helpers.getSystemFacade(context);
195    }
196
197    public static DownloadInfo queryDownloadInfo(Context context, long downloadId) {
198        final ContentResolver resolver = context.getContentResolver();
199        try (Cursor cursor = resolver.query(
200                ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId),
201                null, null, null, null)) {
202            final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
203            final DownloadInfo info = new DownloadInfo(context);
204            if (cursor.moveToFirst()) {
205                reader.updateFromDatabase(info);
206                reader.readRequestHeaders(info);
207                return info;
208            }
209        }
210        return null;
211    }
212
213    public Collection<Pair<String, String>> getHeaders() {
214        return Collections.unmodifiableList(mRequestHeaders);
215    }
216
217    public String getUserAgent() {
218        if (mUserAgent != null) {
219            return mUserAgent;
220        } else {
221            return Constants.DEFAULT_USER_AGENT;
222        }
223    }
224
225    public void sendIntentIfRequested() {
226        if (mPackage == null) {
227            return;
228        }
229
230        Intent intent;
231        if (mIsPublicApi) {
232            intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
233            intent.setPackage(mPackage);
234            intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId);
235        } else { // legacy behavior
236            if (mClass == null) {
237                return;
238            }
239            intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED);
240            intent.setClassName(mPackage, mClass);
241            if (mExtras != null) {
242                intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras);
243            }
244            // We only send the content: URI, for security reasons. Otherwise, malicious
245            //     applications would have an easier time spoofing download results by
246            //     sending spoofed intents.
247            intent.setData(getMyDownloadsUri());
248        }
249        mSystemFacade.sendBroadcast(intent);
250    }
251
252    /**
253     * Return if this download is visible to the user while running.
254     */
255    public boolean isVisible() {
256        switch (mVisibility) {
257            case VISIBILITY_VISIBLE:
258            case VISIBILITY_VISIBLE_NOTIFY_COMPLETED:
259                return true;
260            default:
261                return false;
262        }
263    }
264
265    /**
266     * Add random fuzz to the given delay so it's anywhere between 1-1.5x the
267     * requested delay.
268     */
269    private long fuzzDelay(long delay) {
270        return delay + Helpers.sRandom.nextInt((int) (delay / 2));
271    }
272
273    /**
274     * Return minimum latency in milliseconds required before this download is
275     * allowed to start again.
276     *
277     * @see android.app.job.JobInfo.Builder#setMinimumLatency(long)
278     */
279    public long getMinimumLatency() {
280        if (mStatus == Downloads.Impl.STATUS_WAITING_TO_RETRY) {
281            final long now = mSystemFacade.currentTimeMillis();
282            final long startAfter;
283            if (mNumFailed == 0) {
284                startAfter = now;
285            } else if (mRetryAfter > 0) {
286                startAfter = mLastMod + fuzzDelay(mRetryAfter);
287            } else {
288                final long delay = (Constants.RETRY_FIRST_DELAY * DateUtils.SECOND_IN_MILLIS
289                        * (1 << (mNumFailed - 1)));
290                startAfter = mLastMod + fuzzDelay(delay);
291            }
292            return Math.max(0, startAfter - now);
293        } else {
294            return 0;
295        }
296    }
297
298    /**
299     * Return the network type constraint required by this download.
300     *
301     * @see android.app.job.JobInfo.Builder#setRequiredNetworkType(int)
302     */
303    public int getRequiredNetworkType(long totalBytes) {
304        if (!mAllowMetered) {
305            return JobInfo.NETWORK_TYPE_UNMETERED;
306        }
307        if (mAllowedNetworkTypes == DownloadManager.Request.NETWORK_WIFI) {
308            return JobInfo.NETWORK_TYPE_UNMETERED;
309        }
310        if (totalBytes > mSystemFacade.getMaxBytesOverMobile()) {
311            return JobInfo.NETWORK_TYPE_UNMETERED;
312        }
313        if (totalBytes > mSystemFacade.getRecommendedMaxBytesOverMobile()
314                && mBypassRecommendedSizeLimit == 0) {
315            return JobInfo.NETWORK_TYPE_UNMETERED;
316        }
317        if (!mAllowRoaming) {
318            return JobInfo.NETWORK_TYPE_NOT_ROAMING;
319        }
320        return JobInfo.NETWORK_TYPE_ANY;
321    }
322
323    /**
324     * Returns whether this download is ready to be scheduled.
325     */
326    public boolean isReadyToSchedule() {
327        if (mControl == Downloads.Impl.CONTROL_PAUSED) {
328            // the download is paused, so it's not going to start
329            return false;
330        }
331        switch (mStatus) {
332            case 0:
333            case Downloads.Impl.STATUS_PENDING:
334            case Downloads.Impl.STATUS_RUNNING:
335            case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
336            case Downloads.Impl.STATUS_WAITING_TO_RETRY:
337            case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
338                return true;
339
340            case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
341                // is the media mounted?
342                final Uri uri = Uri.parse(mUri);
343                if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
344                    final File file = new File(uri.getPath());
345                    return Environment.MEDIA_MOUNTED
346                            .equals(Environment.getExternalStorageState(file));
347                } else {
348                    Log.w(TAG, "Expected file URI on external storage: " + mUri);
349                    return false;
350                }
351
352            default:
353                return false;
354        }
355    }
356
357    /**
358     * Returns whether this download has a visible notification after
359     * completion.
360     */
361    public boolean hasCompletionNotification() {
362        if (!Downloads.Impl.isStatusCompleted(mStatus)) {
363            return false;
364        }
365        if (mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
366            return true;
367        }
368        return false;
369    }
370
371    public boolean isMeteredAllowed(long totalBytes) {
372        return getRequiredNetworkType(totalBytes) != JobInfo.NETWORK_TYPE_UNMETERED;
373    }
374
375    public boolean isRoamingAllowed() {
376        if (mIsPublicApi) {
377            return mAllowRoaming;
378        } else { // legacy behavior
379            return mDestination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING;
380        }
381    }
382
383    public Uri getMyDownloadsUri() {
384        return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId);
385    }
386
387    public Uri getAllDownloadsUri() {
388        return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId);
389    }
390
391    @Override
392    public String toString() {
393        final CharArrayWriter writer = new CharArrayWriter();
394        dump(new IndentingPrintWriter(writer, "  "));
395        return writer.toString();
396    }
397
398    public void dump(IndentingPrintWriter pw) {
399        pw.println("DownloadInfo:");
400        pw.increaseIndent();
401
402        pw.printPair("mId", mId);
403        pw.printPair("mLastMod", mLastMod);
404        pw.printPair("mPackage", mPackage);
405        pw.printPair("mUid", mUid);
406        pw.println();
407
408        pw.printPair("mUri", mUri);
409        pw.println();
410
411        pw.printPair("mMimeType", mMimeType);
412        pw.printPair("mCookies", (mCookies != null) ? "yes" : "no");
413        pw.printPair("mReferer", (mReferer != null) ? "yes" : "no");
414        pw.printPair("mUserAgent", mUserAgent);
415        pw.println();
416
417        pw.printPair("mFileName", mFileName);
418        pw.printPair("mDestination", mDestination);
419        pw.println();
420
421        pw.printPair("mStatus", Downloads.Impl.statusToString(mStatus));
422        pw.printPair("mCurrentBytes", mCurrentBytes);
423        pw.printPair("mTotalBytes", mTotalBytes);
424        pw.println();
425
426        pw.printPair("mNumFailed", mNumFailed);
427        pw.printPair("mRetryAfter", mRetryAfter);
428        pw.printPair("mETag", mETag);
429        pw.printPair("mIsPublicApi", mIsPublicApi);
430        pw.println();
431
432        pw.printPair("mAllowedNetworkTypes", mAllowedNetworkTypes);
433        pw.printPair("mAllowRoaming", mAllowRoaming);
434        pw.printPair("mAllowMetered", mAllowMetered);
435        pw.printPair("mFlags", mFlags);
436        pw.println();
437
438        pw.decreaseIndent();
439    }
440
441    /**
442     * Returns whether a file should be scanned
443     */
444    public boolean shouldScanFile(int status) {
445        return (mMediaScanned == 0)
446                && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
447                        mDestination == Downloads.Impl.DESTINATION_FILE_URI ||
448                        mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
449                && Downloads.Impl.isStatusSuccess(status);
450    }
451
452    /**
453     * Query and return status of requested download.
454     */
455    public int queryDownloadStatus() {
456        return queryDownloadInt(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
457    }
458
459    public int queryDownloadControl() {
460        return queryDownloadInt(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN);
461    }
462
463    public int queryDownloadInt(String columnName, int defaultValue) {
464        try (Cursor cursor = mContext.getContentResolver().query(getAllDownloadsUri(),
465                new String[] { columnName }, null, null, null)) {
466            if (cursor.moveToFirst()) {
467                return cursor.getInt(0);
468            } else {
469                return defaultValue;
470            }
471        }
472    }
473}
474