downloads broken by Change-Id: I115b3ba6cfe0262269dbbc4c336b9e1d63c618e4
[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 android.content.ContentValues;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.drm.mobile1.DrmRawContent;
23 import android.net.http.AndroidHttpClient;
24 import android.os.FileUtils;
25 import android.os.PowerManager;
26 import android.os.Process;
27 import android.provider.Downloads;
28 import android.provider.DrmStore;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.util.Pair;
32
33 import org.apache.http.Header;
34 import org.apache.http.HttpResponse;
35 import org.apache.http.client.methods.HttpGet;
36
37 import java.io.File;
38 import java.io.FileNotFoundException;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.SyncFailedException;
43 import java.net.URI;
44 import java.net.URISyntaxException;
45 import java.util.Locale;
46
47 /**
48  * Runs an actual download
49  */
50 public class DownloadThread extends Thread {
51
52     private Context mContext;
53     private DownloadInfo mInfo;
54     private SystemFacade mSystemFacade;
55
56     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info) {
57         mContext = context;
58         mSystemFacade = systemFacade;
59         mInfo = info;
60     }
61
62     /**
63      * Returns the user agent provided by the initiating app, or use the default one
64      */
65     private String userAgent() {
66         String userAgent = mInfo.mUserAgent;
67         if (userAgent != null) {
68         }
69         if (userAgent == null) {
70             userAgent = Constants.DEFAULT_USER_AGENT;
71         }
72         return userAgent;
73     }
74
75     /**
76      * State for the entire run() method.
77      */
78     private static class State {
79         public String mFilename;
80         public FileOutputStream mStream;
81         public String mMimeType;
82         public boolean mCountRetry = false;
83         public int mRetryAfter = 0;
84         public int mRedirectCount = 0;
85         public String mNewUri;
86         public boolean mGotData = false;
87         public String mRequestUri;
88
89         public State(DownloadInfo info) {
90             mMimeType = sanitizeMimeType(info.mMimeType);
91             mRedirectCount = info.mRedirectCount;
92             mRequestUri = info.mUri;
93             mFilename = info.mFileName;
94         }
95     }
96
97     /**
98      * State within executeDownload()
99      */
100     private static class InnerState {
101         public int mBytesSoFar = 0;
102         public String mHeaderETag;
103         public boolean mContinuingDownload = false;
104         public String mHeaderContentLength;
105         public String mHeaderContentDisposition;
106         public String mHeaderContentLocation;
107         public int mBytesNotified = 0;
108         public long mTimeLastNotification = 0;
109     }
110
111     /**
112      * Raised from methods called by run() to indicate that the current request should be stopped
113      * immediately.
114      *
115      * Note the message passed to this exception will be logged and therefore must be guaranteed
116      * not to contain any PII, meaning it generally can't include any information about the request
117      * URI, headers, or destination filename.
118      */
119     private class StopRequest extends Throwable {
120         public int mFinalStatus;
121
122         public StopRequest(int finalStatus, String message) {
123             super(message);
124             mFinalStatus = finalStatus;
125         }
126
127         public StopRequest(int finalStatus, String message, Throwable throwable) {
128             super(message, throwable);
129             mFinalStatus = finalStatus;
130         }
131     }
132
133     /**
134      * Raised from methods called by executeDownload() to indicate that the download should be
135      * retried immediately.
136      */
137     private class RetryDownload extends Throwable {}
138
139     /**
140      * Executes the download in a separate thread
141      */
142     public void run() {
143         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
144
145         State state = new State(mInfo);
146         AndroidHttpClient client = null;
147         PowerManager.WakeLock wakeLock = null;
148         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
149
150         try {
151             PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
152             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
153             wakeLock.acquire();
154
155
156             if (Constants.LOGV) {
157                 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
158             }
159
160             client = AndroidHttpClient.newInstance(userAgent(), mContext);
161
162             boolean finished = false;
163             while(!finished) {
164                 Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
165                 HttpGet request = new HttpGet(state.mRequestUri);
166                 try {
167                     executeDownload(state, client, request);
168                     finished = true;
169                 } catch (RetryDownload exc) {
170                     // fall through
171                 } finally {
172                     request.abort();
173                     request = null;
174                 }
175             }
176
177             if (Constants.LOGV) {
178                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
179             }
180             finalizeDestinationFile(state);
181             finalStatus = Downloads.Impl.STATUS_SUCCESS;
182         } catch (StopRequest error) {
183             // remove the cause before printing, in case it contains PII
184             Log.w(Constants.TAG,
185                     "Aborting request for download " + mInfo.mId + ": " + error.getMessage());
186             finalStatus = error.mFinalStatus;
187             // fall through to finally block
188         } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
189             Log.w(Constants.TAG, "Exception for id " + mInfo.mId + ": " + ex);
190             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
191             // falls through to the code that reports an error
192         } finally {
193             if (wakeLock != null) {
194                 wakeLock.release();
195                 wakeLock = null;
196             }
197             if (client != null) {
198                 client.close();
199                 client = null;
200             }
201             cleanupDestination(state, finalStatus);
202             notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
203                                     state.mRedirectCount, state.mGotData, state.mFilename,
204                                     state.mNewUri, state.mMimeType);
205             mInfo.mHasActiveThread = false;
206         }
207     }
208
209     /**
210      * Fully execute a single download request - setup and send the request, handle the response,
211      * and transfer the data to the destination file.
212      */
213     private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
214             throws StopRequest, RetryDownload {
215         InnerState innerState = new InnerState();
216         byte data[] = new byte[Constants.BUFFER_SIZE];
217
218         setupDestinationFile(state, innerState);
219         addRequestHeaders(innerState, request);
220
221         // check just before sending the request to avoid using an invalid connection at all
222         checkConnectivity(state);
223
224         HttpResponse response = sendRequest(state, client, request);
225         handleExceptionalStatus(state, innerState, response);
226
227         if (Constants.LOGV) {
228             Log.v(Constants.TAG, "received response for " + mInfo.mUri);
229         }
230
231         processResponseHeaders(state, innerState, response);
232         InputStream entityStream = openResponseEntity(state, response);
233         transferData(state, innerState, data, entityStream);
234     }
235
236     /**
237      * Check if current connectivity is valid for this request.
238      */
239     private void checkConnectivity(State state) throws StopRequest {
240         int networkUsable = mInfo.checkCanUseNetwork();
241         if (networkUsable != DownloadInfo.NETWORK_OK) {
242             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
243             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
244                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
245                 mInfo.notifyPauseDueToSize(true);
246             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
247                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
248                 mInfo.notifyPauseDueToSize(false);
249             }
250             throw new StopRequest(status, mInfo.getLogMessageForNetworkError(networkUsable));
251         }
252     }
253
254     /**
255      * Transfer as much data as possible from the HTTP response to the destination file.
256      * @param data buffer to use to read data
257      * @param entityStream stream for reading the HTTP response entity
258      */
259     private void transferData(State state, InnerState innerState, byte[] data,
260                                  InputStream entityStream) throws StopRequest {
261         for (;;) {
262             int bytesRead = readFromResponse(state, innerState, data, entityStream);
263             if (bytesRead == -1) { // success, end of stream already reached
264                 handleEndOfStream(state, innerState);
265                 return;
266             }
267
268             state.mGotData = true;
269             writeDataToDestination(state, data, bytesRead);
270             innerState.mBytesSoFar += bytesRead;
271             reportProgress(state, innerState);
272
273             if (Constants.LOGVV) {
274                 Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar + " for "
275                       + mInfo.mUri);
276             }
277
278             checkPausedOrCanceled(state);
279         }
280     }
281
282     /**
283      * Called after a successful completion to take any necessary action on the downloaded file.
284      */
285     private void finalizeDestinationFile(State state) throws StopRequest {
286         if (isDrmFile(state)) {
287             transferToDrm(state);
288         } else {
289             // make sure the file is readable
290             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
291             syncDestination(state);
292         }
293     }
294
295     /**
296      * Called just before the thread finishes, regardless of status, to take any necessary action on
297      * the downloaded file.
298      */
299     private void cleanupDestination(State state, int finalStatus) {
300         closeDestination(state);
301         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
302             new File(state.mFilename).delete();
303             state.mFilename = null;
304         }
305     }
306
307     /**
308      * Sync the destination file to storage.
309      */
310     private void syncDestination(State state) {
311         FileOutputStream downloadedFileStream = null;
312         try {
313             downloadedFileStream = new FileOutputStream(state.mFilename, true);
314             downloadedFileStream.getFD().sync();
315         } catch (FileNotFoundException ex) {
316             Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
317         } catch (SyncFailedException ex) {
318             Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
319         } catch (IOException ex) {
320             Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
321         } catch (RuntimeException ex) {
322             Log.w(Constants.TAG, "exception while syncing file: ", ex);
323         } finally {
324             if(downloadedFileStream != null) {
325                 try {
326                     downloadedFileStream.close();
327                 } catch (IOException ex) {
328                     Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
329                 } catch (RuntimeException ex) {
330                     Log.w(Constants.TAG, "exception while closing file: ", ex);
331                 }
332             }
333         }
334     }
335
336     /**
337      * @return true if the current download is a DRM file
338      */
339     private boolean isDrmFile(State state) {
340         return DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(state.mMimeType);
341     }
342
343     /**
344      * Transfer the downloaded destination file to the DRM store.
345      */
346     private void transferToDrm(State state) throws StopRequest {
347         File file = new File(state.mFilename);
348         Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
349         file.delete();
350
351         if (item == null) {
352             throw new StopRequest(Downloads.Impl.STATUS_UNKNOWN_ERROR,
353                     "unable to add file to DrmProvider");
354         } else {
355             state.mFilename = item.getDataString();
356             state.mMimeType = item.getType();
357         }
358     }
359
360     /**
361      * Close the destination output stream.
362      */
363     private void closeDestination(State state) {
364         try {
365             // close the file
366             if (state.mStream != null) {
367                 state.mStream.close();
368                 state.mStream = null;
369             }
370         } catch (IOException ex) {
371             if (Constants.LOGV) {
372                 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
373             }
374             // nothing can really be done if the file can't be closed
375         }
376     }
377
378     /**
379      * Check if the download has been paused or canceled, stopping the request appropriately if it
380      * has been.
381      */
382     private void checkPausedOrCanceled(State state) throws StopRequest {
383         synchronized (mInfo) {
384             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
385                 throw new StopRequest(Downloads.Impl.STATUS_PAUSED_BY_APP,
386                         "download paused by owner");
387             }
388         }
389         if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
390             throw new StopRequest(Downloads.Impl.STATUS_CANCELED, "download canceled");
391         }
392     }
393
394     /**
395      * Report download progress through the database if necessary.
396      */
397     private void reportProgress(State state, InnerState innerState) {
398         long now = mSystemFacade.currentTimeMillis();
399         if (innerState.mBytesSoFar - innerState.mBytesNotified
400                         > Constants.MIN_PROGRESS_STEP
401                 && now - innerState.mTimeLastNotification
402                         > Constants.MIN_PROGRESS_TIME) {
403             ContentValues values = new ContentValues();
404             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
405             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
406             innerState.mBytesNotified = innerState.mBytesSoFar;
407             innerState.mTimeLastNotification = now;
408         }
409     }
410
411     /**
412      * Write a data buffer to the destination file.
413      * @param data buffer containing the data to write
414      * @param bytesRead how many bytes to write from the buffer
415      */
416     private void writeDataToDestination(State state, byte[] data, int bytesRead)
417             throws StopRequest {
418         for (;;) {
419             try {
420                 if (state.mStream == null) {
421                     state.mStream = new FileOutputStream(state.mFilename, true);
422                 }
423                 state.mStream.write(data, 0, bytesRead);
424                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
425                             && !isDrmFile(state)) {
426                     closeDestination(state);
427                 }
428                 return;
429             } catch (IOException ex) {
430                 if (mInfo.isOnCache()) {
431                     if (Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
432                         continue;
433                     }
434                 } else if (!Helpers.isExternalMediaMounted()) {
435                     throw new StopRequest(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
436                             "external media not mounted while writing destination file");
437                 }
438
439                 long availableBytes =
440                     Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
441                 if (availableBytes < bytesRead) {
442                     throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
443                             "insufficient space while writing destination file", ex);
444                 }
445                 throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
446                         "while writing destination file: " + ex.toString(), ex);
447             }
448         }
449     }
450
451     /**
452      * Called when we've reached the end of the HTTP response stream, to update the database and
453      * check for consistency.
454      */
455     private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
456         ContentValues values = new ContentValues();
457         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
458         if (innerState.mHeaderContentLength == null) {
459             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
460         }
461         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
462
463         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
464                 && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
465         if (lengthMismatched) {
466             if (cannotResume(innerState)) {
467                 throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
468                         "mismatched content length");
469             } else {
470                 throw new StopRequest(getFinalStatusForHttpError(state),
471                         "closed socket before end of file");
472             }
473         }
474     }
475
476     private boolean cannotResume(InnerState innerState) {
477         return innerState.mBytesSoFar > 0 && !mInfo.mNoIntegrity && innerState.mHeaderETag == null;
478     }
479
480     /**
481      * Read some data from the HTTP response stream, handling I/O errors.
482      * @param data buffer to use to read data
483      * @param entityStream stream for reading the HTTP response entity
484      * @return the number of bytes actually read or -1 if the end of the stream has been reached
485      */
486     private int readFromResponse(State state, InnerState innerState, byte[] data,
487                                  InputStream entityStream) throws StopRequest {
488         try {
489             return entityStream.read(data);
490         } catch (IOException ex) {
491             logNetworkState();
492             ContentValues values = new ContentValues();
493             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
494             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
495             if (cannotResume(innerState)) {
496                 String message = "while reading response: " + ex.toString()
497                 + ", can't resume interrupted download with no ETag";
498                 throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
499                         message, ex);
500             } else {
501                 throw new StopRequest(getFinalStatusForHttpError(state),
502                         "while reading response: " + ex.toString(), ex);
503             }
504         }
505     }
506
507     /**
508      * Open a stream for the HTTP response entity, handling I/O errors.
509      * @return an InputStream to read the response entity
510      */
511     private InputStream openResponseEntity(State state, HttpResponse response)
512             throws StopRequest {
513         try {
514             return response.getEntity().getContent();
515         } catch (IOException ex) {
516             logNetworkState();
517             throw new StopRequest(getFinalStatusForHttpError(state),
518                     "while getting entity: " + ex.toString(), ex);
519         }
520     }
521
522     private void logNetworkState() {
523         if (Constants.LOGX) {
524             Log.i(Constants.TAG,
525                     "Net " + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up" : "Down"));
526         }
527     }
528
529     /**
530      * Read HTTP response headers and take appropriate action, including setting up the destination
531      * file and updating the database.
532      */
533     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
534             throws StopRequest {
535         if (innerState.mContinuingDownload) {
536             // ignore response headers on resume requests
537             return;
538         }
539
540         readResponseHeaders(state, innerState, response);
541
542         try {
543             state.mFilename = Helpers.generateSaveFile(
544                     mContext,
545                     mInfo.mUri,
546                     mInfo.mHint,
547                     innerState.mHeaderContentDisposition,
548                     innerState.mHeaderContentLocation,
549                     state.mMimeType,
550                     mInfo.mDestination,
551                     (innerState.mHeaderContentLength != null) ?
552                             Long.parseLong(innerState.mHeaderContentLength) : 0,
553                     mInfo.mIsPublicApi);
554         } catch (Helpers.GenerateSaveFileError exc) {
555             throw new StopRequest(exc.mStatus, exc.mMessage);
556         }
557         try {
558             state.mStream = new FileOutputStream(state.mFilename);
559         } catch (FileNotFoundException exc) {
560             throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
561                     "while opening destination file: " + exc.toString(), exc);
562         }
563         if (Constants.LOGV) {
564             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
565         }
566
567         updateDatabaseFromHeaders(state, innerState);
568         // check connectivity again now that we know the total size
569         checkConnectivity(state);
570     }
571
572     /**
573      * Update necessary database fields based on values of HTTP response headers that have been
574      * read.
575      */
576     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
577         ContentValues values = new ContentValues();
578         values.put(Downloads.Impl._DATA, state.mFilename);
579         if (innerState.mHeaderETag != null) {
580             values.put(Constants.ETAG, innerState.mHeaderETag);
581         }
582         if (state.mMimeType != null) {
583             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
584         }
585         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
586         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
587     }
588
589     /**
590      * Read headers from the HTTP response and store them into local state.
591      */
592     private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
593             throws StopRequest {
594         Header header = response.getFirstHeader("Content-Disposition");
595         if (header != null) {
596             innerState.mHeaderContentDisposition = header.getValue();
597         }
598         header = response.getFirstHeader("Content-Location");
599         if (header != null) {
600             innerState.mHeaderContentLocation = header.getValue();
601         }
602         if (state.mMimeType == null) {
603             header = response.getFirstHeader("Content-Type");
604             if (header != null) {
605                 state.mMimeType = sanitizeMimeType(header.getValue());
606             }
607         }
608         header = response.getFirstHeader("ETag");
609         if (header != null) {
610             innerState.mHeaderETag = header.getValue();
611         }
612         String headerTransferEncoding = null;
613         header = response.getFirstHeader("Transfer-Encoding");
614         if (header != null) {
615             headerTransferEncoding = header.getValue();
616         }
617         if (headerTransferEncoding == null) {
618             header = response.getFirstHeader("Content-Length");
619             if (header != null) {
620                 innerState.mHeaderContentLength = header.getValue();
621                 mInfo.mTotalBytes = Long.parseLong(innerState.mHeaderContentLength);
622             }
623         } else {
624             // Ignore content-length with transfer-encoding - 2616 4.4 3
625             if (Constants.LOGVV) {
626                 Log.v(Constants.TAG,
627                         "ignoring content-length because of xfer-encoding");
628             }
629         }
630         if (Constants.LOGVV) {
631             Log.v(Constants.TAG, "Content-Disposition: " +
632                     innerState.mHeaderContentDisposition);
633             Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
634             Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
635             Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
636             Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
637             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
638         }
639
640         boolean noSizeInfo = innerState.mHeaderContentLength == null
641                 && (headerTransferEncoding == null
642                     || !headerTransferEncoding.equalsIgnoreCase("chunked"));
643         if (!mInfo.mNoIntegrity && noSizeInfo) {
644             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
645                     "can't know size of download, giving up");
646         }
647     }
648
649     /**
650      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
651      */
652     private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
653             throws StopRequest, RetryDownload {
654         int statusCode = response.getStatusLine().getStatusCode();
655         if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
656             handleServiceUnavailable(state, response);
657         }
658         if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
659             handleRedirect(state, response, statusCode);
660         }
661
662         int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
663         if (statusCode != expectedStatus) {
664             handleOtherStatus(state, innerState, statusCode);
665         }
666     }
667
668     /**
669      * Handle a status that we don't know how to deal with properly.
670      */
671     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
672             throws StopRequest {
673         int finalStatus;
674         if (Downloads.Impl.isStatusError(statusCode)) {
675             finalStatus = statusCode;
676         } else if (statusCode >= 300 && statusCode < 400) {
677             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
678         } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
679             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
680         } else {
681             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
682         }
683         throw new StopRequest(finalStatus, "http error " + statusCode);
684     }
685
686     /**
687      * Handle a 3xx redirect status.
688      */
689     private void handleRedirect(State state, HttpResponse response, int statusCode)
690             throws StopRequest, RetryDownload {
691         if (Constants.LOGVV) {
692             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
693         }
694         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
695             throw new StopRequest(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS, "too many redirects");
696         }
697         Header header = response.getFirstHeader("Location");
698         if (header == null) {
699             return;
700         }
701         if (Constants.LOGVV) {
702             Log.v(Constants.TAG, "Location :" + header.getValue());
703         }
704
705         String newUri;
706         try {
707             newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
708         } catch(URISyntaxException ex) {
709             if (Constants.LOGV) {
710                 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
711                         + " for " + mInfo.mUri);
712             }
713             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
714                     "Couldn't resolve redirect URI");
715         }
716         ++state.mRedirectCount;
717         state.mRequestUri = newUri;
718         if (statusCode == 301 || statusCode == 303) {
719             // use the new URI for all future requests (should a retry/resume be necessary)
720             state.mNewUri = newUri;
721         }
722         throw new RetryDownload();
723     }
724
725     /**
726      * Handle a 503 Service Unavailable status by processing the Retry-After header.
727      */
728     private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
729         if (Constants.LOGVV) {
730             Log.v(Constants.TAG, "got HTTP response code 503");
731         }
732         state.mCountRetry = true;
733         Header header = response.getFirstHeader("Retry-After");
734         if (header != null) {
735            try {
736                if (Constants.LOGVV) {
737                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
738                }
739                state.mRetryAfter = Integer.parseInt(header.getValue());
740                if (state.mRetryAfter < 0) {
741                    state.mRetryAfter = 0;
742                } else {
743                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
744                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
745                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
746                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
747                    }
748                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
749                    state.mRetryAfter *= 1000;
750                }
751            } catch (NumberFormatException ex) {
752                // ignored - retryAfter stays 0 in this case.
753            }
754         }
755         throw new StopRequest(Downloads.Impl.STATUS_WAITING_TO_RETRY,
756                 "got 503 Service Unavailable, will retry later");
757     }
758
759     /**
760      * Send the request to the server, handling any I/O exceptions.
761      */
762     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
763             throws StopRequest {
764         try {
765             return client.execute(request);
766         } catch (IllegalArgumentException ex) {
767             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
768                     "while trying to execute request: " + ex.toString(), ex);
769         } catch (IOException ex) {
770             logNetworkState();
771             throw new StopRequest(getFinalStatusForHttpError(state),
772                     "while trying to execute request: " + ex.toString(), ex);
773         }
774     }
775
776     private int getFinalStatusForHttpError(State state) {
777         if (!Helpers.isNetworkAvailable(mSystemFacade)) {
778             return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
779         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
780             state.mCountRetry = true;
781             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
782         } else {
783             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
784             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
785         }
786     }
787
788     /**
789      * Prepare the destination file to receive data.  If the file already exists, we'll set up
790      * appropriately for resumption.
791      */
792     private void setupDestinationFile(State state, InnerState innerState)
793             throws StopRequest {
794         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
795             if (!Helpers.isFilenameValid(state.mFilename)) {
796                 // this should never happen
797                 throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
798                         "found invalid internal destination filename");
799             }
800             // We're resuming a download that got interrupted
801             File f = new File(state.mFilename);
802             if (f.exists()) {
803                 long fileLength = f.length();
804                 if (fileLength == 0) {
805                     // The download hadn't actually started, we can restart from scratch
806                     f.delete();
807                     state.mFilename = null;
808                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
809                     // This should've been caught upon failure
810                     f.delete();
811                     throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
812                             "Trying to resume a download that can't be resumed");
813                 } else {
814                     // All right, we'll be able to resume this download
815                     try {
816                         state.mStream = new FileOutputStream(state.mFilename, true);
817                     } catch (FileNotFoundException exc) {
818                         throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
819                                 "while opening destination for resuming: " + exc.toString(), exc);
820                     }
821                     innerState.mBytesSoFar = (int) fileLength;
822                     if (mInfo.mTotalBytes != -1) {
823                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
824                     }
825                     innerState.mHeaderETag = mInfo.mETag;
826                     innerState.mContinuingDownload = true;
827                 }
828             }
829         }
830
831         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
832                 && !isDrmFile(state)) {
833             closeDestination(state);
834         }
835     }
836
837     /**
838      * Add custom headers for this download to the HTTP request.
839      */
840     private void addRequestHeaders(InnerState innerState, HttpGet request) {
841         for (Pair<String, String> header : mInfo.getHeaders()) {
842             request.addHeader(header.first, header.second);
843         }
844
845         if (innerState.mContinuingDownload) {
846             if (innerState.mHeaderETag != null) {
847                 request.addHeader("If-Match", innerState.mHeaderETag);
848             }
849             request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
850         }
851     }
852
853     /**
854      * Stores information about the completed download, and notifies the initiating application.
855      */
856     private void notifyDownloadCompleted(
857             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
858             String filename, String uri, String mimeType) {
859         notifyThroughDatabase(
860                 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
861         if (Downloads.Impl.isStatusCompleted(status)) {
862             mInfo.sendIntentIfRequested();
863         }
864     }
865
866     private void notifyThroughDatabase(
867             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
868             String filename, String uri, String mimeType) {
869         ContentValues values = new ContentValues();
870         values.put(Downloads.Impl.COLUMN_STATUS, status);
871         values.put(Downloads.Impl._DATA, filename);
872         if (uri != null) {
873             values.put(Downloads.Impl.COLUMN_URI, uri);
874         }
875         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
876         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
877         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
878         if (!countRetry) {
879             values.put(Constants.FAILED_CONNECTIONS, 0);
880         } else if (gotData) {
881             values.put(Constants.FAILED_CONNECTIONS, 1);
882         } else {
883             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
884         }
885
886         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
887     }
888
889     /**
890      * Clean up a mimeType string so it can be used to dispatch an intent to
891      * view a downloaded asset.
892      * @param mimeType either null or one or more mime types (semi colon separated).
893      * @return null if mimeType was null. Otherwise a string which represents a
894      * single mimetype in lowercase and with surrounding whitespaces trimmed.
895      */
896     private static String sanitizeMimeType(String mimeType) {
897         try {
898             mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
899
900             final int semicolonIndex = mimeType.indexOf(';');
901             if (semicolonIndex != -1) {
902                 mimeType = mimeType.substring(0, semicolonIndex);
903             }
904             return mimeType;
905         } catch (NullPointerException npe) {
906             return null;
907         }
908     }
909 }