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