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