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