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