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