Implement dialogs for wifi required + recommended limits.
[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.util.Config;
30 import android.util.Log;
31
32 import org.apache.http.Header;
33 import org.apache.http.HttpResponse;
34 import org.apache.http.client.methods.HttpGet;
35
36 import java.io.File;
37 import java.io.FileNotFoundException;
38 import java.io.FileOutputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.io.SyncFailedException;
42 import java.net.URI;
43 import java.net.URISyntaxException;
44 import java.util.Locale;
45 import java.util.Map;
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     private class StopRequest extends Throwable {
116         public int mFinalStatus;
117
118         public StopRequest(int finalStatus) {
119             mFinalStatus = finalStatus;
120         }
121
122         public StopRequest(int finalStatus, Throwable throwable) {
123             super(throwable);
124             mFinalStatus = finalStatus;
125         }
126     }
127
128     /**
129      * Raised from methods called by executeDownload() to indicate that the download should be
130      * retried immediately.
131      */
132     private class RetryDownload extends Throwable {}
133
134     /**
135      * Executes the download in a separate thread
136      */
137     public void run() {
138         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
139
140         State state = new State(mInfo);
141         AndroidHttpClient client = null;
142         PowerManager.WakeLock wakeLock = null;
143         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
144         mInfo.mPausedReason = null;
145
146         try {
147             PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
148             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
149             wakeLock.acquire();
150
151
152             if (Constants.LOGV) {
153                 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
154             }
155
156             client = AndroidHttpClient.newInstance(userAgent(), mContext);
157
158             boolean finished = false;
159             while(!finished) {
160                 HttpGet request = new HttpGet(state.mRequestUri);
161                 try {
162                     executeDownload(state, client, request);
163                     finished = true;
164                 } catch (RetryDownload exc) {
165                     // fall through
166                 } finally {
167                     request.abort();
168                     request = null;
169                 }
170             }
171
172             if (Constants.LOGV) {
173                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
174             }
175             finalizeDestinationFile(state);
176             finalStatus = Downloads.Impl.STATUS_SUCCESS;
177         } catch (StopRequest error) {
178             if (Constants.LOGV) {
179                 Log.v(Constants.TAG, "Aborting request for " + mInfo.mUri, error);
180             }
181             finalStatus = error.mFinalStatus;
182             // fall through to finally block
183         } catch (FileNotFoundException ex) {
184             Log.d(Constants.TAG, "FileNotFoundException for " + state.mFilename + " : " +  ex);
185             finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
186             // falls through to the code that reports an error
187         } catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
188             if (Constants.LOGV) {
189                 Log.d(Constants.TAG, "Exception for " + mInfo.mUri, ex);
190             } else if (Config.LOGD) {
191                 Log.d(Constants.TAG, "Exception for id " + mInfo.mId, ex);
192             }
193             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
194             // falls through to the code that reports an error
195         } finally {
196             mInfo.mHasActiveThread = false;
197             if (wakeLock != null) {
198                 wakeLock.release();
199                 wakeLock = null;
200             }
201             if (client != null) {
202                 client.close();
203                 client = null;
204             }
205             cleanupDestination(state, finalStatus);
206             notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
207                                     state.mRedirectCount, state.mGotData, state.mFilename,
208                                     state.mNewUri, state.mMimeType);
209         }
210     }
211
212     /**
213      * Fully execute a single download request - setup and send the request, handle the response,
214      * and transfer the data to the destination file.
215      */
216     private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
217             throws StopRequest, RetryDownload, FileNotFoundException {
218         InnerState innerState = new InnerState();
219         byte data[] = new byte[Constants.BUFFER_SIZE];
220
221         setupDestinationFile(state, innerState);
222         addRequestHeaders(innerState, request);
223
224         // check just before sending the request to avoid using an invalid connection at all
225         checkConnectivity(state);
226
227         HttpResponse response = sendRequest(state, client, request);
228         handleExceptionalStatus(state, innerState, response);
229
230         if (Constants.LOGV) {
231             Log.v(Constants.TAG, "received response for " + mInfo.mUri);
232         }
233
234         processResponseHeaders(state, innerState, response);
235         InputStream entityStream = openResponseEntity(state, response);
236         transferData(state, innerState, data, entityStream);
237     }
238
239     /**
240      * Check if current connectivity is valid for this request.
241      */
242     private void checkConnectivity(State state) throws StopRequest {
243         int networkUsable = mInfo.checkCanUseNetwork();
244         if (networkUsable != DownloadInfo.NETWORK_OK) {
245             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
246                 mInfo.notifyPauseDueToSize(true);
247             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
248                 mInfo.notifyPauseDueToSize(false);
249             }
250             throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
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             Log.w(Constants.TAG, "unable to add file " + state.mFilename + " to DrmProvider");
353             throw new StopRequest(Downloads.Impl.STATUS_UNKNOWN_ERROR);
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                 if (Constants.LOGV) {
386                     Log.v(Constants.TAG, "paused " + mInfo.mUri);
387                 }
388                 throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
389             }
390         }
391         if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
392             if (Constants.LOGV) {
393                 Log.d(Constants.TAG, "canceled " + mInfo.mUri);
394             }
395             throw new StopRequest(Downloads.Impl.STATUS_CANCELED);
396         }
397     }
398
399     /**
400      * Report download progress through the database if necessary.
401      */
402     private void reportProgress(State state, InnerState innerState) {
403         long now = mSystemFacade.currentTimeMillis();
404         if (innerState.mBytesSoFar - innerState.mBytesNotified
405                         > Constants.MIN_PROGRESS_STEP
406                 && now - innerState.mTimeLastNotification
407                         > Constants.MIN_PROGRESS_TIME) {
408             ContentValues values = new ContentValues();
409             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
410             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
411             innerState.mBytesNotified = innerState.mBytesSoFar;
412             innerState.mTimeLastNotification = now;
413         }
414     }
415
416     /**
417      * Write a data buffer to the destination file.
418      * @param data buffer containing the data to write
419      * @param bytesRead how many bytes to write from the buffer
420      */
421     private void writeDataToDestination(State state, byte[] data, int bytesRead)
422             throws StopRequest {
423         for (;;) {
424             try {
425                 if (state.mStream == null) {
426                     state.mStream = new FileOutputStream(state.mFilename, true);
427                 }
428                 state.mStream.write(data, 0, bytesRead);
429                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
430                             && !isDrmFile(state)) {
431                     closeDestination(state);
432                 }
433                 return;
434             } catch (IOException ex) {
435                 if (mInfo.isOnCache()) {
436                     if (Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
437                         continue;
438                     }
439                 } else if (!Helpers.isExternalMediaMounted()) {
440                     throw new StopRequest(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
441                 }
442
443                 long availableBytes =
444                     Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
445                 if (availableBytes < bytesRead) {
446                     throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR, ex);
447                 }
448                 throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR, ex);
449             }
450         }
451     }
452
453     /**
454      * Called when we've reached the end of the HTTP response stream, to update the database and
455      * check for consistency.
456      */
457     private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
458         ContentValues values = new ContentValues();
459         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
460         if (innerState.mHeaderContentLength == null) {
461             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
462         }
463         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
464
465         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
466                 && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
467         if (lengthMismatched) {
468             if (cannotResume(innerState)) {
469                 if (Constants.LOGV) {
470                     Log.d(Constants.TAG, "mismatched content length " +
471                             mInfo.mUri);
472                 } else if (Config.LOGD) {
473                     Log.d(Constants.TAG, "mismatched content length for " +
474                             mInfo.mId);
475                 }
476                 throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME);
477             } else {
478                 throw new StopRequest(handleHttpError(state, "closed socket"));
479             }
480         }
481     }
482
483     private boolean cannotResume(InnerState innerState) {
484         return innerState.mBytesSoFar > 0 && !mInfo.mNoIntegrity && innerState.mHeaderETag == null;
485     }
486
487     /**
488      * Read some data from the HTTP response stream, handling I/O errors.
489      * @param data buffer to use to read data
490      * @param entityStream stream for reading the HTTP response entity
491      * @return the number of bytes actually read or -1 if the end of the stream has been reached
492      */
493     private int readFromResponse(State state, InnerState innerState, byte[] data,
494                                  InputStream entityStream) throws StopRequest {
495         try {
496             return entityStream.read(data);
497         } catch (IOException ex) {
498             logNetworkState();
499             ContentValues values = new ContentValues();
500             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
501             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
502             if (cannotResume(innerState)) {
503                 Log.d(Constants.TAG, "download IOException for download " + mInfo.mId, ex);
504                 Log.d(Constants.TAG, "can't resume interrupted download with no ETag");
505                 throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME, ex);
506             } else {
507                 throw new StopRequest(handleHttpError(state, "download IOException"), ex);
508             }
509         }
510     }
511
512     /**
513      * Open a stream for the HTTP response entity, handling I/O errors.
514      * @return an InputStream to read the response entity
515      */
516     private InputStream openResponseEntity(State state, HttpResponse response)
517             throws StopRequest {
518         try {
519             return response.getEntity().getContent();
520         } catch (IOException ex) {
521             logNetworkState();
522             throw new StopRequest(handleHttpError(state, "IOException getting entity"), ex);
523         }
524     }
525
526     private void logNetworkState() {
527         if (Constants.LOGX) {
528             Log.i(Constants.TAG,
529                     "Net " + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up" : "Down"));
530         }
531     }
532
533     /**
534      * Read HTTP response headers and take appropriate action, including setting up the destination
535      * file and updating the database.
536      */
537     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
538             throws StopRequest, FileNotFoundException {
539         if (innerState.mContinuingDownload) {
540             // ignore response headers on resume requests
541             return;
542         }
543
544         readResponseHeaders(state, innerState, response);
545
546         DownloadFileInfo fileInfo = Helpers.generateSaveFile(
547                 mContext,
548                 mInfo.mUri,
549                 mInfo.mHint,
550                 innerState.mHeaderContentDisposition,
551                 innerState.mHeaderContentLocation,
552                 state.mMimeType,
553                 mInfo.mDestination,
554                 (innerState.mHeaderContentLength != null) ?
555                         Long.parseLong(innerState.mHeaderContentLength) : 0,
556                 mInfo.mIsPublicApi);
557         if (fileInfo.mFileName == null) {
558             throw new StopRequest(fileInfo.mStatus);
559         }
560         state.mFilename = fileInfo.mFileName;
561         state.mStream = fileInfo.mStream;
562         if (Constants.LOGV) {
563             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
564         }
565
566         updateDatabaseFromHeaders(state, innerState);
567         // check connectivity again now that we know the total size
568         checkConnectivity(state);
569     }
570
571     /**
572      * Update necessary database fields based on values of HTTP response headers that have been
573      * read.
574      */
575     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
576         ContentValues values = new ContentValues();
577         values.put(Downloads.Impl._DATA, state.mFilename);
578         if (innerState.mHeaderETag != null) {
579             values.put(Constants.ETAG, innerState.mHeaderETag);
580         }
581         if (state.mMimeType != null) {
582             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
583         }
584         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
585         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
586     }
587
588     /**
589      * Read headers from the HTTP response and store them into local state.
590      */
591     private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
592             throws StopRequest {
593         Header header = response.getFirstHeader("Content-Disposition");
594         if (header != null) {
595             innerState.mHeaderContentDisposition = header.getValue();
596         }
597         header = response.getFirstHeader("Content-Location");
598         if (header != null) {
599             innerState.mHeaderContentLocation = header.getValue();
600         }
601         if (state.mMimeType == null) {
602             header = response.getFirstHeader("Content-Type");
603             if (header != null) {
604                 state.mMimeType = sanitizeMimeType(header.getValue());
605             }
606         }
607         header = response.getFirstHeader("ETag");
608         if (header != null) {
609             innerState.mHeaderETag = header.getValue();
610         }
611         String headerTransferEncoding = null;
612         header = response.getFirstHeader("Transfer-Encoding");
613         if (header != null) {
614             headerTransferEncoding = header.getValue();
615         }
616         if (headerTransferEncoding == null) {
617             header = response.getFirstHeader("Content-Length");
618             if (header != null) {
619                 innerState.mHeaderContentLength = header.getValue();
620                 mInfo.mTotalBytes = Long.parseLong(innerState.mHeaderContentLength);
621             }
622         } else {
623             // Ignore content-length with transfer-encoding - 2616 4.4 3
624             if (Constants.LOGVV) {
625                 Log.v(Constants.TAG,
626                         "ignoring content-length because of xfer-encoding");
627             }
628         }
629         if (Constants.LOGVV) {
630             Log.v(Constants.TAG, "Content-Disposition: " +
631                     innerState.mHeaderContentDisposition);
632             Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
633             Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
634             Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
635             Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
636             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
637         }
638
639         boolean noSizeInfo = innerState.mHeaderContentLength == null
640                 && (headerTransferEncoding == null
641                     || !headerTransferEncoding.equalsIgnoreCase("chunked"));
642         if (!mInfo.mNoIntegrity && noSizeInfo) {
643             Log.d(Constants.TAG, "can't know size of download, giving up");
644             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR);
645         }
646     }
647
648     /**
649      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
650      */
651     private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
652             throws StopRequest, RetryDownload {
653         int statusCode = response.getStatusLine().getStatusCode();
654         if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
655             handleServiceUnavailable(state, response);
656         }
657         if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
658             handleRedirect(state, response, statusCode);
659         }
660
661         int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
662         if (statusCode != expectedStatus) {
663             handleOtherStatus(state, innerState, statusCode);
664         }
665     }
666
667     /**
668      * Handle a status that we don't know how to deal with properly.
669      */
670     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
671             throws StopRequest {
672         if (Constants.LOGV) {
673             Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
674         } else if (Config.LOGD) {
675             Log.d(Constants.TAG, "http error " + statusCode + " for download " +
676                     mInfo.mId);
677         }
678         int finalStatus;
679         if (Downloads.Impl.isStatusError(statusCode)) {
680             finalStatus = statusCode;
681         } else if (statusCode >= 300 && statusCode < 400) {
682             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
683         } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
684             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
685         } else {
686             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
687         }
688         throw new StopRequest(finalStatus);
689     }
690
691     /**
692      * Handle a 3xx redirect status.
693      */
694     private void handleRedirect(State state, HttpResponse response, int statusCode)
695             throws StopRequest, RetryDownload {
696         if (Constants.LOGVV) {
697             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
698         }
699         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
700             if (Constants.LOGV) {
701                 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
702                         " at " + mInfo.mUri);
703             } else if (Config.LOGD) {
704                 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
705             }
706             throw new StopRequest(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS);
707         }
708         Header header = response.getFirstHeader("Location");
709         if (header == null) {
710             return;
711         }
712         if (Constants.LOGVV) {
713             Log.v(Constants.TAG, "Location :" + header.getValue());
714         }
715
716         String newUri;
717         try {
718             newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
719         } catch(URISyntaxException ex) {
720             if (Constants.LOGV) {
721                 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
722                         + " for " + mInfo.mUri);
723             } else if (Config.LOGD) {
724                 Log.d(Constants.TAG,
725                         "Couldn't resolve redirect URI for download " +
726                         mInfo.mId);
727             }
728             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR);
729         }
730         ++state.mRedirectCount;
731         state.mRequestUri = newUri;
732         if (statusCode == 301 || statusCode == 303) {
733             // use the new URI for all future requests (should a retry/resume be necessary)
734             state.mNewUri = newUri;
735         }
736         throw new RetryDownload();
737     }
738
739     /**
740      * Handle a 503 Service Unavailable status by processing the Retry-After header.
741      */
742     private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
743         if (Constants.LOGVV) {
744             Log.v(Constants.TAG, "got HTTP response code 503");
745         }
746         state.mCountRetry = true;
747         Header header = response.getFirstHeader("Retry-After");
748         if (header != null) {
749            try {
750                if (Constants.LOGVV) {
751                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
752                }
753                state.mRetryAfter = Integer.parseInt(header.getValue());
754                if (state.mRetryAfter < 0) {
755                    state.mRetryAfter = 0;
756                } else {
757                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
758                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
759                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
760                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
761                    }
762                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
763                    state.mRetryAfter *= 1000;
764                }
765            } catch (NumberFormatException ex) {
766                // ignored - retryAfter stays 0 in this case.
767            }
768         }
769         throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
770     }
771
772     /**
773      * Send the request to the server, handling any I/O exceptions.
774      */
775     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
776             throws StopRequest {
777         try {
778             return client.execute(request);
779         } catch (IllegalArgumentException ex) {
780             if (Constants.LOGV) {
781                 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
782                         mInfo.mUri + " : " + ex);
783             } else if (Config.LOGD) {
784                 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
785                         mInfo.mId + " : " +  ex);
786             }
787             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR, ex);
788         } catch (IOException ex) {
789             logNetworkState();
790             throw new StopRequest(handleHttpError(state, "IOException trying to execute request"),
791                     ex);
792         }
793     }
794
795     /**
796      * @return the final status for this attempt
797      */
798     private int handleHttpError(State state, String message) {
799         if (Constants.LOGV) {
800             Log.d(Constants.TAG, message + " for " + mInfo.mUri);
801         }
802
803         if (!Helpers.isNetworkAvailable(mSystemFacade)) {
804             return Downloads.Impl.STATUS_RUNNING_PAUSED;
805         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
806             state.mCountRetry = true;
807             return Downloads.Impl.STATUS_RUNNING_PAUSED;
808         } else {
809             Log.d(Constants.TAG, "reached max retries: " + message + " for " + mInfo.mId);
810             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
811         }
812     }
813
814     /**
815      * Prepare the destination file to receive data.  If the file already exists, we'll set up
816      * appropriately for resumption.
817      */
818     private void setupDestinationFile(State state, InnerState innerState)
819             throws StopRequest, FileNotFoundException {
820         if (state.mFilename != null) { // only true if we've already run a thread for this download
821             if (!Helpers.isFilenameValid(state.mFilename)) {
822                 throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR);
823             }
824             // We're resuming a download that got interrupted
825             File f = new File(state.mFilename);
826             if (f.exists()) {
827                 long fileLength = f.length();
828                 if (fileLength == 0) {
829                     // The download hadn't actually started, we can restart from scratch
830                     f.delete();
831                     state.mFilename = null;
832                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
833                     // This should've been caught upon failure
834                     Log.wtf(Constants.TAG, "Trying to resume a download that can't be resumed");
835                     f.delete();
836                     throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME);
837                 } else {
838                     // All right, we'll be able to resume this download
839                     state.mStream = new FileOutputStream(state.mFilename, true);
840                     innerState.mBytesSoFar = (int) fileLength;
841                     if (mInfo.mTotalBytes != -1) {
842                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
843                     }
844                     innerState.mHeaderETag = mInfo.mETag;
845                     innerState.mContinuingDownload = true;
846                 }
847             }
848         }
849
850         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
851                 && !isDrmFile(state)) {
852             closeDestination(state);
853         }
854     }
855
856     /**
857      * Add custom headers for this download to the HTTP request.
858      */
859     private void addRequestHeaders(InnerState innerState, HttpGet request) {
860         for (Map.Entry<String, String> header : mInfo.getHeaders().entrySet()) {
861             request.addHeader(header.getKey(), header.getValue());
862         }
863
864         if (innerState.mContinuingDownload) {
865             if (innerState.mHeaderETag != null) {
866                 request.addHeader("If-Match", innerState.mHeaderETag);
867             }
868             request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
869         }
870     }
871
872     /**
873      * Stores information about the completed download, and notifies the initiating application.
874      */
875     private void notifyDownloadCompleted(
876             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
877             String filename, String uri, String mimeType) {
878         notifyThroughDatabase(
879                 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
880         if (Downloads.Impl.isStatusCompleted(status)) {
881             mInfo.sendIntentIfRequested();
882         }
883     }
884
885     private void notifyThroughDatabase(
886             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
887             String filename, String uri, String mimeType) {
888         ContentValues values = new ContentValues();
889         values.put(Downloads.Impl.COLUMN_STATUS, status);
890         values.put(Downloads.Impl._DATA, filename);
891         if (uri != null) {
892             values.put(Downloads.Impl.COLUMN_URI, uri);
893         }
894         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
895         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
896         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
897         if (!countRetry) {
898             values.put(Constants.FAILED_CONNECTIONS, 0);
899         } else if (gotData) {
900             values.put(Constants.FAILED_CONNECTIONS, 1);
901         } else {
902             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
903         }
904
905         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
906     }
907
908     /**
909      * Clean up a mimeType string so it can be used to dispatch an intent to
910      * view a downloaded asset.
911      * @param mimeType either null or one or more mime types (semi colon separated).
912      * @return null if mimeType was null. Otherwise a string which represents a
913      * single mimetype in lowercase and with surrounding whitespaces trimmed.
914      */
915     private static String sanitizeMimeType(String mimeType) {
916         try {
917             mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
918
919             final int semicolonIndex = mimeType.indexOf(';');
920             if (semicolonIndex != -1) {
921                 mimeType = mimeType.substring(0, semicolonIndex);
922             }
923             return mimeType;
924         } catch (NullPointerException npe) {
925             return null;
926         }
927     }
928 }