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