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