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 
17 package com.android.providers.downloads;
18 
19 import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE;
20 import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
21 
22 import static com.android.providers.downloads.Constants.TAG;
23 
24 import android.app.DownloadManager;
25 import android.app.job.JobInfo;
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Environment;
33 import android.provider.Downloads;
34 import android.text.TextUtils;
35 import android.text.format.DateUtils;
36 import android.util.Log;
37 import android.util.Pair;
38 
39 import com.android.internal.util.IndentingPrintWriter;
40 
41 import java.io.CharArrayWriter;
42 import java.io.File;
43 import java.util.ArrayList;
44 import java.util.Collection;
45 import java.util.Collections;
46 import java.util.List;
47 
48 /**
49  * Details about a specific download. Fields should only be mutated by updating
50  * from database query.
51  */
52 public 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 
Reader(ContentResolver resolver, Cursor cursor)60         public Reader(ContentResolver resolver, Cursor cursor) {
61             mResolver = resolver;
62             mCursor = cursor;
63         }
64 
updateFromDatabase(DownloadInfo info)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 
readRequestHeaders(DownloadInfo info)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 
addHeader(DownloadInfo info, String header, String value)132         private void addHeader(DownloadInfo info, String header, String value) {
133             info.mRequestHeaders.add(Pair.create(header, value));
134         }
135 
getString(String column)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 
getInt(String column)142         private Integer getInt(String column) {
143             return mCursor.getInt(mCursor.getColumnIndexOrThrow(column));
144         }
145 
getLong(String column)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 
DownloadInfo(Context context)192     public DownloadInfo(Context context) {
193         mContext = context;
194         mSystemFacade = Helpers.getSystemFacade(context);
195     }
196 
queryDownloadInfo(Context context, long downloadId)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 
getHeaders()213     public Collection<Pair<String, String>> getHeaders() {
214         return Collections.unmodifiableList(mRequestHeaders);
215     }
216 
getUserAgent()217     public String getUserAgent() {
218         if (mUserAgent != null) {
219             return mUserAgent;
220         } else {
221             return Constants.DEFAULT_USER_AGENT;
222         }
223     }
224 
sendIntentIfRequested()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      */
isVisible()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      */
fuzzDelay(long delay)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      */
getMinimumLatency()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      */
getRequiredNetworkType(long totalBytes)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      */
isReadyToSchedule()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      */
hasCompletionNotification()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 
isMeteredAllowed(long totalBytes)371     public boolean isMeteredAllowed(long totalBytes) {
372         return getRequiredNetworkType(totalBytes) != JobInfo.NETWORK_TYPE_UNMETERED;
373     }
374 
isRoamingAllowed()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 
isOnCache()383     public boolean isOnCache() {
384         return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION
385                 || mDestination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION
386                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
387                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
388     }
389 
getMyDownloadsUri()390     public Uri getMyDownloadsUri() {
391         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId);
392     }
393 
getAllDownloadsUri()394     public Uri getAllDownloadsUri() {
395         return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId);
396     }
397 
398     @Override
toString()399     public String toString() {
400         final CharArrayWriter writer = new CharArrayWriter();
401         dump(new IndentingPrintWriter(writer, "  "));
402         return writer.toString();
403     }
404 
dump(IndentingPrintWriter pw)405     public void dump(IndentingPrintWriter pw) {
406         pw.println("DownloadInfo:");
407         pw.increaseIndent();
408 
409         pw.printPair("mId", mId);
410         pw.printPair("mLastMod", mLastMod);
411         pw.printPair("mPackage", mPackage);
412         pw.printPair("mUid", mUid);
413         pw.println();
414 
415         pw.printPair("mUri", mUri);
416         pw.println();
417 
418         pw.printPair("mMimeType", mMimeType);
419         pw.printPair("mCookies", (mCookies != null) ? "yes" : "no");
420         pw.printPair("mReferer", (mReferer != null) ? "yes" : "no");
421         pw.printPair("mUserAgent", mUserAgent);
422         pw.println();
423 
424         pw.printPair("mFileName", mFileName);
425         pw.printPair("mDestination", mDestination);
426         pw.println();
427 
428         pw.printPair("mStatus", Downloads.Impl.statusToString(mStatus));
429         pw.printPair("mCurrentBytes", mCurrentBytes);
430         pw.printPair("mTotalBytes", mTotalBytes);
431         pw.println();
432 
433         pw.printPair("mNumFailed", mNumFailed);
434         pw.printPair("mRetryAfter", mRetryAfter);
435         pw.printPair("mETag", mETag);
436         pw.printPair("mIsPublicApi", mIsPublicApi);
437         pw.println();
438 
439         pw.printPair("mAllowedNetworkTypes", mAllowedNetworkTypes);
440         pw.printPair("mAllowRoaming", mAllowRoaming);
441         pw.printPair("mAllowMetered", mAllowMetered);
442         pw.printPair("mFlags", mFlags);
443         pw.println();
444 
445         pw.decreaseIndent();
446     }
447 
448     /**
449      * Returns whether a file should be scanned
450      */
shouldScanFile(int status)451     public boolean shouldScanFile(int status) {
452         return (mMediaScanned == 0)
453                 && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
454                         mDestination == Downloads.Impl.DESTINATION_FILE_URI ||
455                         mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
456                 && Downloads.Impl.isStatusSuccess(status);
457     }
458 
459     /**
460      * Query and return status of requested download.
461      */
queryDownloadStatus()462     public int queryDownloadStatus() {
463         return queryDownloadInt(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
464     }
465 
queryDownloadControl()466     public int queryDownloadControl() {
467         return queryDownloadInt(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN);
468     }
469 
queryDownloadInt(String columnName, int defaultValue)470     public int queryDownloadInt(String columnName, int defaultValue) {
471         try (Cursor cursor = mContext.getContentResolver().query(getAllDownloadsUri(),
472                 new String[] { columnName }, null, null, null)) {
473             if (cursor.moveToFirst()) {
474                 return cursor.getInt(0);
475             } else {
476                 return defaultValue;
477             }
478         }
479     }
480 }
481