Better handling of retryable errors.
[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_CANNOT_RESUME;
21 import static android.provider.Downloads.Impl.STATUS_FILE_ERROR;
22 import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR;
23 import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
24 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
25 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
26 import static com.android.providers.downloads.Constants.TAG;
27 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
28 import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
29 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
30 import static java.net.HttpURLConnection.HTTP_OK;
31 import static java.net.HttpURLConnection.HTTP_PARTIAL;
32 import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
33 import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
34 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
35
36 import android.content.ContentValues;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.drm.DrmManagerClient;
40 import android.drm.DrmOutputStream;
41 import android.net.INetworkPolicyListener;
42 import android.net.NetworkPolicyManager;
43 import android.net.TrafficStats;
44 import android.os.FileUtils;
45 import android.os.PowerManager;
46 import android.os.Process;
47 import android.os.SystemClock;
48 import android.provider.Downloads;
49 import android.text.TextUtils;
50 import android.util.Log;
51 import android.util.Pair;
52
53 import com.android.providers.downloads.DownloadInfo.NetworkState;
54
55 import java.io.File;
56 import java.io.FileDescriptor;
57 import java.io.FileOutputStream;
58 import java.io.IOException;
59 import java.io.InputStream;
60 import java.io.OutputStream;
61 import java.io.RandomAccessFile;
62 import java.net.HttpURLConnection;
63 import java.net.MalformedURLException;
64 import java.net.URL;
65 import java.net.URLConnection;
66
67 import libcore.io.IoUtils;
68 import libcore.net.http.HttpEngine;
69
70 /**
71  * Thread which executes a given {@link DownloadInfo}: making network requests,
72  * persisting data to disk, and updating {@link DownloadProvider}.
73  */
74 public class DownloadThread extends Thread {
75
76     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
77     private static final int HTTP_TEMP_REDIRECT = 307;
78
79     private static final int DEFAULT_TIMEOUT = (int) MINUTE_IN_MILLIS;
80
81     private final Context mContext;
82     private final DownloadInfo mInfo;
83     private final SystemFacade mSystemFacade;
84     private final StorageManager mStorageManager;
85
86     private volatile boolean mPolicyDirty;
87
88     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
89             StorageManager storageManager) {
90         mContext = context;
91         mSystemFacade = systemFacade;
92         mInfo = info;
93         mStorageManager = storageManager;
94     }
95
96     /**
97      * Returns the user agent provided by the initiating app, or use the default one
98      */
99     private String userAgent() {
100         String userAgent = mInfo.mUserAgent;
101         if (userAgent == null) {
102             userAgent = Constants.DEFAULT_USER_AGENT;
103         }
104         return userAgent;
105     }
106
107     /**
108      * State for the entire run() method.
109      */
110     static class State {
111         public String mFilename;
112         public String mMimeType;
113         public int mRetryAfter = 0;
114         public boolean mGotData = false;
115         public String mRequestUri;
116         public long mTotalBytes = -1;
117         public long mCurrentBytes = 0;
118         public String mHeaderETag;
119         public boolean mContinuingDownload = false;
120         public long mBytesNotified = 0;
121         public long mTimeLastNotification = 0;
122
123         /** Historical bytes/second speed of this download. */
124         public long mSpeed;
125         /** Time when current sample started. */
126         public long mSpeedSampleStart;
127         /** Bytes transferred since current sample started. */
128         public long mSpeedSampleBytes;
129
130         public long mContentLength = -1;
131         public String mContentDisposition;
132         public String mContentLocation;
133
134         public int mRedirectionCount;
135         public URL mUrl;
136
137         public State(DownloadInfo info) {
138             mMimeType = Intent.normalizeMimeType(info.mMimeType);
139             mRequestUri = info.mUri;
140             mFilename = info.mFileName;
141             mTotalBytes = info.mTotalBytes;
142             mCurrentBytes = info.mCurrentBytes;
143         }
144
145         public void resetBeforeExecute() {
146             // Reset any state from previous execution
147             mContentLength = -1;
148             mContentDisposition = null;
149             mContentLocation = null;
150             mRedirectionCount = 0;
151         }
152     }
153
154     /**
155      * Executes the download in a separate thread
156      */
157     @Override
158     public void run() {
159         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
160         try {
161             runInternal();
162         } finally {
163             DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
164         }
165     }
166
167     private void runInternal() {
168         // Skip when download already marked as finished; this download was
169         // probably started again while racing with UpdateThread.
170         if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
171                 == Downloads.Impl.STATUS_SUCCESS) {
172             Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
173             return;
174         }
175
176         State state = new State(mInfo);
177         PowerManager.WakeLock wakeLock = null;
178         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
179         int numFailed = mInfo.mNumFailed;
180         String errorMsg = null;
181
182         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
183         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
184
185         try {
186             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
187             wakeLock.acquire();
188
189             // while performing download, register for rules updates
190             netPolicy.registerListener(mPolicyListener);
191
192             Log.i(Constants.TAG, "Initiating download " + mInfo.mId);
193
194             // Network traffic on this thread should be counted against the
195             // requesting UID, and is tagged with well-known value.
196             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
197             TrafficStats.setThreadStatsUid(mInfo.mUid);
198
199             try {
200                 // TODO: migrate URL sanity checking into client side of API
201                 state.mUrl = new URL(state.mRequestUri);
202             } catch (MalformedURLException e) {
203                 throw new StopRequestException(STATUS_BAD_REQUEST, e);
204             }
205
206             executeDownload(state);
207
208             if (Constants.LOGV) {
209                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
210             }
211             finalizeDestinationFile(state);
212             finalStatus = Downloads.Impl.STATUS_SUCCESS;
213         } catch (StopRequestException error) {
214             // remove the cause before printing, in case it contains PII
215             errorMsg = error.getMessage();
216             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
217             Log.w(Constants.TAG, msg);
218             if (Constants.LOGV) {
219                 Log.w(Constants.TAG, msg, error);
220             }
221             finalStatus = error.getFinalStatus();
222
223             if (finalStatus == STATUS_WAITING_TO_RETRY) {
224                 throw new IllegalStateException("Execution should always throw final error codes");
225             }
226
227             // Some errors should be retryable later, unless we fail too many times.
228             if (isStatusRetryable(finalStatus)) {
229                 if (state.mGotData) {
230                     numFailed = 1;
231                 } else {
232                     numFailed += 1;
233                 }
234
235                 if (numFailed < Constants.MAX_RETRIES) {
236                     finalStatus = STATUS_WAITING_TO_RETRY;
237                 }
238             }
239
240             // fall through to finally block
241         } catch (Throwable ex) {
242             errorMsg = ex.getMessage();
243             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
244             Log.w(Constants.TAG, msg, ex);
245             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
246             // falls through to the code that reports an error
247         } finally {
248             TrafficStats.clearThreadStatsTag();
249             TrafficStats.clearThreadStatsUid();
250
251             cleanupDestination(state, finalStatus);
252             notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
253
254             netPolicy.unregisterListener(mPolicyListener);
255
256             if (wakeLock != null) {
257                 wakeLock.release();
258                 wakeLock = null;
259             }
260         }
261         mStorageManager.incrementNumDownloadsSoFar();
262     }
263
264     /**
265      * Fully execute a single download request. Setup and send the request,
266      * handle the response, and transfer the data to the destination file.
267      */
268     private void executeDownload(State state) throws StopRequestException {
269         state.resetBeforeExecute();
270         setupDestinationFile(state);
271
272         // skip when already finished; remove after fixing race in 5217390
273         if (state.mCurrentBytes == state.mTotalBytes) {
274             Log.i(Constants.TAG, "Skipping initiating request for download " +
275                   mInfo.mId + "; already completed");
276             return;
277         }
278
279         // TODO: compare mInfo.mNumFailed against Constants.MAX_RETRIES
280
281         while (state.mRedirectionCount++ < HttpEngine.MAX_REDIRECTS) {
282             // Open connection and follow any redirects until we have a useful
283             // response with body.
284             HttpURLConnection conn = null;
285             try {
286                 checkConnectivity();
287                 conn = (HttpURLConnection) state.mUrl.openConnection();
288                 conn.setInstanceFollowRedirects(false);
289                 conn.setConnectTimeout(DEFAULT_TIMEOUT);
290                 conn.setReadTimeout(DEFAULT_TIMEOUT);
291
292                 addRequestHeaders(state, conn);
293
294                 final int responseCode = conn.getResponseCode();
295                 switch (responseCode) {
296                     case HTTP_OK:
297                         if (state.mContinuingDownload) {
298                             throw new StopRequestException(
299                                     STATUS_CANNOT_RESUME, "Expected partial, but received OK");
300                         }
301                         processResponseHeaders(state, conn);
302                         transferData(state, conn);
303                         return;
304
305                     case HTTP_PARTIAL:
306                         if (!state.mContinuingDownload) {
307                             throw new StopRequestException(
308                                     STATUS_CANNOT_RESUME, "Expected OK, but received partial");
309                         }
310                         transferData(state, conn);
311                         return;
312
313                     case HTTP_MOVED_PERM:
314                     case HTTP_MOVED_TEMP:
315                     case HTTP_SEE_OTHER:
316                     case HTTP_TEMP_REDIRECT:
317                         final String location = conn.getHeaderField("Location");
318                         state.mUrl = new URL(state.mUrl, location);
319                         continue;
320
321                     case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
322                         throw new StopRequestException(
323                                 STATUS_CANNOT_RESUME, "Requested range not satisfiable");
324
325                     case HTTP_PRECON_FAILED:
326                         // TODO: probably means our etag precondition was
327                         // changed; flush and retry again
328                         StopRequestException.throwUnhandledHttpError(
329                                 responseCode, conn.getResponseMessage());
330
331                     case HTTP_UNAVAILABLE:
332                         parseRetryAfterHeaders(state, conn);
333                         throw new StopRequestException(
334                                 HTTP_UNAVAILABLE, conn.getResponseMessage());
335
336                     case HTTP_INTERNAL_ERROR:
337                         throw new StopRequestException(
338                                 HTTP_INTERNAL_ERROR, conn.getResponseMessage());
339
340                     default:
341                         StopRequestException.throwUnhandledHttpError(
342                                 responseCode, conn.getResponseMessage());
343                 }
344             } catch (IOException e) {
345                 // Trouble with low-level sockets
346                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
347
348             } finally {
349                 if (conn != null) conn.disconnect();
350             }
351         }
352
353         throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
354     }
355
356     /**
357      * Transfer data from the given connection to the destination file.
358      */
359     private void transferData(State state, HttpURLConnection conn) throws StopRequestException {
360         DrmManagerClient drmClient = null;
361         InputStream in = null;
362         OutputStream out = null;
363         FileDescriptor outFd = null;
364         try {
365             try {
366                 in = conn.getInputStream();
367             } catch (IOException e) {
368                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
369             }
370
371             try {
372                 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
373                     drmClient = new DrmManagerClient(mContext);
374                     final RandomAccessFile file = new RandomAccessFile(
375                             new File(state.mFilename), "rw");
376                     out = new DrmOutputStream(drmClient, file, state.mMimeType);
377                     outFd = file.getFD();
378                 } else {
379                     out = new FileOutputStream(state.mFilename, true);
380                     outFd = ((FileOutputStream) out).getFD();
381                 }
382             } catch (IOException e) {
383                 throw new StopRequestException(STATUS_FILE_ERROR, e);
384             }
385
386             // Start streaming data, periodically watch for pause/cancel
387             // commands and checking disk space as needed.
388             transferData(state, in, out);
389
390             try {
391                 if (out instanceof DrmOutputStream) {
392                     ((DrmOutputStream) out).finish();
393                 }
394             } catch (IOException e) {
395                 throw new StopRequestException(STATUS_FILE_ERROR, e);
396             }
397
398         } finally {
399             if (drmClient != null) {
400                 drmClient.release();
401             }
402
403             IoUtils.closeQuietly(in);
404
405             try {
406                 if (out != null) out.flush();
407                 if (outFd != null) outFd.sync();
408             } catch (IOException e) {
409             } finally {
410                 IoUtils.closeQuietly(out);
411             }
412         }
413     }
414
415     /**
416      * Check if current connectivity is valid for this request.
417      */
418     private void checkConnectivity() throws StopRequestException {
419         // checking connectivity will apply current policy
420         mPolicyDirty = false;
421
422         final NetworkState networkUsable = mInfo.checkCanUseNetwork();
423         if (networkUsable != NetworkState.OK) {
424             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
425             if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
426                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
427                 mInfo.notifyPauseDueToSize(true);
428             } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
429                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
430                 mInfo.notifyPauseDueToSize(false);
431             }
432             throw new StopRequestException(status, networkUsable.name());
433         }
434     }
435
436     /**
437      * Transfer as much data as possible from the HTTP response to the
438      * destination file.
439      */
440     private void transferData(State state, InputStream in, OutputStream out)
441             throws StopRequestException {
442         final byte data[] = new byte[Constants.BUFFER_SIZE];
443         for (;;) {
444             int bytesRead = readFromResponse(state, data, in);
445             if (bytesRead == -1) { // success, end of stream already reached
446                 handleEndOfStream(state);
447                 return;
448             }
449
450             state.mGotData = true;
451             writeDataToDestination(state, data, bytesRead, out);
452             state.mCurrentBytes += bytesRead;
453             reportProgress(state);
454
455             if (Constants.LOGVV) {
456                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
457                       + mInfo.mUri);
458             }
459
460             checkPausedOrCanceled(state);
461         }
462     }
463
464     /**
465      * Called after a successful completion to take any necessary action on the downloaded file.
466      */
467     private void finalizeDestinationFile(State state) {
468         if (state.mFilename != null) {
469             // make sure the file is readable
470             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
471         }
472     }
473
474     /**
475      * Called just before the thread finishes, regardless of status, to take any necessary action on
476      * the downloaded file.
477      */
478     private void cleanupDestination(State state, int finalStatus) {
479         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
480             if (Constants.LOGVV) {
481                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
482             }
483             new File(state.mFilename).delete();
484             state.mFilename = null;
485         }
486     }
487
488     /**
489      * Check if the download has been paused or canceled, stopping the request appropriately if it
490      * has been.
491      */
492     private void checkPausedOrCanceled(State state) throws StopRequestException {
493         synchronized (mInfo) {
494             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
495                 throw new StopRequestException(
496                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
497             }
498             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
499                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
500             }
501         }
502
503         // if policy has been changed, trigger connectivity check
504         if (mPolicyDirty) {
505             checkConnectivity();
506         }
507     }
508
509     /**
510      * Report download progress through the database if necessary.
511      */
512     private void reportProgress(State state) {
513         final long now = SystemClock.elapsedRealtime();
514
515         final long sampleDelta = now - state.mSpeedSampleStart;
516         if (sampleDelta > 500) {
517             final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
518                     / sampleDelta;
519
520             if (state.mSpeed == 0) {
521                 state.mSpeed = sampleSpeed;
522             } else {
523                 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
524             }
525
526             state.mSpeedSampleStart = now;
527             state.mSpeedSampleBytes = state.mCurrentBytes;
528
529             DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
530         }
531
532         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
533             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
534             ContentValues values = new ContentValues();
535             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
536             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
537             state.mBytesNotified = state.mCurrentBytes;
538             state.mTimeLastNotification = now;
539         }
540     }
541
542     /**
543      * Write a data buffer to the destination file.
544      * @param data buffer containing the data to write
545      * @param bytesRead how many bytes to write from the buffer
546      */
547     private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
548             throws StopRequestException {
549         mStorageManager.verifySpaceBeforeWritingToFile(
550                 mInfo.mDestination, state.mFilename, bytesRead);
551
552         boolean forceVerified = false;
553         while (true) {
554             try {
555                 out.write(data, 0, bytesRead);
556                 return;
557             } catch (IOException ex) {
558                 // TODO: better differentiate between DRM and disk failures
559                 if (!forceVerified) {
560                     // couldn't write to file. are we out of space? check.
561                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
562                     forceVerified = true;
563                 } else {
564                     throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
565                             "Failed to write data: " + ex);
566                 }
567             }
568         }
569     }
570
571     /**
572      * Called when we've reached the end of the HTTP response stream, to update the database and
573      * check for consistency.
574      */
575     private void handleEndOfStream(State state) throws StopRequestException {
576         ContentValues values = new ContentValues();
577         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
578         if (state.mContentLength == -1) {
579             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
580         }
581         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
582
583         final boolean lengthMismatched = (state.mContentLength != -1)
584                 && (state.mCurrentBytes != state.mContentLength);
585         if (lengthMismatched) {
586             if (cannotResume(state)) {
587                 throw new StopRequestException(STATUS_CANNOT_RESUME,
588                         "mismatched content length; unable to resume");
589             } else {
590                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
591                         "closed socket before end of file");
592             }
593         }
594     }
595
596     private boolean cannotResume(State state) {
597         return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
598                 || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
599     }
600
601     /**
602      * Read some data from the HTTP response stream, handling I/O errors.
603      * @param data buffer to use to read data
604      * @param entityStream stream for reading the HTTP response entity
605      * @return the number of bytes actually read or -1 if the end of the stream has been reached
606      */
607     private int readFromResponse(State state, byte[] data, InputStream entityStream)
608             throws StopRequestException {
609         try {
610             return entityStream.read(data);
611         } catch (IOException ex) {
612             // TODO: handle stream errors the same as other retries
613             if ("unexpected end of stream".equals(ex.getMessage())) {
614                 return -1;
615             }
616
617             ContentValues values = new ContentValues();
618             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
619             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
620             if (cannotResume(state)) {
621                 throw new StopRequestException(STATUS_CANNOT_RESUME,
622                         "Failed reading response: " + ex + "; unable to resume", ex);
623             } else {
624                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
625                         "Failed reading response: " + ex, ex);
626             }
627         }
628     }
629
630     /**
631      * Prepare target file based on given network response. Derives filename and
632      * target size as needed.
633      */
634     private void processResponseHeaders(State state, HttpURLConnection conn)
635             throws StopRequestException {
636         // TODO: fallocate the entire file if header gave us specific length
637
638         readResponseHeaders(state, conn);
639
640         state.mFilename = Helpers.generateSaveFile(
641                 mContext,
642                 mInfo.mUri,
643                 mInfo.mHint,
644                 state.mContentDisposition,
645                 state.mContentLocation,
646                 state.mMimeType,
647                 mInfo.mDestination,
648                 state.mContentLength,
649                 mInfo.mIsPublicApi, mStorageManager);
650
651         updateDatabaseFromHeaders(state);
652         // check connectivity again now that we know the total size
653         checkConnectivity();
654     }
655
656     /**
657      * Update necessary database fields based on values of HTTP response headers that have been
658      * read.
659      */
660     private void updateDatabaseFromHeaders(State state) {
661         ContentValues values = new ContentValues();
662         values.put(Downloads.Impl._DATA, state.mFilename);
663         if (state.mHeaderETag != null) {
664             values.put(Constants.ETAG, state.mHeaderETag);
665         }
666         if (state.mMimeType != null) {
667             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
668         }
669         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
670         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
671     }
672
673     /**
674      * Read headers from the HTTP response and store them into local state.
675      */
676     private void readResponseHeaders(State state, HttpURLConnection conn)
677             throws StopRequestException {
678         state.mContentDisposition = conn.getHeaderField("Content-Disposition");
679         state.mContentLocation = conn.getHeaderField("Content-Location");
680
681         if (state.mMimeType == null) {
682             state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
683         }
684
685         state.mHeaderETag = conn.getHeaderField("ETag");
686
687         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
688         if (transferEncoding == null) {
689             state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
690         } else {
691             Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
692             state.mContentLength = -1;
693         }
694
695         state.mTotalBytes = state.mContentLength;
696         mInfo.mTotalBytes = state.mContentLength;
697
698         final boolean noSizeInfo = state.mContentLength == -1
699                 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
700         if (!mInfo.mNoIntegrity && noSizeInfo) {
701             throw new StopRequestException(STATUS_CANNOT_RESUME,
702                     "can't know size of download, giving up");
703         }
704     }
705
706     private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
707         state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
708         if (state.mRetryAfter < 0) {
709             state.mRetryAfter = 0;
710         } else {
711             if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
712                 state.mRetryAfter = Constants.MIN_RETRY_AFTER;
713             } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
714                 state.mRetryAfter = Constants.MAX_RETRY_AFTER;
715             }
716             state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
717             state.mRetryAfter *= 1000;
718         }
719     }
720
721     /**
722      * Prepare the destination file to receive data.  If the file already exists, we'll set up
723      * appropriately for resumption.
724      */
725     private void setupDestinationFile(State state) throws StopRequestException {
726         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
727             if (Constants.LOGV) {
728                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
729                         ", and state.mFilename: " + state.mFilename);
730             }
731             if (!Helpers.isFilenameValid(state.mFilename,
732                     mStorageManager.getDownloadDataDirectory())) {
733                 // this should never happen
734                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
735                         "found invalid internal destination filename");
736             }
737             // We're resuming a download that got interrupted
738             File f = new File(state.mFilename);
739             if (f.exists()) {
740                 if (Constants.LOGV) {
741                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
742                             ", and state.mFilename: " + state.mFilename);
743                 }
744                 long fileLength = f.length();
745                 if (fileLength == 0) {
746                     // The download hadn't actually started, we can restart from scratch
747                     if (Constants.LOGVV) {
748                         Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
749                                 + state.mFilename);
750                     }
751                     f.delete();
752                     state.mFilename = null;
753                     if (Constants.LOGV) {
754                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
755                                 ", BUT starting from scratch again: ");
756                     }
757                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
758                     // This should've been caught upon failure
759                     if (Constants.LOGVV) {
760                         Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
761                                 + state.mFilename);
762                     }
763                     f.delete();
764                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
765                             "Trying to resume a download that can't be resumed");
766                 } else {
767                     // All right, we'll be able to resume this download
768                     if (Constants.LOGV) {
769                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
770                                 ", and starting with file of length: " + fileLength);
771                     }
772                     state.mCurrentBytes = (int) fileLength;
773                     if (mInfo.mTotalBytes != -1) {
774                         state.mContentLength = mInfo.mTotalBytes;
775                     }
776                     state.mHeaderETag = mInfo.mETag;
777                     state.mContinuingDownload = true;
778                     if (Constants.LOGV) {
779                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
780                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
781                                 ", and setting mContinuingDownload to true: ");
782                     }
783                 }
784             }
785         }
786     }
787
788     /**
789      * Add custom headers for this download to the HTTP request.
790      */
791     private void addRequestHeaders(State state, HttpURLConnection conn) {
792         for (Pair<String, String> header : mInfo.getHeaders()) {
793             conn.addRequestProperty(header.first, header.second);
794         }
795
796         // Only splice in user agent when not already defined
797         if (conn.getRequestProperty("User-Agent") == null) {
798             conn.addRequestProperty("User-Agent", userAgent());
799         }
800
801         if (state.mContinuingDownload) {
802             if (state.mHeaderETag != null) {
803                 conn.addRequestProperty("If-Match", state.mHeaderETag);
804             }
805             conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
806         }
807     }
808
809     /**
810      * Stores information about the completed download, and notifies the initiating application.
811      */
812     private void notifyDownloadCompleted(
813             State state, int finalStatus, String errorMsg, int numFailed) {
814         notifyThroughDatabase(state, finalStatus, errorMsg, numFailed);
815         if (Downloads.Impl.isStatusCompleted(finalStatus)) {
816             mInfo.sendIntentIfRequested();
817         }
818     }
819
820     private void notifyThroughDatabase(
821             State state, int finalStatus, String errorMsg, int numFailed) {
822         ContentValues values = new ContentValues();
823         values.put(Downloads.Impl.COLUMN_STATUS, finalStatus);
824         values.put(Downloads.Impl._DATA, state.mFilename);
825         values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
826         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
827         values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed);
828         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter);
829
830         // save the error message. could be useful to developers.
831         if (!TextUtils.isEmpty(errorMsg)) {
832             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
833         }
834         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
835     }
836
837     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
838         @Override
839         public void onUidRulesChanged(int uid, int uidRules) {
840             // caller is NPMS, since we only register with them
841             if (uid == mInfo.mUid) {
842                 mPolicyDirty = true;
843             }
844         }
845
846         @Override
847         public void onMeteredIfacesChanged(String[] meteredIfaces) {
848             // caller is NPMS, since we only register with them
849             mPolicyDirty = true;
850         }
851
852         @Override
853         public void onRestrictBackgroundChanged(boolean restrictBackground) {
854             // caller is NPMS, since we only register with them
855             mPolicyDirty = true;
856         }
857     };
858
859     public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
860         try {
861             return Long.parseLong(conn.getHeaderField(field));
862         } catch (NumberFormatException e) {
863             return defaultValue;
864         }
865     }
866
867     /**
868      * Return if given status is eligible to be treated as
869      * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
870      */
871     public static boolean isStatusRetryable(int status) {
872         switch (status) {
873             case STATUS_HTTP_DATA_ERROR:
874             case HTTP_UNAVAILABLE:
875             case HTTP_INTERNAL_ERROR:
876                 return true;
877             default:
878                 return false;
879         }
880     }
881 }