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