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