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