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