resolved conflicts for merge of b5629da7 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
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             }
329         } catch (IOException ex) {
330             if (Constants.LOGV) {
331                 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
332             }
333             // nothing can really be done if the file can't be closed
334         }
335     }
336
337     /**
338      * Check if the download has been paused or canceled, stopping the request appropriately if it
339      * has been.
340      */
341     private void checkPausedOrCanceled(State state) throws StopRequest {
342         synchronized (mInfo) {
343             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
344                 if (Constants.LOGV) {
345                     Log.v(Constants.TAG, "paused " + mInfo.mUri);
346                 }
347                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
348                 throw new StopRequest();
349             }
350         }
351         if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
352             if (Constants.LOGV) {
353                 Log.d(Constants.TAG, "canceled " + mInfo.mUri);
354             }
355             state.mFinalStatus = Downloads.Impl.STATUS_CANCELED;
356             throw new StopRequest();
357         }
358     }
359
360     /**
361      * Report download progress through the database if necessary.
362      */
363     private void reportProgress(State state, InnerState innerState) {
364         long now = mSystemFacade.currentTimeMillis();
365         if (innerState.mBytesSoFar - innerState.mBytesNotified
366                         > Constants.MIN_PROGRESS_STEP
367                 && now - innerState.mTimeLastNotification
368                         > Constants.MIN_PROGRESS_TIME) {
369             ContentValues values = new ContentValues();
370             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
371             mContext.getContentResolver().update(
372                     state.mContentUri, values, null, null);
373             innerState.mBytesNotified = innerState.mBytesSoFar;
374             innerState.mTimeLastNotification = now;
375         }
376     }
377
378     /**
379      * Write a data buffer to the destination file.
380      * @param data buffer containing the data to write
381      * @param bytesRead how many bytes to write from the buffer
382      */
383     private void writeDataToDestination(State state, byte[] data, int bytesRead)
384             throws StopRequest {
385         for (;;) {
386             try {
387                 if (state.mStream == null) {
388                     state.mStream = new FileOutputStream(state.mFilename, true);
389                 }
390                 state.mStream.write(data, 0, bytesRead);
391                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
392                             && !isDrmFile(state)) {
393                     closeDestination(state);
394                 }
395                 return;
396             } catch (IOException ex) {
397                 if (!Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
398                     state.mFinalStatus = Downloads.Impl.STATUS_FILE_ERROR;
399                     throw new StopRequest();
400                 }
401             }
402         }
403     }
404
405     /**
406      * Called when we've reached the end of the HTTP response stream, to update the database and
407      * check for consistency.
408      */
409     private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
410         ContentValues values = new ContentValues();
411         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
412         if (innerState.mHeaderContentLength == null) {
413             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
414         }
415         mContext.getContentResolver().update(state.mContentUri, values, null, null);
416
417         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
418                 && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
419         if (lengthMismatched) {
420             if (!mInfo.mNoIntegrity && innerState.mHeaderETag == null) {
421                 if (Constants.LOGV) {
422                     Log.d(Constants.TAG, "mismatched content length " +
423                             mInfo.mUri);
424                 } else if (Config.LOGD) {
425                     Log.d(Constants.TAG, "mismatched content length for " +
426                             mInfo.mId);
427                 }
428                 state.mFinalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
429             } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
430                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
431             } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
432                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
433                 state.mCountRetry = true;
434             } else {
435                 if (Constants.LOGV) {
436                     Log.v(Constants.TAG, "closed socket for " + mInfo.mUri);
437                 } else if (Config.LOGD) {
438                     Log.d(Constants.TAG, "closed socket for download " +
439                             mInfo.mId);
440                 }
441                 state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
442             }
443             throw new StopRequest();
444         }
445     }
446
447     /**
448      * Read some data from the HTTP response stream, handling I/O errors.
449      * @param data buffer to use to read data
450      * @param entityStream stream for reading the HTTP response entity
451      * @return the number of bytes actually read or -1 if the end of the stream has been reached
452      */
453     private int readFromResponse(State state, InnerState innerState, byte[] data,
454                                  InputStream entityStream) throws StopRequest {
455         try {
456             return entityStream.read(data);
457         } catch (IOException ex) {
458             if (Constants.LOGX) {
459                 if (Helpers.isNetworkAvailable(mSystemFacade)) {
460                     Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Up");
461                 } else {
462                     Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Down");
463                 }
464             }
465             ContentValues values = new ContentValues();
466             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
467             mContext.getContentResolver().update(state.mContentUri, values, null, null);
468             if (!mInfo.mNoIntegrity && innerState.mHeaderETag == null) {
469                 Log.d(Constants.TAG, "download IOException for download " + mInfo.mId + " : " + ex);
470                 Log.d(Constants.TAG, "can't resume interrupted download with no ETag");
471                 state.mFinalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
472             } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
473                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
474             } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
475                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
476                 state.mCountRetry = true;
477             } else {
478                 Log.d(Constants.TAG, "download IOException for download " + mInfo.mId + " : " + ex);
479                 state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
480             }
481             throw new StopRequest();
482         }
483     }
484
485     /**
486      * Open a stream for the HTTP response entity, handling I/O errors.
487      * @return an InputStream to read the response entity
488      */
489     private InputStream openResponseEntity(State state, HttpResponse response)
490             throws StopRequest {
491         try {
492             return response.getEntity().getContent();
493         } catch (IOException ex) {
494             if (Constants.LOGX) {
495                 if (Helpers.isNetworkAvailable(mSystemFacade)) {
496                     Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Up");
497                 } else {
498                     Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Down");
499                 }
500             }
501             if (!Helpers.isNetworkAvailable(mSystemFacade)) {
502                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
503             } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
504                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
505                 state.mCountRetry = true;
506             } else {
507                 if (Constants.LOGV) {
508                     Log.d(Constants.TAG,
509                             "IOException getting entity for " +
510                             mInfo.mUri +
511                             " : " +
512                             ex);
513                 } else if (Config.LOGD) {
514                     Log.d(Constants.TAG, "IOException getting entity for download " +
515                             mInfo.mId + " : " + ex);
516                 }
517                 state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
518             }
519             throw new StopRequest();
520         }
521     }
522
523     /**
524      * Read HTTP response headers and take appropriate action, including setting up the destination
525      * file and updating the database.
526      */
527     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
528             throws StopRequest, FileNotFoundException {
529         if (innerState.mContinuingDownload) {
530             // ignore response headers on resume requests
531             return;
532         }
533
534         readResponseHeaders(state, innerState, response);
535
536         DownloadFileInfo fileInfo = Helpers.generateSaveFile(
537                 mContext,
538                 mInfo.mUri,
539                 mInfo.mHint,
540                 innerState.mHeaderContentDisposition,
541                 innerState.mHeaderContentLocation,
542                 state.mMimeType,
543                 mInfo.mDestination,
544                 (innerState.mHeaderContentLength != null) ?
545                         Long.parseLong(innerState.mHeaderContentLength) : 0);
546         if (fileInfo.mFileName == null) {
547             state.mFinalStatus = fileInfo.mStatus;
548             throw new StopRequest();
549         }
550         state.mFilename = fileInfo.mFileName;
551         state.mStream = fileInfo.mStream;
552         if (Constants.LOGV) {
553             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
554         }
555
556         updateDatabaseFromHeaders(state, innerState);
557
558         // check connectivity again now that we know the total size
559         if (!mInfo.canUseNetwork()) {
560             state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
561             throw new StopRequest();
562         }
563     }
564
565     /**
566      * Update necessary database fields based on values of HTTP response headers that have been
567      * read.
568      */
569     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
570         ContentValues values = new ContentValues();
571         values.put(Downloads.Impl._DATA, state.mFilename);
572         if (innerState.mHeaderETag != null) {
573             values.put(Constants.ETAG, innerState.mHeaderETag);
574         }
575         if (state.mMimeType != null) {
576             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
577         }
578         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
579         mContext.getContentResolver().update(state.mContentUri, values, null, null);
580     }
581
582     /**
583      * Read headers from the HTTP response and store them into local state.
584      */
585     private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
586             throws StopRequest {
587         Header header = response.getFirstHeader("Content-Disposition");
588         if (header != null) {
589             innerState.mHeaderContentDisposition = header.getValue();
590         }
591         header = response.getFirstHeader("Content-Location");
592         if (header != null) {
593             innerState.mHeaderContentLocation = header.getValue();
594         }
595         if (state.mMimeType == null) {
596             header = response.getFirstHeader("Content-Type");
597             if (header != null) {
598                 state.mMimeType = sanitizeMimeType(header.getValue());
599             }
600         }
601         header = response.getFirstHeader("ETag");
602         if (header != null) {
603             innerState.mHeaderETag = header.getValue();
604         }
605         String headerTransferEncoding = null;
606         header = response.getFirstHeader("Transfer-Encoding");
607         if (header != null) {
608             headerTransferEncoding = header.getValue();
609         }
610         if (headerTransferEncoding == null) {
611             header = response.getFirstHeader("Content-Length");
612             if (header != null) {
613                 innerState.mHeaderContentLength = header.getValue();
614                 mInfo.mTotalBytes = Long.parseLong(innerState.mHeaderContentLength);
615             }
616         } else {
617             // Ignore content-length with transfer-encoding - 2616 4.4 3
618             if (Constants.LOGVV) {
619                 Log.v(Constants.TAG,
620                         "ignoring content-length because of xfer-encoding");
621             }
622         }
623         if (Constants.LOGVV) {
624             Log.v(Constants.TAG, "Content-Disposition: " +
625                     innerState.mHeaderContentDisposition);
626             Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
627             Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
628             Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
629             Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
630             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
631         }
632
633         boolean noSizeInfo = innerState.mHeaderContentLength == null
634                 && (headerTransferEncoding == null
635                     || !headerTransferEncoding.equalsIgnoreCase("chunked"));
636         if (!mInfo.mNoIntegrity && noSizeInfo) {
637             Log.d(Constants.TAG, "can't know size of download, giving up");
638             state.mFinalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
639             throw new StopRequest();
640         }
641     }
642
643     /**
644      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
645      */
646     private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
647             throws StopRequest {
648         int statusCode = response.getStatusLine().getStatusCode();
649         if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
650             handleServiceUnavailable(state, response);
651         }
652         if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
653             handleRedirect(state, response, statusCode);
654         }
655
656         int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
657         if (statusCode != expectedStatus) {
658             handleOtherStatus(state, innerState, statusCode);
659         }
660     }
661
662     /**
663      * Handle a status that we don't know how to deal with properly.
664      */
665     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
666             throws StopRequest {
667         if (Constants.LOGV) {
668             Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
669         } else if (Config.LOGD) {
670             Log.d(Constants.TAG, "http error " + statusCode + " for download " +
671                     mInfo.mId);
672         }
673         if (Downloads.Impl.isStatusError(statusCode)) {
674             state.mFinalStatus = statusCode;
675         } else if (statusCode >= 300 && statusCode < 400) {
676             state.mFinalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
677         } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
678             state.mFinalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
679         } else {
680             state.mFinalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
681         }
682         throw new StopRequest();
683     }
684
685     /**
686      * Handle a 3xx redirect status.
687      */
688     private void handleRedirect(State state, HttpResponse response, int statusCode)
689             throws StopRequest {
690         if (Constants.LOGVV) {
691             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
692         }
693         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
694             if (Constants.LOGV) {
695                 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
696                         " at " + mInfo.mUri);
697             } else if (Config.LOGD) {
698                 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
699             }
700             state.mFinalStatus = Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
701             throw new StopRequest();
702         }
703         Header header = response.getFirstHeader("Location");
704         if (header != null) {
705             if (Constants.LOGVV) {
706                 Log.v(Constants.TAG, "Location :" + header.getValue());
707             }
708             try {
709                 state.mNewUri = new URI(mInfo.mUri).
710                         resolve(new URI(header.getValue())).
711                         toString();
712             } catch(URISyntaxException ex) {
713                 if (Constants.LOGV) {
714                     Log.d(Constants.TAG,
715                             "Couldn't resolve redirect URI " +
716                             header.getValue() +
717                             " for " +
718                             mInfo.mUri);
719                 } else if (Config.LOGD) {
720                     Log.d(Constants.TAG,
721                             "Couldn't resolve redirect URI for download " +
722                             mInfo.mId);
723                 }
724                 state.mFinalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
725                 throw new StopRequest();
726             }
727             ++state.mRedirectCount;
728             state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
729             throw new StopRequest();
730         }
731     }
732
733     /**
734      * Handle a 503 Service Unavailable status by processing the Retry-After header.
735      */
736     private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
737         if (Constants.LOGVV) {
738             Log.v(Constants.TAG, "got HTTP response code 503");
739         }
740         state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
741         state.mCountRetry = true;
742         Header header = response.getFirstHeader("Retry-After");
743         if (header != null) {
744            try {
745                if (Constants.LOGVV) {
746                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
747                }
748                state.mRetryAfter = Integer.parseInt(header.getValue());
749                if (state.mRetryAfter < 0) {
750                    state.mRetryAfter = 0;
751                } else {
752                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
753                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
754                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
755                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
756                    }
757                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
758                    state.mRetryAfter *= 1000;
759                }
760            } catch (NumberFormatException ex) {
761                // ignored - retryAfter stays 0 in this case.
762            }
763         }
764         throw new StopRequest();
765     }
766
767     /**
768      * Send the request to the server, handling any I/O exceptions.
769      */
770     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
771             throws StopRequest {
772         try {
773             return client.execute(request);
774         } catch (IllegalArgumentException ex) {
775             if (Constants.LOGV) {
776                 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
777                         mInfo.mUri + " : " + ex);
778             } else if (Config.LOGD) {
779                 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
780                         mInfo.mId + " : " +  ex);
781             }
782             state.mFinalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
783             throw new StopRequest();
784         } catch (IOException ex) {
785             if (Constants.LOGX) {
786                 if (Helpers.isNetworkAvailable(mSystemFacade)) {
787                     Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Up");
788                 } else {
789                     Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Down");
790                 }
791             }
792             if (!Helpers.isNetworkAvailable(mSystemFacade)) {
793                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
794             } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
795                 state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
796                 state.mCountRetry = true;
797             } else {
798                 if (Constants.LOGV) {
799                     Log.d(Constants.TAG, "IOException trying to execute request for " +
800                             mInfo.mUri + " : " + ex);
801                 } else if (Config.LOGD) {
802                     Log.d(Constants.TAG, "IOException trying to execute request for " +
803                             mInfo.mId + " : " + ex);
804                 }
805                 state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
806             }
807             throw new StopRequest();
808         }
809     }
810
811     /**
812      * Prepare the destination file to receive data.  If the file already exists, we'll set up
813      * appropriately for resumption.
814      */
815     private void setupDestinationFile(State state, InnerState innerState)
816             throws StopRequest, FileNotFoundException {
817         state.mFilename = mInfo.mFileName;
818         if (state.mFilename != null) {
819             if (!Helpers.isFilenameValid(state.mFilename)) {
820                 state.mFinalStatus = Downloads.Impl.STATUS_FILE_ERROR;
821                 throw new StopRequest();
822             }
823             // We're resuming a download that got interrupted
824             File f = new File(state.mFilename);
825             if (f.exists()) {
826                 long fileLength = f.length();
827                 if (fileLength == 0) {
828                     // The download hadn't actually started, we can restart from scratch
829                     f.delete();
830                     state.mFilename = null;
831                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
832                     // Tough luck, that's not a resumable download
833                     if (Config.LOGD) {
834                         Log.d(Constants.TAG,
835                                 "can't resume interrupted non-resumable download");
836                     }
837                     f.delete();
838                     state.mFinalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
839                     throw new StopRequest();
840                 } else {
841                     // All right, we'll be able to resume this download
842                     state.mStream = new FileOutputStream(state.mFilename, true);
843                     innerState.mBytesSoFar = (int) fileLength;
844                     if (mInfo.mTotalBytes != -1) {
845                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
846                     }
847                     innerState.mHeaderETag = mInfo.mETag;
848                     innerState.mContinuingDownload = true;
849                 }
850             }
851         }
852
853         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
854                 && !isDrmFile(state)) {
855             closeDestination(state);
856         }
857     }
858
859     /**
860      * Add custom headers for this download to the HTTP request.
861      */
862     private void addRequestHeaders(InnerState innerState, HttpGet request) {
863         for (Map.Entry<String, String> header : mInfo.getHeaders().entrySet()) {
864             request.addHeader(header.getKey(), header.getValue());
865         }
866
867         if (innerState.mContinuingDownload) {
868             if (innerState.mHeaderETag != null) {
869                 request.addHeader("If-Match", innerState.mHeaderETag);
870             }
871             request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
872         }
873     }
874
875     /**
876      * Stores information about the completed download, and notifies the initiating application.
877      */
878     private void notifyDownloadCompleted(
879             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
880             String filename, String uri, String mimeType) {
881         notifyThroughDatabase(
882                 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
883         if (Downloads.Impl.isStatusCompleted(status)) {
884             notifyThroughIntent();
885         }
886     }
887
888     private void notifyThroughDatabase(
889             int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
890             String filename, String uri, String mimeType) {
891         ContentValues values = new ContentValues();
892         values.put(Downloads.Impl.COLUMN_STATUS, status);
893         values.put(Downloads.Impl._DATA, filename);
894         if (uri != null) {
895             values.put(Downloads.Impl.COLUMN_URI, uri);
896         }
897         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
898         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
899         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
900         if (!countRetry) {
901             values.put(Constants.FAILED_CONNECTIONS, 0);
902         } else if (gotData) {
903             values.put(Constants.FAILED_CONNECTIONS, 1);
904         } else {
905             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
906         }
907
908         mContext.getContentResolver().update(ContentUris.withAppendedId(
909                 Downloads.Impl.CONTENT_URI, mInfo.mId), values, null, null);
910     }
911
912     /**
913      * Notifies the initiating app if it requested it. That way, it can know that the
914      * download completed even if it's not actively watching the cursor.
915      */
916     private void notifyThroughIntent() {
917         Uri uri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
918         mInfo.sendIntentIfRequested(uri);
919     }
920
921     /**
922      * Clean up a mimeType string so it can be used to dispatch an intent to
923      * view a downloaded asset.
924      * @param mimeType either null or one or more mime types (semi colon separated).
925      * @return null if mimeType was null. Otherwise a string which represents a
926      * single mimetype in lowercase and with surrounding whitespaces trimmed.
927      */
928     private static String sanitizeMimeType(String mimeType) {
929         try {
930             mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
931
932             final int semicolonIndex = mimeType.indexOf(';');
933             if (semicolonIndex != -1) {
934                 mimeType = mimeType.substring(0, semicolonIndex);
935             }
936             return mimeType;
937         } catch (NullPointerException npe) {
938             return null;
939         }
940     }
941 }