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