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