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