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