Merge "Download provider change for DRM Forward Lock plugin: to convert .dm files...
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadThread.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads;
18
19 import org.apache.http.conn.params.ConnRouteParams;
20
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.net.http.AndroidHttpClient;
25 import android.net.Proxy;
26 import android.net.TrafficStats;
27 import android.os.FileUtils;
28 import android.os.PowerManager;
29 import android.os.Process;
30 import android.provider.Downloads;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.util.Pair;
34
35 import org.apache.http.Header;
36 import org.apache.http.HttpResponse;
37 import org.apache.http.client.methods.HttpGet;
38
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.SyncFailedException;
45 import java.net.URI;
46 import java.net.URISyntaxException;
47 import java.util.Locale;
48
49 /**
50  * Runs an actual download
51  */
52 public class DownloadThread extends Thread {
53
54     private final Context mContext;
55     private final DownloadInfo mInfo;
56     private final SystemFacade mSystemFacade;
57     private final StorageManager mStorageManager;
58     private DrmConvertSession mDrmConvertSession;
59
60     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
61             StorageManager storageManager) {
62         mContext = context;
63         mSystemFacade = systemFacade;
64         mInfo = info;
65         mStorageManager = storageManager;
66     }
67
68     /**
69      * Returns the user agent provided by the initiating app, or use the default one
70      */
71     private String userAgent() {
72         String userAgent = mInfo.mUserAgent;
73         if (userAgent != null) {
74         }
75         if (userAgent == null) {
76             userAgent = Constants.DEFAULT_USER_AGENT;
77         }
78         return userAgent;
79     }
80
81     /**
82      * State for the entire run() method.
83      */
84     static class State {
85         public String mFilename;
86         public FileOutputStream mStream;
87         public String mMimeType;
88         public boolean mCountRetry = false;
89         public int mRetryAfter = 0;
90         public int mRedirectCount = 0;
91         public String mNewUri;
92         public boolean mGotData = false;
93         public String mRequestUri;
94
95         public State(DownloadInfo info) {
96             mMimeType = sanitizeMimeType(info.mMimeType);
97             mRequestUri = info.mUri;
98             mFilename = info.mFileName;
99         }
100     }
101
102     /**
103      * State within executeDownload()
104      */
105     private static class InnerState {
106         public int mBytesSoFar = 0;
107         public String mHeaderETag;
108         public boolean mContinuingDownload = false;
109         public String mHeaderContentLength;
110         public String mHeaderContentDisposition;
111         public String mHeaderContentLocation;
112         public int mBytesNotified = 0;
113         public long mTimeLastNotification = 0;
114         public long mTotalBytes = -1;
115     }
116
117     /**
118      * Raised from methods called by executeDownload() to indicate that the download should be
119      * retried immediately.
120      */
121     private class RetryDownload extends Throwable {}
122
123     /**
124      * Executes the download in a separate thread
125      */
126     @Override
127     public void run() {
128         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
129
130         State state = new State(mInfo);
131         AndroidHttpClient client = null;
132         PowerManager.WakeLock wakeLock = null;
133         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
134         String errorMsg = null;
135
136         try {
137             PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
138             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
139             wakeLock.acquire();
140
141             if (Constants.LOGV) {
142                 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
143             }
144
145             client = AndroidHttpClient.newInstance(userAgent(), mContext);
146
147             // network traffic on this thread should be counted against the
148             // requesting uid, and is tagged with well-known value.
149             TrafficStats.setThreadStatsTag("android:DownloadManager");
150             TrafficStats.setThreadStatsUid(mInfo.mUid);
151
152             boolean finished = false;
153             while(!finished) {
154                 Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
155                 // Set or unset proxy, which may have changed since last GET request.
156                 // setDefaultProxy() supports null as proxy parameter.
157                 ConnRouteParams.setDefaultProxy(client.getParams(),
158                         Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
159                 HttpGet request = new HttpGet(state.mRequestUri);
160                 try {
161                     executeDownload(state, client, request);
162                     finished = true;
163                 } catch (RetryDownload exc) {
164                     // fall through
165                 } finally {
166                     request.abort();
167                     request = null;
168                 }
169             }
170
171             if (Constants.LOGV) {
172                 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
173             }
174             finalizeDestinationFile(state);
175             finalStatus = Downloads.Impl.STATUS_SUCCESS;
176         } catch (StopRequestException error) {
177             // remove the cause before printing, in case it contains PII
178             errorMsg = error.getMessage();
179             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
180             Log.w(Constants.TAG, msg);
181             if (Constants.LOGV) {
182                 Log.w(Constants.TAG, msg, error);
183             }
184             finalStatus = error.mFinalStatus;
185             // fall through to finally block
186         } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
187             errorMsg = ex.getMessage();
188             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
189             Log.w(Constants.TAG, msg, ex);
190             finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
191             // falls through to the code that reports an error
192         } finally {
193             TrafficStats.clearThreadStatsTag();
194             TrafficStats.clearThreadStatsUid();
195
196             if (client != null) {
197                 client.close();
198                 client = null;
199             }
200             cleanupDestination(state, finalStatus);
201             notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
202                                     state.mGotData, state.mFilename,
203                                     state.mNewUri, state.mMimeType, errorMsg);
204             DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
205
206             if (wakeLock != null) {
207                 wakeLock.release();
208                 wakeLock = null;
209             }
210         }
211         mStorageManager.incrementNumDownloadsSoFar();
212     }
213
214     /**
215      * Fully execute a single download request - setup and send the request, handle the response,
216      * and transfer the data to the destination file.
217      */
218     private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
219             throws StopRequestException, RetryDownload {
220         InnerState innerState = new InnerState();
221         byte data[] = new byte[Constants.BUFFER_SIZE];
222
223         setupDestinationFile(state, innerState);
224         addRequestHeaders(innerState, request);
225
226         // check just before sending the request to avoid using an invalid connection at all
227         checkConnectivity();
228
229         HttpResponse response = sendRequest(state, client, request);
230         handleExceptionalStatus(state, innerState, response);
231
232         if (Constants.LOGV) {
233             Log.v(Constants.TAG, "received response for " + mInfo.mUri);
234         }
235
236         processResponseHeaders(state, innerState, response);
237         InputStream entityStream = openResponseEntity(state, response);
238         transferData(state, innerState, data, entityStream);
239     }
240
241     /**
242      * Check if current connectivity is valid for this request.
243      */
244     private void checkConnectivity() throws StopRequestException {
245         int networkUsable = mInfo.checkCanUseNetwork();
246         if (networkUsable != DownloadInfo.NETWORK_OK) {
247             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
248             if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
249                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
250                 mInfo.notifyPauseDueToSize(true);
251             } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
252                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
253                 mInfo.notifyPauseDueToSize(false);
254             }
255             throw new StopRequestException(status,
256                     mInfo.getLogMessageForNetworkError(networkUsable));
257         }
258     }
259
260     /**
261      * Transfer as much data as possible from the HTTP response to the destination file.
262      * @param data buffer to use to read data
263      * @param entityStream stream for reading the HTTP response entity
264      */
265     private void transferData(State state, InnerState innerState, byte[] data,
266                                  InputStream entityStream) throws StopRequestException {
267         for (;;) {
268             int bytesRead = readFromResponse(state, innerState, data, entityStream);
269             if (bytesRead == -1) { // success, end of stream already reached
270                 handleEndOfStream(state, innerState);
271                 return;
272             }
273
274             state.mGotData = true;
275             writeDataToDestination(state, data, bytesRead);
276             innerState.mBytesSoFar += bytesRead;
277             reportProgress(state, innerState);
278
279             if (Constants.LOGVV) {
280                 Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar + " for "
281                       + mInfo.mUri);
282             }
283
284             checkPausedOrCanceled(state);
285         }
286     }
287
288     /**
289      * Called after a successful completion to take any necessary action on the downloaded file.
290      */
291     private void finalizeDestinationFile(State state) throws StopRequestException {
292         // make sure the file is readable
293         FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
294         syncDestination(state);
295     }
296
297     /**
298      * Called just before the thread finishes, regardless of status, to take any necessary action on
299      * the downloaded file.
300      */
301     private void cleanupDestination(State state, int finalStatus) {
302         if (mDrmConvertSession != null) {
303             finalStatus = mDrmConvertSession.close(state.mFilename);
304         }
305
306         closeDestination(state);
307         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
308             new File(state.mFilename).delete();
309             state.mFilename = null;
310         }
311     }
312
313     /**
314      * Sync the destination file to storage.
315      */
316     private void syncDestination(State state) {
317         FileOutputStream downloadedFileStream = null;
318         try {
319             downloadedFileStream = new FileOutputStream(state.mFilename, true);
320             downloadedFileStream.getFD().sync();
321         } catch (FileNotFoundException ex) {
322             Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
323         } catch (SyncFailedException ex) {
324             Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
325         } catch (IOException ex) {
326             Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
327         } catch (RuntimeException ex) {
328             Log.w(Constants.TAG, "exception while syncing file: ", ex);
329         } finally {
330             if(downloadedFileStream != null) {
331                 try {
332                     downloadedFileStream.close();
333                 } catch (IOException ex) {
334                     Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
335                 } catch (RuntimeException ex) {
336                     Log.w(Constants.TAG, "exception while closing file: ", ex);
337                 }
338             }
339         }
340     }
341
342     /**
343      * Close the destination output stream.
344      */
345     private void closeDestination(State state) {
346         try {
347             // close the file
348             if (state.mStream != null) {
349                 state.mStream.close();
350                 state.mStream = null;
351             }
352         } catch (IOException ex) {
353             if (Constants.LOGV) {
354                 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
355             }
356             // nothing can really be done if the file can't be closed
357         }
358     }
359
360     /**
361      * Check if the download has been paused or canceled, stopping the request appropriately if it
362      * has been.
363      */
364     private void checkPausedOrCanceled(State state) throws StopRequestException {
365         synchronized (mInfo) {
366             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
367                 throw new StopRequestException(Downloads.Impl.STATUS_PAUSED_BY_APP,
368                         "download paused by owner");
369             }
370         }
371         if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
372             throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
373         }
374     }
375
376     /**
377      * Report download progress through the database if necessary.
378      */
379     private void reportProgress(State state, InnerState innerState) {
380         long now = mSystemFacade.currentTimeMillis();
381         if (innerState.mBytesSoFar - innerState.mBytesNotified
382                         > Constants.MIN_PROGRESS_STEP
383                 && now - innerState.mTimeLastNotification
384                         > Constants.MIN_PROGRESS_TIME) {
385             ContentValues values = new ContentValues();
386             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
387             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
388             innerState.mBytesNotified = innerState.mBytesSoFar;
389             innerState.mTimeLastNotification = now;
390         }
391     }
392
393     /**
394      * Write a data buffer to the destination file.
395      * @param data buffer containing the data to write
396      * @param bytesRead how many bytes to write from the buffer
397      */
398     private void writeDataToDestination(State state, byte[] data, int bytesRead)
399             throws StopRequestException {
400         for (;;) {
401             try {
402                 if (state.mStream == null) {
403                     state.mStream = new FileOutputStream(state.mFilename, true);
404                 }
405                 mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename,
406                         bytesRead);
407                 if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) {
408                     state.mStream.write(data, 0, bytesRead);
409                 } else {
410                     byte[] convertedData = mDrmConvertSession.convert(data, bytesRead);
411                     if (convertedData != null) {
412                         state.mStream.write(convertedData, 0, convertedData.length);
413                     } else {
414                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
415                                 "Error converting drm data.");
416                     }
417                 }
418                 return;
419             } catch (IOException ex) {
420                 // couldn't write to file. are we out of space? check.
421                 // TODO this check should only be done once. why is this being done 
422                 // in a while(true) loop (see the enclosing statement: for(;;)
423                 if (state.mStream != null) {
424                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
425                 }
426             } finally {
427                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
428                     closeDestination(state);
429                 }
430             }
431         }
432     }
433
434     /**
435      * Called when we've reached the end of the HTTP response stream, to update the database and
436      * check for consistency.
437      */
438     private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
439         ContentValues values = new ContentValues();
440         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
441         if (innerState.mHeaderContentLength == null) {
442             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
443         }
444         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
445
446         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
447                 && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
448         if (lengthMismatched) {
449             if (cannotResume(innerState)) {
450                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
451                         "mismatched content length");
452             } else {
453                 throw new StopRequestException(getFinalStatusForHttpError(state),
454                         "closed socket before end of file");
455             }
456         }
457     }
458
459     private boolean cannotResume(InnerState innerState) {
460         return innerState.mBytesSoFar > 0 && !mInfo.mNoIntegrity && innerState.mHeaderETag == null;
461     }
462
463     /**
464      * Read some data from the HTTP response stream, handling I/O errors.
465      * @param data buffer to use to read data
466      * @param entityStream stream for reading the HTTP response entity
467      * @return the number of bytes actually read or -1 if the end of the stream has been reached
468      */
469     private int readFromResponse(State state, InnerState innerState, byte[] data,
470                                  InputStream entityStream) throws StopRequestException {
471         try {
472             return entityStream.read(data);
473         } catch (IOException ex) {
474             logNetworkState();
475             ContentValues values = new ContentValues();
476             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
477             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
478             if (cannotResume(innerState)) {
479                 String message = "while reading response: " + ex.toString()
480                 + ", can't resume interrupted download with no ETag";
481                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
482                         message, ex);
483             } else {
484                 throw new StopRequestException(getFinalStatusForHttpError(state),
485                         "while reading response: " + ex.toString(), ex);
486             }
487         }
488     }
489
490     /**
491      * Open a stream for the HTTP response entity, handling I/O errors.
492      * @return an InputStream to read the response entity
493      */
494     private InputStream openResponseEntity(State state, HttpResponse response)
495             throws StopRequestException {
496         try {
497             return response.getEntity().getContent();
498         } catch (IOException ex) {
499             logNetworkState();
500             throw new StopRequestException(getFinalStatusForHttpError(state),
501                     "while getting entity: " + ex.toString(), ex);
502         }
503     }
504
505     private void logNetworkState() {
506         if (Constants.LOGX) {
507             Log.i(Constants.TAG,
508                     "Net " + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up" : "Down"));
509         }
510     }
511
512     /**
513      * Read HTTP response headers and take appropriate action, including setting up the destination
514      * file and updating the database.
515      */
516     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
517             throws StopRequestException {
518         if (innerState.mContinuingDownload) {
519             // ignore response headers on resume requests
520             return;
521         }
522
523         readResponseHeaders(state, innerState, response);
524         if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
525             mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType);
526             if (mDrmConvertSession == null) {
527                 throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype "
528                         + state.mMimeType + " can not be converted.");
529             }
530         }
531
532         state.mFilename = Helpers.generateSaveFile(
533                 mContext,
534                 mInfo.mUri,
535                 mInfo.mHint,
536                 innerState.mHeaderContentDisposition,
537                 innerState.mHeaderContentLocation,
538                 state.mMimeType,
539                 mInfo.mDestination,
540                 (innerState.mHeaderContentLength != null) ?
541                         Long.parseLong(innerState.mHeaderContentLength) : 0,
542                 mInfo.mIsPublicApi, mStorageManager);
543         try {
544             state.mStream = new FileOutputStream(state.mFilename);
545         } catch (FileNotFoundException exc) {
546             throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
547                     "while opening destination file: " + exc.toString(), exc);
548         }
549         if (Constants.LOGV) {
550             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
551         }
552
553         updateDatabaseFromHeaders(state, innerState);
554         // check connectivity again now that we know the total size
555         checkConnectivity();
556     }
557
558     /**
559      * Update necessary database fields based on values of HTTP response headers that have been
560      * read.
561      */
562     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
563         ContentValues values = new ContentValues();
564         values.put(Downloads.Impl._DATA, state.mFilename);
565         if (innerState.mHeaderETag != null) {
566             values.put(Constants.ETAG, innerState.mHeaderETag);
567         }
568         if (state.mMimeType != null) {
569             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
570         }
571         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
572         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
573     }
574
575     /**
576      * Read headers from the HTTP response and store them into local state.
577      */
578     private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
579             throws StopRequestException {
580         Header header = response.getFirstHeader("Content-Disposition");
581         if (header != null) {
582             innerState.mHeaderContentDisposition = header.getValue();
583         }
584         header = response.getFirstHeader("Content-Location");
585         if (header != null) {
586             innerState.mHeaderContentLocation = header.getValue();
587         }
588         if (state.mMimeType == null) {
589             header = response.getFirstHeader("Content-Type");
590             if (header != null) {
591                 state.mMimeType = sanitizeMimeType(header.getValue());
592             }
593         }
594         header = response.getFirstHeader("ETag");
595         if (header != null) {
596             innerState.mHeaderETag = header.getValue();
597         }
598         String headerTransferEncoding = null;
599         header = response.getFirstHeader("Transfer-Encoding");
600         if (header != null) {
601             headerTransferEncoding = header.getValue();
602         }
603         if (headerTransferEncoding == null) {
604             header = response.getFirstHeader("Content-Length");
605             if (header != null) {
606                 innerState.mHeaderContentLength = header.getValue();
607                 innerState.mTotalBytes = mInfo.mTotalBytes =
608                         Long.parseLong(innerState.mHeaderContentLength);
609             }
610         } else {
611             // Ignore content-length with transfer-encoding - 2616 4.4 3
612             if (Constants.LOGVV) {
613                 Log.v(Constants.TAG,
614                         "ignoring content-length because of xfer-encoding");
615             }
616         }
617         if (Constants.LOGVV) {
618             Log.v(Constants.TAG, "Content-Disposition: " +
619                     innerState.mHeaderContentDisposition);
620             Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
621             Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
622             Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
623             Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
624             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
625         }
626
627         boolean noSizeInfo = innerState.mHeaderContentLength == null
628                 && (headerTransferEncoding == null
629                     || !headerTransferEncoding.equalsIgnoreCase("chunked"));
630         if (!mInfo.mNoIntegrity && noSizeInfo) {
631             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
632                     "can't know size of download, giving up");
633         }
634     }
635
636     /**
637      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
638      */
639     private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
640             throws StopRequestException, RetryDownload {
641         int statusCode = response.getStatusLine().getStatusCode();
642         if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
643             handleServiceUnavailable(state, response);
644         }
645         if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
646             handleRedirect(state, response, statusCode);
647         }
648
649         if (Constants.LOGV) {
650             Log.i(Constants.TAG, "recevd_status = " + statusCode +
651                     ", mContinuingDownload = " + innerState.mContinuingDownload);
652         }
653         int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
654         if (statusCode != expectedStatus) {
655             handleOtherStatus(state, innerState, statusCode);
656         }
657     }
658
659     /**
660      * Handle a status that we don't know how to deal with properly.
661      */
662     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
663             throws StopRequestException {
664         if (statusCode == 416) {
665             // range request failed. it should never fail.
666             throw new IllegalStateException("Http Range request failure: totalBytes = " +
667                     innerState.mTotalBytes + ", bytes recvd so far: " + innerState.mBytesSoFar);
668         }
669         int finalStatus;
670         if (Downloads.Impl.isStatusError(statusCode)) {
671             finalStatus = statusCode;
672         } else if (statusCode >= 300 && statusCode < 400) {
673             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
674         } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
675             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
676         } else {
677             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
678         }
679         throw new StopRequestException(finalStatus, "http error " +
680                 statusCode + ", mContinuingDownload: " + innerState.mContinuingDownload);
681     }
682
683     /**
684      * Handle a 3xx redirect status.
685      */
686     private void handleRedirect(State state, HttpResponse response, int statusCode)
687             throws StopRequestException, RetryDownload {
688         if (Constants.LOGVV) {
689             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
690         }
691         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
692             throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
693                     "too many redirects");
694         }
695         Header header = response.getFirstHeader("Location");
696         if (header == null) {
697             return;
698         }
699         if (Constants.LOGVV) {
700             Log.v(Constants.TAG, "Location :" + header.getValue());
701         }
702
703         String newUri;
704         try {
705             newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
706         } catch(URISyntaxException ex) {
707             if (Constants.LOGV) {
708                 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
709                         + " for " + mInfo.mUri);
710             }
711             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
712                     "Couldn't resolve redirect URI");
713         }
714         ++state.mRedirectCount;
715         state.mRequestUri = newUri;
716         if (statusCode == 301 || statusCode == 303) {
717             // use the new URI for all future requests (should a retry/resume be necessary)
718             state.mNewUri = newUri;
719         }
720         throw new RetryDownload();
721     }
722
723     /**
724      * Handle a 503 Service Unavailable status by processing the Retry-After header.
725      */
726     private void handleServiceUnavailable(State state, HttpResponse response)
727             throws StopRequestException {
728         if (Constants.LOGVV) {
729             Log.v(Constants.TAG, "got HTTP response code 503");
730         }
731         state.mCountRetry = true;
732         Header header = response.getFirstHeader("Retry-After");
733         if (header != null) {
734            try {
735                if (Constants.LOGVV) {
736                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
737                }
738                state.mRetryAfter = Integer.parseInt(header.getValue());
739                if (state.mRetryAfter < 0) {
740                    state.mRetryAfter = 0;
741                } else {
742                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
743                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
744                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
745                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
746                    }
747                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
748                    state.mRetryAfter *= 1000;
749                }
750            } catch (NumberFormatException ex) {
751                // ignored - retryAfter stays 0 in this case.
752            }
753         }
754         throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
755                 "got 503 Service Unavailable, will retry later");
756     }
757
758     /**
759      * Send the request to the server, handling any I/O exceptions.
760      */
761     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
762             throws StopRequestException {
763         try {
764             return client.execute(request);
765         } catch (IllegalArgumentException ex) {
766             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
767                     "while trying to execute request: " + ex.toString(), ex);
768         } catch (IOException ex) {
769             logNetworkState();
770             throw new StopRequestException(getFinalStatusForHttpError(state),
771                     "while trying to execute request: " + ex.toString(), ex);
772         }
773     }
774
775     private int getFinalStatusForHttpError(State state) {
776         int networkUsable = mInfo.checkCanUseNetwork();
777         if (networkUsable != DownloadInfo.NETWORK_OK) {
778             return (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE ||
779                     networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE)
780                     ? Downloads.Impl.STATUS_QUEUED_FOR_WIFI
781                     : Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
782         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
783             state.mCountRetry = true;
784             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
785         } else {
786             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
787             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
788         }
789     }
790
791     /**
792      * Prepare the destination file to receive data.  If the file already exists, we'll set up
793      * appropriately for resumption.
794      */
795     private void setupDestinationFile(State state, InnerState innerState)
796             throws StopRequestException {
797         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
798             if (Constants.LOGV) {
799                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
800                         ", and state.mFilename: " + state.mFilename);
801             }
802             if (!Helpers.isFilenameValid(state.mFilename,
803                     mStorageManager.getDownloadDataDirectory())) {
804                 // this should never happen
805                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
806                         "found invalid internal destination filename");
807             }
808             // We're resuming a download that got interrupted
809             File f = new File(state.mFilename);
810             if (f.exists()) {
811                 if (Constants.LOGV) {
812                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
813                             ", and state.mFilename: " + state.mFilename);
814                 }
815                 long fileLength = f.length();
816                 if (fileLength == 0) {
817                     // The download hadn't actually started, we can restart from scratch
818                     f.delete();
819                     state.mFilename = null;
820                     if (Constants.LOGV) {
821                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
822                                 ", BUT starting from scratch again: ");
823                     }
824                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
825                     // This should've been caught upon failure
826                     f.delete();
827                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
828                             "Trying to resume a download that can't be resumed");
829                 } else {
830                     // All right, we'll be able to resume this download
831                     if (Constants.LOGV) {
832                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
833                                 ", and starting with file of length: " + fileLength);
834                     }
835                     try {
836                         state.mStream = new FileOutputStream(state.mFilename, true);
837                     } catch (FileNotFoundException exc) {
838                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
839                                 "while opening destination for resuming: " + exc.toString(), exc);
840                     }
841                     innerState.mBytesSoFar = (int) fileLength;
842                     if (mInfo.mTotalBytes != -1) {
843                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
844                     }
845                     innerState.mHeaderETag = mInfo.mETag;
846                     innerState.mContinuingDownload = true;
847                     if (Constants.LOGV) {
848                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
849                                 ", innerState.mBytesSoFar: " + innerState.mBytesSoFar +
850                                 ", and setting mContinuingDownload to true: ");
851                     }
852                 }
853             }
854         }
855
856         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
857             closeDestination(state);
858         }
859     }
860
861     /**
862      * Add custom headers for this download to the HTTP request.
863      */
864     private void addRequestHeaders(InnerState innerState, HttpGet request) {
865         for (Pair<String, String> header : mInfo.getHeaders()) {
866             request.addHeader(header.first, header.second);
867         }
868
869         if (innerState.mContinuingDownload) {
870             if (innerState.mHeaderETag != null) {
871                 request.addHeader("If-Match", innerState.mHeaderETag);
872             }
873             request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
874             if (Constants.LOGV) {
875                 Log.i(Constants.TAG, "Adding Range header: " +
876                         "bytes=" + innerState.mBytesSoFar + "-");
877             }
878         }
879     }
880
881     /**
882      * Stores information about the completed download, and notifies the initiating application.
883      */
884     private void notifyDownloadCompleted(
885             int status, boolean countRetry, int retryAfter, boolean gotData,
886             String filename, String uri, String mimeType, String errorMsg) {
887         notifyThroughDatabase(
888                 status, countRetry, retryAfter, gotData, filename, uri, mimeType,
889                 errorMsg);
890         if (Downloads.Impl.isStatusCompleted(status)) {
891             mInfo.sendIntentIfRequested();
892         }
893     }
894
895     private void notifyThroughDatabase(
896             int status, boolean countRetry, int retryAfter, boolean gotData,
897             String filename, String uri, String mimeType, String errorMsg) {
898         ContentValues values = new ContentValues();
899         values.put(Downloads.Impl.COLUMN_STATUS, status);
900         values.put(Downloads.Impl._DATA, filename);
901         if (uri != null) {
902             values.put(Downloads.Impl.COLUMN_URI, uri);
903         }
904         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
905         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
906         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
907         if (!countRetry) {
908             values.put(Constants.FAILED_CONNECTIONS, 0);
909         } else if (gotData) {
910             values.put(Constants.FAILED_CONNECTIONS, 1);
911         } else {
912             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
913         }
914         // save the error message. could be useful to developers.
915         if (!TextUtils.isEmpty(errorMsg)) {
916             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
917         }
918         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
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 }