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