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