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