am ebb555c0: Support multi-valued HTTP headers, handle new API changes
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadThread.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads;
18
19 import org.apache.http.conn.params.ConnRouteParams;
20
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.drm.mobile1.DrmRawContent;
25 import android.net.http.AndroidHttpClient;
26 import android.net.Proxy;
27 import android.net.Uri;
28 import android.os.FileUtils;
29 import android.os.PowerManager;
30 import android.os.Process;
31 import android.provider.Downloads;
32 import android.provider.DrmStore;
33 import android.util.Config;
34 import android.util.Log;
35 import android.util.Pair;
36
37 import org.apache.http.Header;
38 import org.apache.http.HttpResponse;
39 import org.apache.http.client.methods.HttpGet;
40
41 import java.io.File;
42 import java.io.FileNotFoundException;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.SyncFailedException;
47 import java.net.URI;
48 import java.net.URISyntaxException;
49 import java.util.Locale;
50
51 /**
52  * Runs an actual download
53  */
54 public class DownloadThread extends Thread {
55
56     private Context mContext;
57     private DownloadInfo mInfo;
58     private SystemFacade mSystemFacade;
59
60     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info) {
61         mContext = context;
62         mSystemFacade = systemFacade;
63         mInfo = info;
64     }
65
66     /**
67      * Returns the user agent provided by the initiating app, or use the default one
68      */
69     private String userAgent() {
70         String userAgent = mInfo.mUserAgent;
71         if (userAgent != null) {
72         }
73         if (userAgent == null) {
74             userAgent = Constants.DEFAULT_USER_AGENT;
75         }
76         return userAgent;
77     }
78
79     /**
80      * State for the entire run() method.
81      */
82     private static class State {
83         public String mFilename;
84         public FileOutputStream mStream;
85         public String mMimeType;
86         public boolean mCountRetry = false;
87         public int mRetryAfter = 0;
88         public int mRedirectCount = 0;
89         public String mNewUri;
90         public boolean mGotData = false;
91         public String mRequestUri;
92
93         public State(DownloadInfo info) {
94             mMimeType = sanitizeMimeType(info.mMimeType);
95             mRedirectCount = info.mRedirectCount;
96             mRequestUri = info.mUri;
97             mFilename = info.mFileName;
98         }
99     }
100
101     /**
102      * State within executeDownload()
103      */
104     private static class InnerState {
105         public int mBytesSoFar = 0;
106         public String mHeaderETag;
107         public boolean mContinuingDownload = false;
108         public String mHeaderContentLength;
109         public String mHeaderContentDisposition;
110         public String mHeaderContentLocation;
111         public int mBytesNotified = 0;
112         public long mTimeLastNotification = 0;
113     }
114
115     /**
116      * Raised from methods called by run() to indicate that the current request should be stopped
117      * immediately.
118      */
119     private class StopRequest extends Throwable {
120         public int mFinalStatus;
121
122         public StopRequest(int finalStatus) {
123             mFinalStatus = finalStatus;
124         }
125
126         public StopRequest(int finalStatus, Throwable throwable) {
127             super(throwable);
128             mFinalStatus = finalStatus;
129         }
130     }
131
132     /**
133      * Raised from methods called by executeDownload() to indicate that the download should be
134      * retried immediately.
135      */
136     private class RetryDownload extends Throwable {}
137
138     /**
139      * Executes the download in a separate thread
140      */
141     public void run() {
142         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
143
144         State state = new State(mInfo);
145         AndroidHttpClient client = null;
146         PowerManager.WakeLock wakeLock = null;
147         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
148         mInfo.mPausedReason = null;
149
150         try {
151             PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
152             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
153             wakeLock.acquire();
154
155
156             if (Constants.LOGV) {
157                 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
158             }
159
160             client = AndroidHttpClient.newInstance(userAgent(), mContext);
161
162             boolean finished = false;
163             while(!finished) {
164                 // Set or unset proxy, which may have changed since last GET request.
165                 // setDefaultProxy() supports null as proxy parameter.
166                 ConnRouteParams.setDefaultProxy(client.getParams(),
167                         Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
168                 HttpGet request = new HttpGet(state.mRequestUri);
169                 try {
170                     executeDownload(state, client, request);
171                     finished = true;
172                 } catch (RetryDownload exc) {
173                     // fall through
174                 } finally {
175                     request.abort();
176                     request = null;
177                 }
178             }
179
180             if (Constants.LOGV) {
181                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
182             }
183             finalizeDestinationFile(state);
184             finalStatus = Downloads.Impl.STATUS_SUCCESS;
185         } catch (StopRequest error) {
186             if (Constants.LOGV) {
187                 Log.v(Constants.TAG, "Aborting request for " + mInfo.mUri, error);
188             }
189             finalStatus = error.mFinalStatus;
190             // fall through to finally block
191         } catch (FileNotFoundException ex) {
192             Log.d(Constants.TAG, "FileNotFoundException for " + state.mFilename + " : " +  ex);
193             finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
194             // falls through to the code that reports an error
195         } catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
196             if (Constants.LOGV) {
197                 Log.d(Constants.TAG, "Exception for " + mInfo.mUri, ex);
198             } else if (Config.LOGD) {
199                 Log.d(Constants.TAG, "Exception for id " + mInfo.mId, ex);
200             }
201             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
202             // falls through to the code that reports an error
203         } finally {
204             mInfo.mHasActiveThread = false;
205             if (wakeLock != null) {
206                 wakeLock.release();
207                 wakeLock = null;
208             }
209             if (client != null) {
210                 client.close();
211                 client = null;
212             }
213             cleanupDestination(state, finalStatus);
214             notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
215                                     state.mRedirectCount, state.mGotData, state.mFilename,
216                                     state.mNewUri, state.mMimeType);
217         }
218     }
219
220     /**
221      * Fully execute a single download request - setup and send the request, handle the response,
222      * and transfer the data to the destination file.
223      */
224     private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
225             throws StopRequest, RetryDownload, FileNotFoundException {
226         InnerState innerState = new InnerState();
227         byte data[] = new byte[Constants.BUFFER_SIZE];
228
229         setupDestinationFile(state, innerState);
230         addRequestHeaders(innerState, request);
231
232         // check just before sending the request to avoid using an invalid connection at all
233         checkConnectivity(state);
234
235         HttpResponse response = sendRequest(state, client, request);
236         handleExceptionalStatus(state, innerState, response);
237
238         if (Constants.LOGV) {
239             Log.v(Constants.TAG, "received response for " + mInfo.mUri);
240         }
241
242         processResponseHeaders(state, innerState, response);
243         InputStream entityStream = openResponseEntity(state, response);
244         transferData(state, innerState, data, entityStream);
245     }
246
247     /**
248      * Check if current connectivity is valid for this request.
249      */
250     private void checkConnectivity(State state) throws StopRequest {
251         int networkUsable = mInfo.checkCanUseNetwork();
252         if (networkUsable != DownloadInfo.NETWORK_OK) {
253             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
254                 mInfo.notifyPauseDueToSize(true);
255             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
256                 mInfo.notifyPauseDueToSize(false);
257             }
258             throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
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             Log.w(Constants.TAG, "unable to add file " + state.mFilename + " to DrmProvider");
361             throw new StopRequest(Downloads.Impl.STATUS_UNKNOWN_ERROR);
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                 if (Constants.LOGV) {
394                     Log.v(Constants.TAG, "paused " + mInfo.mUri);
395                 }
396                 throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
397             }
398         }
399         if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
400             if (Constants.LOGV) {
401                 Log.d(Constants.TAG, "canceled " + mInfo.mUri);
402             }
403             throw new StopRequest(Downloads.Impl.STATUS_CANCELED);
404         }
405     }
406
407     /**
408      * Report download progress through the database if necessary.
409      */
410     private void reportProgress(State state, InnerState innerState) {
411         long now = mSystemFacade.currentTimeMillis();
412         if (innerState.mBytesSoFar - innerState.mBytesNotified
413                         > Constants.MIN_PROGRESS_STEP
414                 && now - innerState.mTimeLastNotification
415                         > Constants.MIN_PROGRESS_TIME) {
416             ContentValues values = new ContentValues();
417             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
418             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
419             innerState.mBytesNotified = innerState.mBytesSoFar;
420             innerState.mTimeLastNotification = now;
421         }
422     }
423
424     /**
425      * Write a data buffer to the destination file.
426      * @param data buffer containing the data to write
427      * @param bytesRead how many bytes to write from the buffer
428      */
429     private void writeDataToDestination(State state, byte[] data, int bytesRead)
430             throws StopRequest {
431         for (;;) {
432             try {
433                 if (state.mStream == null) {
434                     state.mStream = new FileOutputStream(state.mFilename, true);
435                 }
436                 state.mStream.write(data, 0, bytesRead);
437                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
438                             && !isDrmFile(state)) {
439                     closeDestination(state);
440                 }
441                 return;
442             } catch (IOException ex) {
443                 if (mInfo.isOnCache()) {
444                     if (Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
445                         continue;
446                     }
447                 } else if (!Helpers.isExternalMediaMounted()) {
448                     throw new StopRequest(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
449                 }
450
451                 long availableBytes =
452                     Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
453                 if (availableBytes < bytesRead) {
454                     throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR, ex);
455                 }
456                 throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR, ex);
457             }
458         }
459     }
460
461     /**
462      * Called when we've reached the end of the HTTP response stream, to update the database and
463      * check for consistency.
464      */
465     private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
466         ContentValues values = new ContentValues();
467         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
468         if (innerState.mHeaderContentLength == null) {
469             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
470         }
471         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
472
473         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
474                 && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
475         if (lengthMismatched) {
476             if (cannotResume(innerState)) {
477                 if (Constants.LOGV) {
478                     Log.d(Constants.TAG, "mismatched content length " +
479                             mInfo.mUri);
480                 } else if (Config.LOGD) {
481                     Log.d(Constants.TAG, "mismatched content length for " +
482                             mInfo.mId);
483                 }
484                 throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME);
485             } else {
486                 throw new StopRequest(handleHttpError(state, "closed socket"));
487             }
488         }
489     }
490
491     private boolean cannotResume(InnerState innerState) {
492         return innerState.mBytesSoFar > 0 && !mInfo.mNoIntegrity && innerState.mHeaderETag == null;
493     }
494
495     /**
496      * Read some data from the HTTP response stream, handling I/O errors.
497      * @param data buffer to use to read data
498      * @param entityStream stream for reading the HTTP response entity
499      * @return the number of bytes actually read or -1 if the end of the stream has been reached
500      */
501     private int readFromResponse(State state, InnerState innerState, byte[] data,
502                                  InputStream entityStream) throws StopRequest {
503         try {
504             return entityStream.read(data);
505         } catch (IOException ex) {
506             logNetworkState();
507             ContentValues values = new ContentValues();
508             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
509             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
510             if (cannotResume(innerState)) {
511                 Log.d(Constants.TAG, "download IOException for download " + mInfo.mId, ex);
512                 Log.d(Constants.TAG, "can't resume interrupted download with no ETag");
513                 throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME, ex);
514             } else {
515                 throw new StopRequest(handleHttpError(state, "download IOException"), ex);
516             }
517         }
518     }
519
520     /**
521      * Open a stream for the HTTP response entity, handling I/O errors.
522      * @return an InputStream to read the response entity
523      */
524     private InputStream openResponseEntity(State state, HttpResponse response)
525             throws StopRequest {
526         try {
527             return response.getEntity().getContent();
528         } catch (IOException ex) {
529             logNetworkState();
530             throw new StopRequest(handleHttpError(state, "IOException getting entity"), ex);
531         }
532     }
533
534     private void logNetworkState() {
535         if (Constants.LOGX) {
536             Log.i(Constants.TAG,
537                     "Net " + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up" : "Down"));
538         }
539     }
540
541     /**
542      * Read HTTP response headers and take appropriate action, including setting up the destination
543      * file and updating the database.
544      */
545     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
546             throws StopRequest, FileNotFoundException {
547         if (innerState.mContinuingDownload) {
548             // ignore response headers on resume requests
549             return;
550         }
551
552         readResponseHeaders(state, innerState, response);
553
554         DownloadFileInfo fileInfo = Helpers.generateSaveFile(
555                 mContext,
556                 mInfo.mUri,
557                 mInfo.mHint,
558                 innerState.mHeaderContentDisposition,
559                 innerState.mHeaderContentLocation,
560                 state.mMimeType,
561                 mInfo.mDestination,
562                 (innerState.mHeaderContentLength != null) ?
563                         Long.parseLong(innerState.mHeaderContentLength) : 0,
564                 mInfo.mIsPublicApi);
565         if (fileInfo.mFileName == null) {
566             throw new StopRequest(fileInfo.mStatus);
567         }
568         state.mFilename = fileInfo.mFileName;
569         state.mStream = fileInfo.mStream;
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             Log.d(Constants.TAG, "can't know size of download, giving up");
652             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR);
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         if (Constants.LOGV) {
681             Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
682         } else if (Config.LOGD) {
683             Log.d(Constants.TAG, "http error " + statusCode + " for download " +
684                     mInfo.mId);
685         }
686         int finalStatus;
687         if (Downloads.Impl.isStatusError(statusCode)) {
688             finalStatus = statusCode;
689         } else if (statusCode >= 300 && statusCode < 400) {
690             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
691         } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
692             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
693         } else {
694             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
695         }
696         throw new StopRequest(finalStatus);
697     }
698
699     /**
700      * Handle a 3xx redirect status.
701      */
702     private void handleRedirect(State state, HttpResponse response, int statusCode)
703             throws StopRequest, RetryDownload {
704         if (Constants.LOGVV) {
705             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
706         }
707         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
708             if (Constants.LOGV) {
709                 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
710                         " at " + mInfo.mUri);
711             } else if (Config.LOGD) {
712                 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
713             }
714             throw new StopRequest(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS);
715         }
716         Header header = response.getFirstHeader("Location");
717         if (header == null) {
718             return;
719         }
720         if (Constants.LOGVV) {
721             Log.v(Constants.TAG, "Location :" + header.getValue());
722         }
723
724         String newUri;
725         try {
726             newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
727         } catch(URISyntaxException ex) {
728             if (Constants.LOGV) {
729                 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
730                         + " for " + mInfo.mUri);
731             } else if (Config.LOGD) {
732                 Log.d(Constants.TAG,
733                         "Couldn't resolve redirect URI for download " +
734                         mInfo.mId);
735             }
736             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR);
737         }
738         ++state.mRedirectCount;
739         state.mRequestUri = newUri;
740         if (statusCode == 301 || statusCode == 303) {
741             // use the new URI for all future requests (should a retry/resume be necessary)
742             state.mNewUri = newUri;
743         }
744         throw new RetryDownload();
745     }
746
747     /**
748      * Handle a 503 Service Unavailable status by processing the Retry-After header.
749      */
750     private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
751         if (Constants.LOGVV) {
752             Log.v(Constants.TAG, "got HTTP response code 503");
753         }
754         state.mCountRetry = true;
755         Header header = response.getFirstHeader("Retry-After");
756         if (header != null) {
757            try {
758                if (Constants.LOGVV) {
759                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
760                }
761                state.mRetryAfter = Integer.parseInt(header.getValue());
762                if (state.mRetryAfter < 0) {
763                    state.mRetryAfter = 0;
764                } else {
765                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
766                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
767                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
768                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
769                    }
770                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
771                    state.mRetryAfter *= 1000;
772                }
773            } catch (NumberFormatException ex) {
774                // ignored - retryAfter stays 0 in this case.
775            }
776         }
777         throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
778     }
779
780     /**
781      * Send the request to the server, handling any I/O exceptions.
782      */
783     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
784             throws StopRequest {
785         try {
786             return client.execute(request);
787         } catch (IllegalArgumentException ex) {
788             if (Constants.LOGV) {
789                 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
790                         mInfo.mUri + " : " + ex);
791             } else if (Config.LOGD) {
792                 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
793                         mInfo.mId + " : " +  ex);
794             }
795             throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR, ex);
796         } catch (IOException ex) {
797             logNetworkState();
798             throw new StopRequest(handleHttpError(state, "IOException trying to execute request"),
799                     ex);
800         }
801     }
802
803     /**
804      * @return the final status for this attempt
805      */
806     private int handleHttpError(State state, String message) {
807         if (Constants.LOGV) {
808             Log.d(Constants.TAG, message + " for " + mInfo.mUri);
809         }
810
811         if (!Helpers.isNetworkAvailable(mSystemFacade)) {
812             return Downloads.Impl.STATUS_RUNNING_PAUSED;
813         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
814             state.mCountRetry = true;
815             return Downloads.Impl.STATUS_RUNNING_PAUSED;
816         } else {
817             Log.d(Constants.TAG, "reached max retries: " + message + " for " + mInfo.mId);
818             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
819         }
820     }
821
822     /**
823      * Prepare the destination file to receive data.  If the file already exists, we'll set up
824      * appropriately for resumption.
825      */
826     private void setupDestinationFile(State state, InnerState innerState)
827             throws StopRequest, FileNotFoundException {
828         if (state.mFilename != null) { // only true if we've already run a thread for this download
829             if (!Helpers.isFilenameValid(state.mFilename)) {
830                 throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR);
831             }
832             // We're resuming a download that got interrupted
833             File f = new File(state.mFilename);
834             if (f.exists()) {
835                 long fileLength = f.length();
836                 if (fileLength == 0) {
837                     // The download hadn't actually started, we can restart from scratch
838                     f.delete();
839                     state.mFilename = null;
840                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
841                     // This should've been caught upon failure
842                     Log.wtf(Constants.TAG, "Trying to resume a download that can't be resumed");
843                     f.delete();
844                     throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME);
845                 } else {
846                     // All right, we'll be able to resume this download
847                     state.mStream = new FileOutputStream(state.mFilename, true);
848                     innerState.mBytesSoFar = (int) fileLength;
849                     if (mInfo.mTotalBytes != -1) {
850                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
851                     }
852                     innerState.mHeaderETag = mInfo.mETag;
853                     innerState.mContinuingDownload = true;
854                 }
855             }
856         }
857
858         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
859                 && !isDrmFile(state)) {
860             closeDestination(state);
861         }
862     }
863
864     /**
865      * Add custom headers for this download to the HTTP request.
866      */
867     private void addRequestHeaders(InnerState innerState, HttpGet request) {
868         for (Pair<String, String> header : mInfo.getHeaders()) {
869             request.addHeader(header.first, header.second);
870         }
871
872         if (innerState.mContinuingDownload) {
873             if (innerState.mHeaderETag != null) {
874                 request.addHeader("If-Match", innerState.mHeaderETag);
875             }
876             request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
877         }
878     }
879
880     /**
881      * Stores information about the completed download, and notifies the initiating application.
882      */
883     private void notifyDownloadCompleted(
884             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
885             String filename, String uri, String mimeType) {
886         notifyThroughDatabase(
887                 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
888         if (Downloads.Impl.isStatusCompleted(status)) {
889             mInfo.sendIntentIfRequested();
890         }
891     }
892
893     private void notifyThroughDatabase(
894             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
895             String filename, String uri, String mimeType) {
896         ContentValues values = new ContentValues();
897         values.put(Downloads.Impl.COLUMN_STATUS, status);
898         values.put(Downloads.Impl._DATA, filename);
899         if (uri != null) {
900             values.put(Downloads.Impl.COLUMN_URI, uri);
901         }
902         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
903         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
904         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
905         if (!countRetry) {
906             values.put(Constants.FAILED_CONNECTIONS, 0);
907         } else if (gotData) {
908             values.put(Constants.FAILED_CONNECTIONS, 1);
909         } else {
910             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
911         }
912
913         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
914     }
915
916     /**
917      * Clean up a mimeType string so it can be used to dispatch an intent to
918      * view a downloaded asset.
919      * @param mimeType either null or one or more mime types (semi colon separated).
920      * @return null if mimeType was null. Otherwise a string which represents a
921      * single mimetype in lowercase and with surrounding whitespaces trimmed.
922      */
923     private static String sanitizeMimeType(String mimeType) {
924         try {
925             mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
926
927             final int semicolonIndex = mimeType.indexOf(';');
928             if (semicolonIndex != -1) {
929                 mimeType = mimeType.substring(0, semicolonIndex);
930             }
931             return mimeType;
932         } catch (NullPointerException npe) {
933             return null;
934         }
935     }
936 }