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