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