Simplify download flow control, handle redirects.
[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(responseCode);
310
311                     case HTTP_UNAVAILABLE:
312                         parseRetryAfterHeaders(state, conn);
313                         if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
314                             throw new StopRequestException(STATUS_WAITING_TO_RETRY, "Unavailable");
315                         } else {
316                             throw new StopRequestException(STATUS_CANNOT_RESUME, "Unavailable");
317                         }
318
319                     case HTTP_INTERNAL_ERROR:
320                         throw new StopRequestException(STATUS_WAITING_TO_RETRY, "Internal error");
321
322                     default:
323                         StopRequestException.throwUnhandledHttpError(responseCode);
324                 }
325             } catch (IOException e) {
326                 // Trouble with low-level sockets
327                 throw new StopRequestException(STATUS_WAITING_TO_RETRY, e);
328
329             } finally {
330                 if (conn != null) conn.disconnect();
331             }
332         }
333
334         throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
335     }
336
337     /**
338      * Transfer data from the given connection to the destination file.
339      */
340     private void transferData(State state, HttpURLConnection conn) throws StopRequestException {
341         DrmManagerClient drmClient = null;
342         InputStream in = null;
343         OutputStream out = null;
344         FileDescriptor outFd = null;
345         try {
346             try {
347                 in = conn.getInputStream();
348             } catch (IOException e) {
349                 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
350             }
351
352             try {
353                 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
354                     drmClient = new DrmManagerClient(mContext);
355                     final RandomAccessFile file = new RandomAccessFile(
356                             new File(state.mFilename), "rw");
357                     out = new DrmOutputStream(drmClient, file, state.mMimeType);
358                     outFd = file.getFD();
359                 } else {
360                     out = new FileOutputStream(state.mFilename, true);
361                     outFd = ((FileOutputStream) out).getFD();
362                 }
363             } catch (IOException e) {
364                 throw new StopRequestException(STATUS_FILE_ERROR, e);
365             }
366
367             // Start streaming data, periodically watch for pause/cancel
368             // commands and checking disk space as needed.
369             transferData(state, in, out);
370
371             try {
372                 if (out instanceof DrmOutputStream) {
373                     ((DrmOutputStream) out).finish();
374                 }
375             } catch (IOException e) {
376                 throw new StopRequestException(STATUS_FILE_ERROR, e);
377             }
378
379         } finally {
380             if (drmClient != null) {
381                 drmClient.release();
382             }
383
384             IoUtils.closeQuietly(in);
385
386             try {
387                 if (out != null) out.flush();
388                 if (outFd != null) outFd.sync();
389             } catch (IOException e) {
390             } finally {
391                 IoUtils.closeQuietly(out);
392             }
393         }
394     }
395
396     /**
397      * Check if current connectivity is valid for this request.
398      */
399     private void checkConnectivity() throws StopRequestException {
400         // checking connectivity will apply current policy
401         mPolicyDirty = false;
402
403         int networkUsable = mInfo.checkCanUseNetwork();
404         if (networkUsable != DownloadInfo.NETWORK_OK) {
405             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
406             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
407                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
408                 mInfo.notifyPauseDueToSize(true);
409             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
410                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
411                 mInfo.notifyPauseDueToSize(false);
412             }
413             throw new StopRequestException(status,
414                     mInfo.getLogMessageForNetworkError(networkUsable));
415         }
416     }
417
418     /**
419      * Transfer as much data as possible from the HTTP response to the
420      * destination file.
421      */
422     private void transferData(State state, InputStream in, OutputStream out)
423             throws StopRequestException {
424         final byte data[] = new byte[Constants.BUFFER_SIZE];
425         for (;;) {
426             int bytesRead = readFromResponse(state, data, in);
427             if (bytesRead == -1) { // success, end of stream already reached
428                 handleEndOfStream(state);
429                 return;
430             }
431
432             state.mGotData = true;
433             writeDataToDestination(state, data, bytesRead, out);
434             state.mCurrentBytes += bytesRead;
435             reportProgress(state);
436
437             if (Constants.LOGVV) {
438                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
439                       + mInfo.mUri);
440             }
441
442             checkPausedOrCanceled(state);
443         }
444     }
445
446     /**
447      * Called after a successful completion to take any necessary action on the downloaded file.
448      */
449     private void finalizeDestinationFile(State state) {
450         if (state.mFilename != null) {
451             // make sure the file is readable
452             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
453         }
454     }
455
456     /**
457      * Called just before the thread finishes, regardless of status, to take any necessary action on
458      * the downloaded file.
459      */
460     private void cleanupDestination(State state, int finalStatus) {
461         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
462             if (Constants.LOGVV) {
463                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
464             }
465             new File(state.mFilename).delete();
466             state.mFilename = null;
467         }
468     }
469
470     /**
471      * Check if the download has been paused or canceled, stopping the request appropriately if it
472      * has been.
473      */
474     private void checkPausedOrCanceled(State state) throws StopRequestException {
475         synchronized (mInfo) {
476             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
477                 throw new StopRequestException(
478                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
479             }
480             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
481                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
482             }
483         }
484
485         // if policy has been changed, trigger connectivity check
486         if (mPolicyDirty) {
487             checkConnectivity();
488         }
489     }
490
491     /**
492      * Report download progress through the database if necessary.
493      */
494     private void reportProgress(State state) {
495         final long now = SystemClock.elapsedRealtime();
496
497         final long sampleDelta = now - state.mSpeedSampleStart;
498         if (sampleDelta > 500) {
499             final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
500                     / sampleDelta;
501
502             if (state.mSpeed == 0) {
503                 state.mSpeed = sampleSpeed;
504             } else {
505                 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
506             }
507
508             state.mSpeedSampleStart = now;
509             state.mSpeedSampleBytes = state.mCurrentBytes;
510
511             DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
512         }
513
514         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
515             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
516             ContentValues values = new ContentValues();
517             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
518             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
519             state.mBytesNotified = state.mCurrentBytes;
520             state.mTimeLastNotification = now;
521         }
522     }
523
524     /**
525      * Write a data buffer to the destination file.
526      * @param data buffer containing the data to write
527      * @param bytesRead how many bytes to write from the buffer
528      */
529     private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
530             throws StopRequestException {
531         mStorageManager.verifySpaceBeforeWritingToFile(
532                 mInfo.mDestination, state.mFilename, bytesRead);
533
534         boolean forceVerified = false;
535         while (true) {
536             try {
537                 out.write(data, 0, bytesRead);
538                 return;
539             } catch (IOException ex) {
540                 // TODO: better differentiate between DRM and disk failures
541                 if (!forceVerified) {
542                     // couldn't write to file. are we out of space? check.
543                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
544                     forceVerified = true;
545                 } else {
546                     throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
547                             "Failed to write data: " + ex);
548                 }
549             }
550         }
551     }
552
553     /**
554      * Called when we've reached the end of the HTTP response stream, to update the database and
555      * check for consistency.
556      */
557     private void handleEndOfStream(State state) throws StopRequestException {
558         ContentValues values = new ContentValues();
559         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
560         if (state.mContentLength == -1) {
561             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
562         }
563         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
564
565         final boolean lengthMismatched = (state.mContentLength != -1)
566                 && (state.mCurrentBytes != state.mContentLength);
567         if (lengthMismatched) {
568             if (cannotResume(state)) {
569                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
570                         "mismatched content length; unable to resume");
571             } else {
572                 throw new StopRequestException(getFinalStatusForHttpError(state),
573                         "closed socket before end of file");
574             }
575         }
576     }
577
578     private boolean cannotResume(State state) {
579         return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
580                 || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
581     }
582
583     /**
584      * Read some data from the HTTP response stream, handling I/O errors.
585      * @param data buffer to use to read data
586      * @param entityStream stream for reading the HTTP response entity
587      * @return the number of bytes actually read or -1 if the end of the stream has been reached
588      */
589     private int readFromResponse(State state, byte[] data, InputStream entityStream)
590             throws StopRequestException {
591         try {
592             return entityStream.read(data);
593         } catch (IOException ex) {
594             // TODO: handle stream errors the same as other retries
595             if ("unexpected end of stream".equals(ex.getMessage())) {
596                 return -1;
597             }
598
599             ContentValues values = new ContentValues();
600             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
601             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
602             if (cannotResume(state)) {
603                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
604                         "Failed reading response: " + ex + "; unable to resume", ex);
605             } else {
606                 throw new StopRequestException(getFinalStatusForHttpError(state),
607                         "Failed reading response: " + ex, ex);
608             }
609         }
610     }
611
612     /**
613      * Prepare target file based on given network response. Derives filename and
614      * target size as needed.
615      */
616     private void processResponseHeaders(State state, HttpURLConnection conn)
617             throws StopRequestException {
618         // TODO: fallocate the entire file if header gave us specific length
619
620         readResponseHeaders(state, conn);
621
622         state.mFilename = Helpers.generateSaveFile(
623                 mContext,
624                 mInfo.mUri,
625                 mInfo.mHint,
626                 state.mContentDisposition,
627                 state.mContentLocation,
628                 state.mMimeType,
629                 mInfo.mDestination,
630                 state.mContentLength,
631                 mInfo.mIsPublicApi, mStorageManager);
632
633         updateDatabaseFromHeaders(state);
634         // check connectivity again now that we know the total size
635         checkConnectivity();
636     }
637
638     /**
639      * Update necessary database fields based on values of HTTP response headers that have been
640      * read.
641      */
642     private void updateDatabaseFromHeaders(State state) {
643         ContentValues values = new ContentValues();
644         values.put(Downloads.Impl._DATA, state.mFilename);
645         if (state.mHeaderETag != null) {
646             values.put(Constants.ETAG, state.mHeaderETag);
647         }
648         if (state.mMimeType != null) {
649             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
650         }
651         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
652         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
653     }
654
655     /**
656      * Read headers from the HTTP response and store them into local state.
657      */
658     private void readResponseHeaders(State state, HttpURLConnection conn)
659             throws StopRequestException {
660         state.mContentDisposition = conn.getHeaderField("Content-Disposition");
661         state.mContentLocation = conn.getHeaderField("Content-Location");
662
663         if (state.mMimeType == null) {
664             state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
665         }
666
667         state.mHeaderETag = conn.getHeaderField("ETag");
668
669         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
670         if (transferEncoding == null) {
671             state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
672         } else {
673             Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
674             state.mContentLength = -1;
675         }
676
677         state.mTotalBytes = state.mContentLength;
678         mInfo.mTotalBytes = state.mContentLength;
679
680         final boolean noSizeInfo = state.mContentLength == -1
681                 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
682         if (!mInfo.mNoIntegrity && noSizeInfo) {
683             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
684                     "can't know size of download, giving up");
685         }
686     }
687
688     private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
689         state.mCountRetry = true;
690         state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
691         if (state.mRetryAfter < 0) {
692             state.mRetryAfter = 0;
693         } else {
694             if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
695                 state.mRetryAfter = Constants.MIN_RETRY_AFTER;
696             } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
697                 state.mRetryAfter = Constants.MAX_RETRY_AFTER;
698             }
699             state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
700             state.mRetryAfter *= 1000;
701         }
702     }
703
704     private int getFinalStatusForHttpError(State state) {
705         int networkUsable = mInfo.checkCanUseNetwork();
706         if (networkUsable != DownloadInfo.NETWORK_OK) {
707             switch (networkUsable) {
708                 case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
709                 case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
710                     return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
711                 default:
712                     return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
713             }
714         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
715             state.mCountRetry = true;
716             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
717         } else {
718             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
719             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
720         }
721     }
722
723     /**
724      * Prepare the destination file to receive data.  If the file already exists, we'll set up
725      * appropriately for resumption.
726      */
727     private void setupDestinationFile(State state) throws StopRequestException {
728         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
729             if (Constants.LOGV) {
730                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
731                         ", and state.mFilename: " + state.mFilename);
732             }
733             if (!Helpers.isFilenameValid(state.mFilename,
734                     mStorageManager.getDownloadDataDirectory())) {
735                 // this should never happen
736                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
737                         "found invalid internal destination filename");
738             }
739             // We're resuming a download that got interrupted
740             File f = new File(state.mFilename);
741             if (f.exists()) {
742                 if (Constants.LOGV) {
743                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
744                             ", and state.mFilename: " + state.mFilename);
745                 }
746                 long fileLength = f.length();
747                 if (fileLength == 0) {
748                     // The download hadn't actually started, we can restart from scratch
749                     if (Constants.LOGVV) {
750                         Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
751                                 + state.mFilename);
752                     }
753                     f.delete();
754                     state.mFilename = null;
755                     if (Constants.LOGV) {
756                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
757                                 ", BUT starting from scratch again: ");
758                     }
759                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
760                     // This should've been caught upon failure
761                     if (Constants.LOGVV) {
762                         Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
763                                 + state.mFilename);
764                     }
765                     f.delete();
766                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
767                             "Trying to resume a download that can't be resumed");
768                 } else {
769                     // All right, we'll be able to resume this download
770                     if (Constants.LOGV) {
771                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
772                                 ", and starting with file of length: " + fileLength);
773                     }
774                     state.mCurrentBytes = (int) fileLength;
775                     if (mInfo.mTotalBytes != -1) {
776                         state.mContentLength = mInfo.mTotalBytes;
777                     }
778                     state.mHeaderETag = mInfo.mETag;
779                     state.mContinuingDownload = true;
780                     if (Constants.LOGV) {
781                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
782                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
783                                 ", and setting mContinuingDownload to true: ");
784                     }
785                 }
786             }
787         }
788     }
789
790     /**
791      * Add custom headers for this download to the HTTP request.
792      */
793     private void addRequestHeaders(State state, HttpURLConnection conn) {
794         conn.addRequestProperty("User-Agent", userAgent());
795
796         for (Pair<String, String> header : mInfo.getHeaders()) {
797             conn.addRequestProperty(header.first, header.second);
798         }
799
800         if (state.mContinuingDownload) {
801             if (state.mHeaderETag != null) {
802                 conn.addRequestProperty("If-Match", state.mHeaderETag);
803             }
804             conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
805         }
806     }
807
808     /**
809      * Stores information about the completed download, and notifies the initiating application.
810      */
811     private void notifyDownloadCompleted(int status, boolean countRetry, int retryAfter,
812             boolean gotData, String filename, String mimeType, String errorMsg) {
813         notifyThroughDatabase(
814                 status, countRetry, retryAfter, gotData, filename, mimeType, errorMsg);
815         if (Downloads.Impl.isStatusCompleted(status)) {
816             mInfo.sendIntentIfRequested();
817         }
818     }
819
820     private void notifyThroughDatabase(int status, boolean countRetry, int retryAfter,
821             boolean gotData, String filename, String mimeType, String errorMsg) {
822         ContentValues values = new ContentValues();
823         values.put(Downloads.Impl.COLUMN_STATUS, status);
824         values.put(Downloads.Impl._DATA, filename);
825         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
826         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
827         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
828         if (!countRetry) {
829             values.put(Constants.FAILED_CONNECTIONS, 0);
830         } else if (gotData) {
831             values.put(Constants.FAILED_CONNECTIONS, 1);
832         } else {
833             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
834         }
835         // save the error message. could be useful to developers.
836         if (!TextUtils.isEmpty(errorMsg)) {
837             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
838         }
839         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
840     }
841
842     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
843         @Override
844         public void onUidRulesChanged(int uid, int uidRules) {
845             // caller is NPMS, since we only register with them
846             if (uid == mInfo.mUid) {
847                 mPolicyDirty = true;
848             }
849         }
850
851         @Override
852         public void onMeteredIfacesChanged(String[] meteredIfaces) {
853             // caller is NPMS, since we only register with them
854             mPolicyDirty = true;
855         }
856
857         @Override
858         public void onRestrictBackgroundChanged(boolean restrictBackground) {
859             // caller is NPMS, since we only register with them
860             mPolicyDirty = true;
861         }
862     };
863
864     public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
865         try {
866             return Long.parseLong(conn.getHeaderField(field));
867         } catch (NumberFormatException e) {
868             return defaultValue;
869         }
870     }
871 }