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