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