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