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