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