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