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