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