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