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