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