• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
20 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
21 import static android.provider.Downloads.Impl.COLUMN_CONTROL;
22 import static android.provider.Downloads.Impl.COLUMN_DELETED;
23 import static android.provider.Downloads.Impl.COLUMN_STATUS;
24 import static android.provider.Downloads.Impl.CONTROL_PAUSED;
25 import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
26 import static android.provider.Downloads.Impl.STATUS_CANCELED;
27 import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME;
28 import static android.provider.Downloads.Impl.STATUS_FILE_ERROR;
29 import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR;
30 import static android.provider.Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR;
31 import static android.provider.Downloads.Impl.STATUS_PAUSED_BY_APP;
32 import static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
33 import static android.provider.Downloads.Impl.STATUS_RUNNING;
34 import static android.provider.Downloads.Impl.STATUS_SUCCESS;
35 import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
36 import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
37 import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR;
38 import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
39 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
40 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
41 
42 import static com.android.providers.downloads.Constants.TAG;
43 import static com.android.providers.downloads.flags.Flags.downloadViaPlatformHttpEngine;
44 
45 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
46 import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
47 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
48 import static java.net.HttpURLConnection.HTTP_OK;
49 import static java.net.HttpURLConnection.HTTP_PARTIAL;
50 import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
51 import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
52 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
53 
54 import android.app.job.JobParameters;
55 import android.content.ContentValues;
56 import android.content.Context;
57 import android.content.Intent;
58 import android.drm.DrmManagerClient;
59 import android.drm.DrmOutputStream;
60 import android.net.INetworkPolicyListener;
61 import android.net.Network;
62 import android.net.NetworkCapabilities;
63 import android.net.NetworkPolicyManager;
64 import android.net.TrafficStats;
65 import android.net.Uri;
66 import android.net.http.HttpEngine;
67 import android.os.ParcelFileDescriptor;
68 import android.os.Process;
69 import android.os.SystemClock;
70 import android.os.storage.StorageManager;
71 import android.provider.Downloads;
72 import android.system.ErrnoException;
73 import android.system.Os;
74 import android.system.OsConstants;
75 import android.util.Log;
76 import android.util.MathUtils;
77 import android.util.Pair;
78 
79 import libcore.io.IoUtils;
80 
81 import java.io.File;
82 import java.io.FileDescriptor;
83 import java.io.FileNotFoundException;
84 import java.io.IOException;
85 import java.io.InputStream;
86 import java.io.OutputStream;
87 import java.net.HttpURLConnection;
88 import java.net.MalformedURLException;
89 import java.net.ProtocolException;
90 import java.net.URL;
91 import java.net.URLConnection;
92 import java.security.GeneralSecurityException;
93 import java.util.Arrays;
94 
95 import javax.net.ssl.HttpsURLConnection;
96 import javax.net.ssl.SSLContext;
97 
98 /**
99  * Task which executes a given {@link DownloadInfo}: making network requests,
100  * persisting data to disk, and updating {@link DownloadProvider}.
101  * <p>
102  * To know if a download is successful, we need to know either the final content
103  * length to expect, or the transfer to be chunked. To resume an interrupted
104  * download, we need an ETag.
105  * <p>
106  * Failed network requests are retried several times before giving up. Local
107  * disk errors fail immediately and are not retried.
108  */
109 public class DownloadThread extends Thread {
110 
111     // TODO: bind each download to a specific network interface to avoid state
112     // checking races once we have ConnectivityManager API
113 
114     // TODO: add support for saving to content://
115 
116     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
117     private static final int HTTP_TEMP_REDIRECT = 307;
118 
119     private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
120 
121     private final Context mContext;
122     private final SystemFacade mSystemFacade;
123     private final DownloadNotifier mNotifier;
124     private final NetworkPolicyManager mNetworkPolicy;
125     private final StorageManager mStorage;
126 
127     private final DownloadJobService mJobService;
128     private final JobParameters mParams;
129 
130     private final long mId;
131 
132     /**
133      * Info object that should be treated as read-only. Any potentially mutated
134      * fields are tracked in {@link #mInfoDelta}. If a field exists in
135      * {@link #mInfoDelta}, it must not be read from {@link #mInfo}.
136      */
137     private final DownloadInfo mInfo;
138     private final DownloadInfoDelta mInfoDelta;
139 
140     private volatile boolean mPolicyDirty;
141 
142     /**
143      * Local changes to {@link DownloadInfo}. These are kept local to avoid
144      * racing with the thread that updates based on change notifications.
145      */
146     private class DownloadInfoDelta {
147         public String mUri;
148         public String mFileName;
149         public String mMimeType;
150         public int mStatus;
151         public int mNumFailed;
152         public int mRetryAfter;
153         public long mTotalBytes;
154         public long mCurrentBytes;
155         public String mETag;
156 
157         public String mErrorMsg;
158 
159         private static final String NOT_CANCELED = COLUMN_STATUS + " != '" + STATUS_CANCELED + "'";
160         private static final String NOT_DELETED = COLUMN_DELETED + " == '0'";
161         private static final String NOT_PAUSED = "(" + COLUMN_CONTROL + " IS NULL OR "
162                 + COLUMN_CONTROL + " != '" + CONTROL_PAUSED + "')";
163 
164         private static final String SELECTION_VALID = NOT_CANCELED + " AND " + NOT_DELETED + " AND "
165                 + NOT_PAUSED;
166 
DownloadInfoDelta(DownloadInfo info)167         public DownloadInfoDelta(DownloadInfo info) {
168             mUri = info.mUri;
169             mFileName = info.mFileName;
170             mMimeType = info.mMimeType;
171             mStatus = info.mStatus;
172             mNumFailed = info.mNumFailed;
173             mRetryAfter = info.mRetryAfter;
174             mTotalBytes = info.mTotalBytes;
175             mCurrentBytes = info.mCurrentBytes;
176             mETag = info.mETag;
177         }
178 
buildContentValues()179         private ContentValues buildContentValues() {
180             final ContentValues values = new ContentValues();
181 
182             values.put(Downloads.Impl.COLUMN_URI, mUri);
183             values.put(Downloads.Impl._DATA, mFileName);
184             values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
185             values.put(Downloads.Impl.COLUMN_STATUS, mStatus);
186             values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed);
187             values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
188             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes);
189             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes);
190             values.put(Constants.ETAG, mETag);
191 
192             values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
193             values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg);
194 
195             return values;
196         }
197 
198         /**
199          * Blindly push update of current delta values to provider.
200          */
writeToDatabase()201         public void writeToDatabase() {
202             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(),
203                     null, null);
204         }
205 
206         /**
207          * Push update of current delta values to provider, asserting strongly
208          * that we haven't been paused or deleted.
209          */
writeToDatabaseOrThrow()210         public void writeToDatabaseOrThrow() throws StopRequestException {
211             if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
212                     buildContentValues(), SELECTION_VALID, null) == 0) {
213                 if (mInfo.queryDownloadControl() == CONTROL_PAUSED) {
214                     throw new StopRequestException(STATUS_PAUSED_BY_APP, "Download paused!");
215                 } else {
216                     throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!");
217                 }
218             }
219         }
220     }
221 
222     /**
223      * Flag indicating if we've made forward progress transferring file data
224      * from a remote server.
225      */
226     private boolean mMadeProgress = false;
227 
228     /**
229      * Details from the last time we pushed a database update.
230      */
231     private long mLastUpdateBytes = 0;
232     private long mLastUpdateTime = 0;
233 
234     private boolean mIgnoreBlocked;
235     private Network mNetwork;
236 
237     /** Historical bytes/second speed of this download. */
238     private long mSpeed;
239     /** Time when current sample started. */
240     private long mSpeedSampleStart;
241     /** Bytes transferred since current sample started. */
242     private long mSpeedSampleBytes;
243 
244     /** Flag indicating that thread must be halted */
245     private volatile boolean mShutdownRequested;
246 
247     /** This is initialized lazily in startDownload */
248     private HttpEngine mHttpEngine;
249 
DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info)250     public DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info) {
251         mContext = service;
252         mSystemFacade = Helpers.getSystemFacade(mContext);
253         mNotifier = Helpers.getDownloadNotifier(mContext);
254         mNetworkPolicy = mContext.getSystemService(NetworkPolicyManager.class);
255         mStorage = mContext.getSystemService(StorageManager.class);
256 
257         mJobService = service;
258         mParams = params;
259 
260         mId = info.mId;
261         mInfo = info;
262 
263         mInfoDelta = new DownloadInfoDelta(info);
264     }
265 
isUsingHttpEngine()266     private boolean isUsingHttpEngine() {
267         return mHttpEngine != null;
268     }
269 
270     @Override
run()271     public void run() {
272         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
273 
274         // Skip when download already marked as finished; this download was
275         // probably started again while racing with UpdateThread.
276         if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) {
277             logDebug("Already finished; skipping");
278             return;
279         }
280 
281         try {
282             // while performing download, register for rules updates
283             mNetworkPolicy.registerListener(mPolicyListener);
284 
285             logDebug("Starting");
286 
287             mInfoDelta.mStatus = STATUS_RUNNING;
288             mInfoDelta.writeToDatabase();
289 
290             // If we're showing a foreground notification for the requesting
291             // app, the download isn't affected by the blocked status of the
292             // requesting app
293             mIgnoreBlocked = mInfo.isVisible();
294 
295             // Use the caller's default network to make this connection, since
296             // they might be subject to restrictions that we shouldn't let them
297             // circumvent
298             mNetwork = mSystemFacade.getNetwork(mParams);
299             if (mNetwork == null) {
300                 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK,
301                         "No network associated with requesting UID");
302             }
303 
304             // Network traffic on this thread should be counted against the
305             // requesting UID, and is tagged with well-known value.
306             TrafficStats.setThreadStatsTagDownload();
307             TrafficStats.setThreadStatsUid(mInfo.mUid);
308 
309             executeDownload();
310 
311             mInfoDelta.mStatus = STATUS_SUCCESS;
312             TrafficStats.incrementOperationCount(1);
313 
314             // If we just finished a chunked file, record total size
315             if (mInfoDelta.mTotalBytes == -1) {
316                 mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes;
317             }
318 
319         } catch (StopRequestException e) {
320             mInfoDelta.mStatus = e.getFinalStatus();
321             mInfoDelta.mErrorMsg = e.getMessage();
322 
323             logWarning("Stop requested with status "
324                     + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": "
325                     + mInfoDelta.mErrorMsg);
326 
327             // Nobody below our level should request retries, since we handle
328             // failure counts at this level.
329             if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) {
330                 throw new IllegalStateException("Execution should always throw final error codes");
331             }
332 
333             // Some errors should be retryable, unless we fail too many times.
334             if (isStatusRetryable(mInfoDelta.mStatus)) {
335                 if (mMadeProgress) {
336                     mInfoDelta.mNumFailed = 1;
337                 } else {
338                     mInfoDelta.mNumFailed += 1;
339                 }
340 
341                 if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
342                     if (null != mSystemFacade.getNetworkCapabilities(mNetwork)) {
343                         // Underlying network is still intact, use normal backoff
344                         mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
345                     } else {
346                         // Network unavailable, retry on any next available
347                         mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
348                     }
349 
350                     if ((mInfoDelta.mETag == null && mMadeProgress)
351                             || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
352                         // However, if we wrote data and have no ETag to verify
353                         // contents against later, we can't actually resume.
354                         mInfoDelta.mStatus = STATUS_CANNOT_RESUME;
355                     }
356                 }
357             }
358 
359             // If we're waiting for a network that must be unmetered, our status
360             // is actually queued so we show relevant notifications
361             if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
362                     && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
363                 mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI;
364             }
365 
366         } catch (Throwable t) {
367             mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR;
368             mInfoDelta.mErrorMsg = t.toString();
369 
370             logError("Failed: " + mInfoDelta.mErrorMsg, t);
371 
372         } finally {
373             logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus));
374 
375             mNotifier.notifyDownloadSpeed(mId, 0);
376 
377             finalizeDestination();
378 
379             mInfoDelta.writeToDatabase();
380 
381             TrafficStats.clearThreadStatsTag();
382             TrafficStats.clearThreadStatsUid();
383 
384             mNetworkPolicy.unregisterListener(mPolicyListener);
385         }
386 
387         boolean needsReschedule = false;
388         if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY
389                 || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
390                 || mInfoDelta.mStatus == STATUS_QUEUED_FOR_WIFI) {
391             logDebug("rescheduling the job id:" + mId);
392             needsReschedule = true;
393         }
394 
395         mJobService.jobFinishedInternal(mParams, needsReschedule);
396     }
397 
requestShutdown()398     public void requestShutdown() {
399         mShutdownRequested = true;
400     }
401 
402     /**
403      * Fully execute a single download request. Setup and send the request,
404      * handle the response, and transfer the data to the destination file.
405      */
executeDownload()406     private void executeDownload() throws StopRequestException {
407         final boolean resuming = mInfoDelta.mCurrentBytes != 0;
408 
409         URL url;
410         try {
411             // TODO: migrate URL sanity checking into client side of API
412             url = new URL(mInfoDelta.mUri);
413         } catch (MalformedURLException e) {
414             throw new StopRequestException(STATUS_BAD_REQUEST, e);
415         }
416 
417         boolean cleartextTrafficPermitted
418                 = mSystemFacade.isCleartextTrafficPermitted(mInfo.mPackage, url.getHost());
419         SSLContext appContext;
420         try {
421             appContext = mSystemFacade.getSSLContextForPackage(mContext, mInfo.mPackage);
422         } catch (GeneralSecurityException e) {
423             // This should never happen.
424             throw new StopRequestException(STATUS_UNKNOWN_ERROR, "Unable to create SSLContext.");
425         }
426         int redirectionCount = 0;
427         while (redirectionCount++ < Constants.MAX_REDIRECTS) {
428             // Enforce the cleartext traffic opt-out for the UID. This cannot be enforced earlier
429             // because of HTTP redirects which can change the protocol between HTTP and HTTPS.
430             if ((!cleartextTrafficPermitted) && ("http".equalsIgnoreCase(url.getProtocol()))) {
431                 throw new StopRequestException(STATUS_BAD_REQUEST,
432                         "Cleartext traffic not permitted for package " + mInfo.mPackage + ": "
433                                 + Uri.parse(url.toString()).toSafeString());
434             }
435 
436             // Open connection and follow any redirects until we have a useful
437             // response with body.
438             HttpURLConnection conn = null;
439             try {
440                 // Check that the caller is allowed to make network connections. If so, make one on
441                 // their behalf to open the url.
442                 checkConnectivity();
443                 if (downloadViaPlatformHttpEngine() && !mSystemFacade.hasPerDomainConfig(
444                         mInfo.mPackage)) {
445                     // Disable HttpEngine if the caller APK has a per-domain networkConfig as this
446                     // could mean that the APK does have its own CAs / trust anchors. This is a
447                     // feature which Cronet does not support but we plan to add compatibility for
448                     // in the future.
449                     mHttpEngine = new HttpEngine.Builder(mContext).build();
450                     logDebug("HttpEngine is being used for this download");
451                     mHttpEngine.bindToNetwork(mNetwork);
452                     conn = (HttpURLConnection) mHttpEngine.openConnection(url);
453                 } else {
454                     // HttpEngine does not support setConnectTimeout on its HttpUrlConnection
455                     // implementation. The default timeout in HttpEngine is 4 minutes which is much
456                     // longer than what's defined here but that should not be a problem.
457                     conn = (HttpURLConnection) mNetwork.openConnection(url);
458                     conn.setConnectTimeout(DEFAULT_TIMEOUT);
459                 }
460                 conn.setInstanceFollowRedirects(false);
461                 conn.setReadTimeout(DEFAULT_TIMEOUT);
462                 // If this is going over HTTPS configure the trust to be the same as the calling
463                 // package.
464                 if (conn instanceof HttpsURLConnection) {
465                     ((HttpsURLConnection) conn).setSSLSocketFactory(appContext.getSocketFactory());
466                 }
467 
468                 addRequestHeaders(conn, resuming);
469 
470                 final int responseCode = conn.getResponseCode();
471                 switch (responseCode) {
472                     case HTTP_OK:
473                         if (resuming) {
474                             throw new StopRequestException(
475                                     STATUS_CANNOT_RESUME, "Expected partial, but received OK");
476                         }
477                         parseOkHeaders(conn);
478                         transferData(conn);
479                         return;
480 
481                     case HTTP_PARTIAL:
482                         if (!resuming) {
483                             throw new StopRequestException(
484                                     STATUS_CANNOT_RESUME, "Expected OK, but received partial");
485                         }
486                         transferData(conn);
487                         return;
488 
489                     case HTTP_MOVED_PERM:
490                     case HTTP_MOVED_TEMP:
491                     case HTTP_SEE_OTHER:
492                     case HTTP_TEMP_REDIRECT:
493                         final String location = conn.getHeaderField("Location");
494                         url = new URL(url, location);
495                         if (responseCode == HTTP_MOVED_PERM) {
496                             // Push updated URL back to database
497                             mInfoDelta.mUri = url.toString();
498                         }
499                         continue;
500 
501                     case HTTP_PRECON_FAILED:
502                         throw new StopRequestException(
503                                 STATUS_CANNOT_RESUME, "Precondition failed");
504 
505                     case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
506                         throw new StopRequestException(
507                                 STATUS_CANNOT_RESUME, "Requested range not satisfiable");
508 
509                     case HTTP_UNAVAILABLE:
510                         parseUnavailableHeaders(conn);
511                         throw new StopRequestException(
512                                 HTTP_UNAVAILABLE, conn.getResponseMessage());
513 
514                     case HTTP_INTERNAL_ERROR:
515                         throw new StopRequestException(
516                                 HTTP_INTERNAL_ERROR, conn.getResponseMessage());
517 
518                     default:
519                         StopRequestException.throwUnhandledHttpError(
520                                 responseCode, conn.getResponseMessage());
521                 }
522 
523             } catch (IOException e) {
524                 if (e instanceof ProtocolException
525                         && e.getMessage().startsWith("Unexpected status line")) {
526                     throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e);
527                 } else {
528                     // Trouble with low-level sockets
529                     throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
530                 }
531 
532             } finally {
533                 if (conn != null) conn.disconnect();
534             }
535         }
536 
537         throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
538     }
539 
540     /**
541      * Transfer data from the given connection to the destination file.
542      */
transferData(HttpURLConnection conn)543     private void transferData(HttpURLConnection conn) throws StopRequestException {
544 
545         // To detect when we're really finished, we either need a length, closed
546         // connection, or chunked encoding.
547         final boolean hasLength = mInfoDelta.mTotalBytes != -1;
548         final boolean isConnectionClose = "close".equalsIgnoreCase(
549                 conn.getHeaderField("Connection"));
550         final boolean isEncodingChunked = "chunked".equalsIgnoreCase(
551                 conn.getHeaderField("Transfer-Encoding"));
552 
553         final boolean finishKnown = hasLength || isConnectionClose || isEncodingChunked;
554         if (!finishKnown) {
555             throw new StopRequestException(
556                     STATUS_CANNOT_RESUME, "can't know size of download, giving up");
557         }
558 
559         DrmManagerClient drmClient = null;
560         ParcelFileDescriptor outPfd = null;
561         FileDescriptor outFd = null;
562         InputStream in = null;
563         OutputStream out = null;
564         try {
565             try {
566                 in = conn.getInputStream();
567             } catch (IOException e) {
568                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
569             }
570 
571             try {
572                 outPfd = mContext.getContentResolver()
573                         .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
574                 outFd = outPfd.getFileDescriptor();
575 
576                 if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
577                     drmClient = new DrmManagerClient(mContext);
578                     out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType);
579                 } else {
580                     out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd);
581                 }
582 
583                 // Move into place to begin writing
584                 Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET);
585             } catch (ErrnoException e) {
586                 throw new StopRequestException(STATUS_FILE_ERROR, e);
587             } catch (IOException e) {
588                 throw new StopRequestException(STATUS_FILE_ERROR, e);
589             }
590 
591             try {
592                 // Pre-flight disk space requirements, when known
593                 if (mInfoDelta.mTotalBytes > 0 && mStorage.isAllocationSupported(outFd)) {
594                     mStorage.allocateBytes(outFd, mInfoDelta.mTotalBytes);
595                 }
596             } catch (IOException e) {
597                 throw new StopRequestException(STATUS_INSUFFICIENT_SPACE_ERROR, e);
598             }
599 
600             // Start streaming data, periodically watch for pause/cancel
601             // commands and checking disk space as needed.
602             transferData(in, out, outFd);
603 
604             try {
605                 if (out instanceof DrmOutputStream) {
606                     ((DrmOutputStream) out).finish();
607                 }
608             } catch (IOException e) {
609                 throw new StopRequestException(STATUS_FILE_ERROR, e);
610             }
611 
612         } finally {
613             if (drmClient != null) {
614                 drmClient.close();
615             }
616 
617             IoUtils.closeQuietly(in);
618 
619             try {
620                 if (out != null) out.flush();
621                 if (outFd != null) outFd.sync();
622             } catch (IOException e) {
623             } finally {
624                 IoUtils.closeQuietly(out);
625             }
626         }
627     }
628 
629     /**
630      * Transfer as much data as possible from the HTTP response to the
631      * destination file.
632      */
transferData(InputStream in, OutputStream out, FileDescriptor outFd)633     private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
634             throws StopRequestException {
635         final byte buffer[] = new byte[Constants.BUFFER_SIZE];
636         while (true) {
637             if (mPolicyDirty) checkConnectivity();
638 
639             if (mShutdownRequested) {
640                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
641                         "Local halt requested; job probably timed out");
642             }
643 
644             int len = -1;
645             try {
646                 len = in.read(buffer);
647             } catch (IOException e) {
648                 throw new StopRequestException(
649                         STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e);
650             }
651 
652             if (len == -1) {
653                 break;
654             }
655 
656             try {
657                 out.write(buffer, 0, len);
658 
659                 mMadeProgress = true;
660                 mInfoDelta.mCurrentBytes += len;
661 
662                 updateProgress(outFd);
663 
664             } catch (IOException e) {
665                 throw new StopRequestException(STATUS_FILE_ERROR, e);
666             }
667         }
668 
669         // Finished without error; verify length if known
670         if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) {
671             throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch; found "
672                     + mInfoDelta.mCurrentBytes + " instead of " + mInfoDelta.mTotalBytes);
673         }
674     }
675 
676     /**
677      * Called just before the thread finishes, regardless of status, to take any
678      * necessary action on the downloaded file.
679      */
finalizeDestination()680     private void finalizeDestination() {
681         if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) {
682             // When error, free up any disk space
683             try {
684                 final ParcelFileDescriptor target = mContext.getContentResolver()
685                         .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
686                 try {
687                     Os.ftruncate(target.getFileDescriptor(), 0);
688                 } catch (ErrnoException ignored) {
689                 } finally {
690                     IoUtils.closeQuietly(target);
691                 }
692             } catch (FileNotFoundException ignored) {
693             }
694 
695             // Delete if local file
696             if (mInfoDelta.mFileName != null) {
697                 new File(mInfoDelta.mFileName).delete();
698                 mInfoDelta.mFileName = null;
699             }
700 
701         } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
702             // When success, open access if local file
703             if (mInfoDelta.mFileName != null) {
704                 if (Helpers.isFileInExternalAndroidDirs(mInfoDelta.mFileName)) {
705                     // Files that are downloaded in Android/ may need fixing up
706                     // of permissions on devices without sdcardfs; do so here,
707                     // before we give the file back to the client
708                     File file = new File(mInfoDelta.mFileName);
709                     mStorage.fixupAppDir(file.getParentFile());
710                 }
711                 if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
712                     try {
713                         // Move into final resting place, if needed
714                         final File before = new File(mInfoDelta.mFileName);
715                         final File beforeDir = Helpers.getRunningDestinationDirectory(
716                                 mContext, mInfo.mDestination);
717                         final File afterDir = Helpers.getSuccessDestinationDirectory(
718                                 mContext, mInfo.mDestination);
719                         if (!beforeDir.equals(afterDir)
720                                 && before.getParentFile().equals(beforeDir)) {
721                             final File after = new File(afterDir, before.getName());
722                             if (before.renameTo(after)) {
723                                 mInfoDelta.mFileName = after.getAbsolutePath();
724                             }
725                         }
726                     } catch (IOException ignored) {
727                     }
728                 }
729             }
730         }
731     }
732 
733     /**
734      * Check if current connectivity is valid for this request.
735      */
checkConnectivity()736     private void checkConnectivity() throws StopRequestException {
737         // checking connectivity will apply current policy
738         mPolicyDirty = false;
739 
740         final NetworkCapabilities caps = mSystemFacade.getNetworkCapabilities(mNetwork);
741         if (caps == null) {
742             throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected");
743         }
744         if (!caps.hasCapability(NET_CAPABILITY_NOT_ROAMING)
745                 && !mInfo.isRoamingAllowed()) {
746             throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming");
747         }
748         if (!caps.hasCapability(NET_CAPABILITY_NOT_METERED)
749                 && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
750             throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is metered");
751         }
752     }
753 
754     /**
755      * Report download progress through the database if necessary.
756      */
updateProgress(FileDescriptor outFd)757     private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException {
758         final long now = SystemClock.elapsedRealtime();
759         final long currentBytes = mInfoDelta.mCurrentBytes;
760 
761         final long sampleDelta = now - mSpeedSampleStart;
762         if (sampleDelta > 500) {
763             final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000)
764                     / sampleDelta;
765 
766             if (mSpeed == 0) {
767                 mSpeed = sampleSpeed;
768             } else {
769                 mSpeed = ((mSpeed * 3) + sampleSpeed) / 4;
770             }
771 
772             // Only notify once we have a full sample window
773             if (mSpeedSampleStart != 0) {
774                 mNotifier.notifyDownloadSpeed(mId, mSpeed);
775             }
776 
777             mSpeedSampleStart = now;
778             mSpeedSampleBytes = currentBytes;
779         }
780 
781         final long bytesDelta = currentBytes - mLastUpdateBytes;
782         final long timeDelta = now - mLastUpdateTime;
783         if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) {
784             // fsync() to ensure that current progress has been flushed to disk,
785             // so we can always resume based on latest database information.
786             outFd.sync();
787 
788             mInfoDelta.writeToDatabaseOrThrow();
789 
790             mLastUpdateBytes = currentBytes;
791             mLastUpdateTime = now;
792         }
793     }
794 
795     /**
796      * Process response headers from first server response. This derives its
797      * filename, size, and ETag.
798      */
parseOkHeaders(HttpURLConnection conn)799     private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException {
800         if (mInfoDelta.mFileName == null) {
801             final String contentDisposition = conn.getHeaderField("Content-Disposition");
802             final String contentLocation = conn.getHeaderField("Content-Location");
803 
804             try {
805                 mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri,
806                         mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType,
807                         mInfo.mDestination);
808             } catch (IOException e) {
809                 throw new StopRequestException(
810                         Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e);
811             }
812         }
813 
814         if (mInfoDelta.mMimeType == null) {
815             mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType());
816         }
817 
818         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
819         if (transferEncoding == null) {
820             mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1);
821         } else {
822             mInfoDelta.mTotalBytes = -1;
823         }
824 
825         mInfoDelta.mETag = conn.getHeaderField("ETag");
826 
827         mInfoDelta.writeToDatabaseOrThrow();
828 
829         // Check connectivity again now that we know the total size
830         checkConnectivity();
831     }
832 
parseUnavailableHeaders(HttpURLConnection conn)833     private void parseUnavailableHeaders(HttpURLConnection conn) {
834         long retryAfter = conn.getHeaderFieldInt("Retry-After", -1);
835         retryAfter = MathUtils.constrain(retryAfter, Constants.MIN_RETRY_AFTER,
836                 Constants.MAX_RETRY_AFTER);
837         mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS);
838     }
839 
840     /**
841      * Add custom headers for this download to the HTTP request.
842      */
addRequestHeaders(HttpURLConnection conn, boolean resuming)843     private void addRequestHeaders(HttpURLConnection conn, boolean resuming) {
844         for (Pair<String, String> header : mInfo.getHeaders()) {
845             conn.addRequestProperty(header.first, header.second);
846         }
847 
848         // Only splice in user agent when not already defined
849         if (conn.getRequestProperty("User-Agent") == null) {
850             conn.addRequestProperty("User-Agent", mInfo.getUserAgent());
851         }
852 
853         // It's fine to use HttpEngine with a compression algorithm since the default behaviour
854         // of DownloadManager is to always download via plaintext. Using a compression algorithm
855         // with decoding on the fly will greatly reduce the downloaded bytes.
856         // HttpEngine will automatically default to using identity if it's trying to do a partial
857         // download (resumption of previous download).
858         if (!isUsingHttpEngine()) {
859             // Defeat transparent gzip compression, since it doesn't allow us to
860             // easily resume partial downloads.
861             conn.setRequestProperty("Accept-Encoding", "identity");
862         }
863 
864         // Defeat connection reuse, since otherwise servers may continue
865         // streaming large downloads after cancelled.
866         conn.setRequestProperty("Connection", "close");
867 
868         if (resuming) {
869             if (mInfoDelta.mETag != null) {
870                 conn.addRequestProperty("If-Match", mInfoDelta.mETag);
871             }
872             conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-");
873         }
874     }
875 
logDebug(String msg)876     private void logDebug(String msg) {
877         Log.d(TAG, "[" + mId + "] " + msg);
878     }
879 
logWarning(String msg)880     private void logWarning(String msg) {
881         Log.w(TAG, "[" + mId + "] " + msg);
882     }
883 
logError(String msg, Throwable t)884     private void logError(String msg, Throwable t) {
885         Log.e(TAG, "[" + mId + "] " + msg, t);
886     }
887 
888     private INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() {
889         @Override
890         public void onUidRulesChanged(int uid, int uidRules) {
891             // caller is NPMS, since we only register with them
892             if (uid == mInfo.mUid) {
893                 mPolicyDirty = true;
894             }
895         }
896 
897         @Override
898         public void onMeteredIfacesChanged(String[] meteredIfaces) {
899             // caller is NPMS, since we only register with them
900             mPolicyDirty = true;
901         }
902 
903         @Override
904         public void onRestrictBackgroundChanged(boolean restrictBackground) {
905             // caller is NPMS, since we only register with them
906             mPolicyDirty = true;
907         }
908 
909         @Override
910         public void onUidPoliciesChanged(int uid, int uidPolicies) {
911             // caller is NPMS, since we only register with them
912             if (uid == mInfo.mUid) {
913                 mPolicyDirty = true;
914             }
915         }
916     };
917 
getHeaderFieldLong(URLConnection conn, String field, long defaultValue)918     private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
919         try {
920             return Long.parseLong(conn.getHeaderField(field));
921         } catch (NumberFormatException e) {
922             return defaultValue;
923         }
924     }
925 
926     /**
927      * Return if given status is eligible to be treated as
928      * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
929      */
isStatusRetryable(int status)930     public static boolean isStatusRetryable(int status) {
931         switch (status) {
932             case STATUS_HTTP_DATA_ERROR:
933             case HTTP_UNAVAILABLE:
934             case HTTP_INTERNAL_ERROR:
935             case STATUS_FILE_ERROR:
936                 return true;
937             default:
938                 return false;
939         }
940     }
941 }
942