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