Add and enforce ALLOW_METERED column.
[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             }
274             throw new StopRequestException(status,
275                     mInfo.getLogMessageForNetworkError(networkUsable));
276         }
277     }
278
279     /**
280      * Transfer as much data as possible from the HTTP response to the destination file.
281      * @param data buffer to use to read data
282      * @param entityStream stream for reading the HTTP response entity
283      */
284     private void transferData(
285             State state, InnerState innerState, byte[] data, InputStream entityStream)
286             throws StopRequestException {
287         for (;;) {
288             int bytesRead = readFromResponse(state, innerState, data, entityStream);
289             if (bytesRead == -1) { // success, end of stream already reached
290                 handleEndOfStream(state, innerState);
291                 return;
292             }
293
294             state.mGotData = true;
295             writeDataToDestination(state, data, bytesRead);
296             state.mCurrentBytes += bytesRead;
297             reportProgress(state, innerState);
298
299             if (Constants.LOGVV) {
300                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
301                       + mInfo.mUri);
302             }
303
304             checkPausedOrCanceled(state);
305         }
306     }
307
308     /**
309      * Called after a successful completion to take any necessary action on the downloaded file.
310      */
311     private void finalizeDestinationFile(State state) throws StopRequestException {
312         if (state.mFilename != null) {
313             // make sure the file is readable
314             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
315             syncDestination(state);
316         }
317     }
318
319     /**
320      * Called just before the thread finishes, regardless of status, to take any necessary action on
321      * the downloaded file.
322      */
323     private void cleanupDestination(State state, int finalStatus) {
324         if (mDrmConvertSession != null) {
325             finalStatus = mDrmConvertSession.close(state.mFilename);
326         }
327
328         closeDestination(state);
329         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
330             new File(state.mFilename).delete();
331             state.mFilename = null;
332         }
333     }
334
335     /**
336      * Sync the destination file to storage.
337      */
338     private void syncDestination(State state) {
339         FileOutputStream downloadedFileStream = null;
340         try {
341             downloadedFileStream = new FileOutputStream(state.mFilename, true);
342             downloadedFileStream.getFD().sync();
343         } catch (FileNotFoundException ex) {
344             Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
345         } catch (SyncFailedException ex) {
346             Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
347         } catch (IOException ex) {
348             Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
349         } catch (RuntimeException ex) {
350             Log.w(Constants.TAG, "exception while syncing file: ", ex);
351         } finally {
352             if(downloadedFileStream != null) {
353                 try {
354                     downloadedFileStream.close();
355                 } catch (IOException ex) {
356                     Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
357                 } catch (RuntimeException ex) {
358                     Log.w(Constants.TAG, "exception while closing file: ", ex);
359                 }
360             }
361         }
362     }
363
364     /**
365      * Close the destination output stream.
366      */
367     private void closeDestination(State state) {
368         try {
369             // close the file
370             if (state.mStream != null) {
371                 state.mStream.close();
372                 state.mStream = null;
373             }
374         } catch (IOException ex) {
375             if (Constants.LOGV) {
376                 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
377             }
378             // nothing can really be done if the file can't be closed
379         }
380     }
381
382     /**
383      * Check if the download has been paused or canceled, stopping the request appropriately if it
384      * has been.
385      */
386     private void checkPausedOrCanceled(State state) throws StopRequestException {
387         synchronized (mInfo) {
388             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
389                 throw new StopRequestException(
390                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
391             }
392             if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
393                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
394             }
395         }
396
397         // if policy has been changed, trigger connectivity check
398         if (mPolicyDirty) {
399             checkConnectivity();
400         }
401     }
402
403     /**
404      * Report download progress through the database if necessary.
405      */
406     private void reportProgress(State state, InnerState innerState) {
407         long now = mSystemFacade.currentTimeMillis();
408         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
409             now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
410             ContentValues values = new ContentValues();
411             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
412             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
413             state.mBytesNotified = state.mCurrentBytes;
414             state.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                 if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) {
433                     state.mStream.write(data, 0, bytesRead);
434                 } else {
435                     byte[] convertedData = mDrmConvertSession.convert(data, bytesRead);
436                     if (convertedData != null) {
437                         state.mStream.write(convertedData, 0, convertedData.length);
438                     } else {
439                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
440                                 "Error converting drm data.");
441                     }
442                 }
443                 return;
444             } catch (IOException ex) {
445                 // couldn't write to file. are we out of space? check.
446                 // TODO this check should only be done once. why is this being done
447                 // in a while(true) loop (see the enclosing statement: for(;;)
448                 if (state.mStream != null) {
449                     mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
450                 }
451             } finally {
452                 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
453                     closeDestination(state);
454                 }
455             }
456         }
457     }
458
459     /**
460      * Called when we've reached the end of the HTTP response stream, to update the database and
461      * check for consistency.
462      */
463     private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
464         ContentValues values = new ContentValues();
465         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
466         if (innerState.mHeaderContentLength == null) {
467             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
468         }
469         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
470
471         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
472                 && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength));
473         if (lengthMismatched) {
474             if (cannotResume(state)) {
475                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
476                         "mismatched content length");
477             } else {
478                 throw new StopRequestException(getFinalStatusForHttpError(state),
479                         "closed socket before end of file");
480             }
481         }
482     }
483
484     private boolean cannotResume(State state) {
485         return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null;
486     }
487
488     /**
489      * Read some data from the HTTP response stream, handling I/O errors.
490      * @param data buffer to use to read data
491      * @param entityStream stream for reading the HTTP response entity
492      * @return the number of bytes actually read or -1 if the end of the stream has been reached
493      */
494     private int readFromResponse(State state, InnerState innerState, byte[] data,
495                                  InputStream entityStream) throws StopRequestException {
496         try {
497             return entityStream.read(data);
498         } catch (IOException ex) {
499             logNetworkState(mInfo.mUid);
500             ContentValues values = new ContentValues();
501             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
502             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
503             if (cannotResume(state)) {
504                 String message = "while reading response: " + ex.toString()
505                 + ", can't resume interrupted download with no ETag";
506                 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
507                         message, ex);
508             } else {
509                 throw new StopRequestException(getFinalStatusForHttpError(state),
510                         "while reading response: " + ex.toString(), ex);
511             }
512         }
513     }
514
515     /**
516      * Open a stream for the HTTP response entity, handling I/O errors.
517      * @return an InputStream to read the response entity
518      */
519     private InputStream openResponseEntity(State state, HttpResponse response)
520             throws StopRequestException {
521         try {
522             return response.getEntity().getContent();
523         } catch (IOException ex) {
524             logNetworkState(mInfo.mUid);
525             throw new StopRequestException(getFinalStatusForHttpError(state),
526                     "while getting entity: " + ex.toString(), ex);
527         }
528     }
529
530     private void logNetworkState(int uid) {
531         if (Constants.LOGX) {
532             Log.i(Constants.TAG,
533                     "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down"));
534         }
535     }
536
537     /**
538      * Read HTTP response headers and take appropriate action, including setting up the destination
539      * file and updating the database.
540      */
541     private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
542             throws StopRequestException {
543         if (state.mContinuingDownload) {
544             // ignore response headers on resume requests
545             return;
546         }
547
548         readResponseHeaders(state, innerState, response);
549         if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
550             mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType);
551             if (mDrmConvertSession == null) {
552                 throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype "
553                         + state.mMimeType + " can not be converted.");
554             }
555         }
556
557         state.mFilename = Helpers.generateSaveFile(
558                 mContext,
559                 mInfo.mUri,
560                 mInfo.mHint,
561                 innerState.mHeaderContentDisposition,
562                 innerState.mHeaderContentLocation,
563                 state.mMimeType,
564                 mInfo.mDestination,
565                 (innerState.mHeaderContentLength != null) ?
566                         Long.parseLong(innerState.mHeaderContentLength) : 0,
567                 mInfo.mIsPublicApi, mStorageManager);
568         try {
569             state.mStream = new FileOutputStream(state.mFilename);
570         } catch (FileNotFoundException exc) {
571             throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
572                     "while opening destination file: " + exc.toString(), exc);
573         }
574         if (Constants.LOGV) {
575             Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
576         }
577
578         updateDatabaseFromHeaders(state, innerState);
579         // check connectivity again now that we know the total size
580         checkConnectivity();
581     }
582
583     /**
584      * Update necessary database fields based on values of HTTP response headers that have been
585      * read.
586      */
587     private void updateDatabaseFromHeaders(State state, InnerState innerState) {
588         ContentValues values = new ContentValues();
589         values.put(Downloads.Impl._DATA, state.mFilename);
590         if (state.mHeaderETag != null) {
591             values.put(Constants.ETAG, state.mHeaderETag);
592         }
593         if (state.mMimeType != null) {
594             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
595         }
596         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
597         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
598     }
599
600     /**
601      * Read headers from the HTTP response and store them into local state.
602      */
603     private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
604             throws StopRequestException {
605         Header header = response.getFirstHeader("Content-Disposition");
606         if (header != null) {
607             innerState.mHeaderContentDisposition = header.getValue();
608         }
609         header = response.getFirstHeader("Content-Location");
610         if (header != null) {
611             innerState.mHeaderContentLocation = header.getValue();
612         }
613         if (state.mMimeType == null) {
614             header = response.getFirstHeader("Content-Type");
615             if (header != null) {
616                 state.mMimeType = Intent.normalizeMimeType(header.getValue());
617             }
618         }
619         header = response.getFirstHeader("ETag");
620         if (header != null) {
621             state.mHeaderETag = header.getValue();
622         }
623         String headerTransferEncoding = null;
624         header = response.getFirstHeader("Transfer-Encoding");
625         if (header != null) {
626             headerTransferEncoding = header.getValue();
627         }
628         if (headerTransferEncoding == null) {
629             header = response.getFirstHeader("Content-Length");
630             if (header != null) {
631                 innerState.mHeaderContentLength = header.getValue();
632                 state.mTotalBytes = mInfo.mTotalBytes =
633                         Long.parseLong(innerState.mHeaderContentLength);
634             }
635         } else {
636             // Ignore content-length with transfer-encoding - 2616 4.4 3
637             if (Constants.LOGVV) {
638                 Log.v(Constants.TAG,
639                         "ignoring content-length because of xfer-encoding");
640             }
641         }
642         if (Constants.LOGVV) {
643             Log.v(Constants.TAG, "Content-Disposition: " +
644                     innerState.mHeaderContentDisposition);
645             Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
646             Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
647             Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
648             Log.v(Constants.TAG, "ETag: " + state.mHeaderETag);
649             Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
650         }
651
652         boolean noSizeInfo = innerState.mHeaderContentLength == null
653                 && (headerTransferEncoding == null
654                     || !headerTransferEncoding.equalsIgnoreCase("chunked"));
655         if (!mInfo.mNoIntegrity && noSizeInfo) {
656             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
657                     "can't know size of download, giving up");
658         }
659     }
660
661     /**
662      * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
663      */
664     private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
665             throws StopRequestException, RetryDownload {
666         int statusCode = response.getStatusLine().getStatusCode();
667         if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
668             handleServiceUnavailable(state, response);
669         }
670         if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
671             handleRedirect(state, response, statusCode);
672         }
673
674         if (Constants.LOGV) {
675             Log.i(Constants.TAG, "recevd_status = " + statusCode +
676                     ", mContinuingDownload = " + state.mContinuingDownload);
677         }
678         int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
679         if (statusCode != expectedStatus) {
680             handleOtherStatus(state, innerState, statusCode);
681         }
682     }
683
684     /**
685      * Handle a status that we don't know how to deal with properly.
686      */
687     private void handleOtherStatus(State state, InnerState innerState, int statusCode)
688             throws StopRequestException {
689         if (statusCode == 416) {
690             // range request failed. it should never fail.
691             throw new IllegalStateException("Http Range request failure: totalBytes = " +
692                     state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
693         }
694         int finalStatus;
695         if (Downloads.Impl.isStatusError(statusCode)) {
696             finalStatus = statusCode;
697         } else if (statusCode >= 300 && statusCode < 400) {
698             finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
699         } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
700             finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
701         } else {
702             finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
703         }
704         throw new StopRequestException(finalStatus, "http error " +
705                 statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
706     }
707
708     /**
709      * Handle a 3xx redirect status.
710      */
711     private void handleRedirect(State state, HttpResponse response, int statusCode)
712             throws StopRequestException, RetryDownload {
713         if (Constants.LOGVV) {
714             Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
715         }
716         if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
717             throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
718                     "too many redirects");
719         }
720         Header header = response.getFirstHeader("Location");
721         if (header == null) {
722             return;
723         }
724         if (Constants.LOGVV) {
725             Log.v(Constants.TAG, "Location :" + header.getValue());
726         }
727
728         String newUri;
729         try {
730             newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
731         } catch(URISyntaxException ex) {
732             if (Constants.LOGV) {
733                 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
734                         + " for " + mInfo.mUri);
735             }
736             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
737                     "Couldn't resolve redirect URI");
738         }
739         ++state.mRedirectCount;
740         state.mRequestUri = newUri;
741         if (statusCode == 301 || statusCode == 303) {
742             // use the new URI for all future requests (should a retry/resume be necessary)
743             state.mNewUri = newUri;
744         }
745         throw new RetryDownload();
746     }
747
748     /**
749      * Handle a 503 Service Unavailable status by processing the Retry-After header.
750      */
751     private void handleServiceUnavailable(State state, HttpResponse response)
752             throws StopRequestException {
753         if (Constants.LOGVV) {
754             Log.v(Constants.TAG, "got HTTP response code 503");
755         }
756         state.mCountRetry = true;
757         Header header = response.getFirstHeader("Retry-After");
758         if (header != null) {
759            try {
760                if (Constants.LOGVV) {
761                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
762                }
763                state.mRetryAfter = Integer.parseInt(header.getValue());
764                if (state.mRetryAfter < 0) {
765                    state.mRetryAfter = 0;
766                } else {
767                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
768                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
769                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
770                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
771                    }
772                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
773                    state.mRetryAfter *= 1000;
774                }
775            } catch (NumberFormatException ex) {
776                // ignored - retryAfter stays 0 in this case.
777            }
778         }
779         throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
780                 "got 503 Service Unavailable, will retry later");
781     }
782
783     /**
784      * Send the request to the server, handling any I/O exceptions.
785      */
786     private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
787             throws StopRequestException {
788         try {
789             return client.execute(request);
790         } catch (IllegalArgumentException ex) {
791             throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
792                     "while trying to execute request: " + ex.toString(), ex);
793         } catch (IOException ex) {
794             logNetworkState(mInfo.mUid);
795             throw new StopRequestException(getFinalStatusForHttpError(state),
796                     "while trying to execute request: " + ex.toString(), ex);
797         }
798     }
799
800     private int getFinalStatusForHttpError(State state) {
801         int networkUsable = mInfo.checkCanUseNetwork();
802         if (networkUsable != DownloadInfo.NETWORK_OK) {
803             switch (networkUsable) {
804                 case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
805                 case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
806                     return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
807                 default:
808                     return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
809             }
810         } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
811             state.mCountRetry = true;
812             return Downloads.Impl.STATUS_WAITING_TO_RETRY;
813         } else {
814             Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
815             return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
816         }
817     }
818
819     /**
820      * Prepare the destination file to receive data.  If the file already exists, we'll set up
821      * appropriately for resumption.
822      */
823     private void setupDestinationFile(State state, InnerState innerState)
824             throws StopRequestException {
825         if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
826             if (Constants.LOGV) {
827                 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
828                         ", and state.mFilename: " + state.mFilename);
829             }
830             if (!Helpers.isFilenameValid(state.mFilename,
831                     mStorageManager.getDownloadDataDirectory())) {
832                 // this should never happen
833                 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
834                         "found invalid internal destination filename");
835             }
836             // We're resuming a download that got interrupted
837             File f = new File(state.mFilename);
838             if (f.exists()) {
839                 if (Constants.LOGV) {
840                     Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
841                             ", and state.mFilename: " + state.mFilename);
842                 }
843                 long fileLength = f.length();
844                 if (fileLength == 0) {
845                     // The download hadn't actually started, we can restart from scratch
846                     f.delete();
847                     state.mFilename = null;
848                     if (Constants.LOGV) {
849                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
850                                 ", BUT starting from scratch again: ");
851                     }
852                 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
853                     // This should've been caught upon failure
854                     f.delete();
855                     throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
856                             "Trying to resume a download that can't be resumed");
857                 } else {
858                     // All right, we'll be able to resume this download
859                     if (Constants.LOGV) {
860                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
861                                 ", and starting with file of length: " + fileLength);
862                     }
863                     try {
864                         state.mStream = new FileOutputStream(state.mFilename, true);
865                     } catch (FileNotFoundException exc) {
866                         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
867                                 "while opening destination for resuming: " + exc.toString(), exc);
868                     }
869                     state.mCurrentBytes = (int) fileLength;
870                     if (mInfo.mTotalBytes != -1) {
871                         innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
872                     }
873                     state.mHeaderETag = mInfo.mETag;
874                     state.mContinuingDownload = true;
875                     if (Constants.LOGV) {
876                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
877                                 ", state.mCurrentBytes: " + state.mCurrentBytes +
878                                 ", and setting mContinuingDownload to true: ");
879                     }
880                 }
881             }
882         }
883
884         if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
885             closeDestination(state);
886         }
887     }
888
889     /**
890      * Add custom headers for this download to the HTTP request.
891      */
892     private void addRequestHeaders(State state, HttpGet request) {
893         for (Pair<String, String> header : mInfo.getHeaders()) {
894             request.addHeader(header.first, header.second);
895         }
896
897         if (state.mContinuingDownload) {
898             if (state.mHeaderETag != null) {
899                 request.addHeader("If-Match", state.mHeaderETag);
900             }
901             request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-");
902             if (Constants.LOGV) {
903                 Log.i(Constants.TAG, "Adding Range header: " +
904                         "bytes=" + state.mCurrentBytes + "-");
905                 Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
906             }
907         }
908     }
909
910     /**
911      * Stores information about the completed download, and notifies the initiating application.
912      */
913     private void notifyDownloadCompleted(
914             int status, boolean countRetry, int retryAfter, boolean gotData,
915             String filename, String uri, String mimeType, String errorMsg) {
916         notifyThroughDatabase(
917                 status, countRetry, retryAfter, gotData, filename, uri, mimeType,
918                 errorMsg);
919         if (Downloads.Impl.isStatusCompleted(status)) {
920             mInfo.sendIntentIfRequested();
921         }
922     }
923
924     private void notifyThroughDatabase(
925             int status, boolean countRetry, int retryAfter, boolean gotData,
926             String filename, String uri, String mimeType, String errorMsg) {
927         ContentValues values = new ContentValues();
928         values.put(Downloads.Impl.COLUMN_STATUS, status);
929         values.put(Downloads.Impl._DATA, filename);
930         if (uri != null) {
931             values.put(Downloads.Impl.COLUMN_URI, uri);
932         }
933         values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
934         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
935         values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
936         if (!countRetry) {
937             values.put(Constants.FAILED_CONNECTIONS, 0);
938         } else if (gotData) {
939             values.put(Constants.FAILED_CONNECTIONS, 1);
940         } else {
941             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
942         }
943         // save the error message. could be useful to developers.
944         if (!TextUtils.isEmpty(errorMsg)) {
945             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
946         }
947         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
948     }
949
950     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
951         @Override
952         public void onUidRulesChanged(int uid, int uidRules) {
953             // caller is NPMS, since we only register with them
954             if (uid == mInfo.mUid) {
955                 mPolicyDirty = true;
956             }
957         }
958
959         @Override
960         public void onMeteredIfacesChanged(String[] meteredIfaces) {
961             // caller is NPMS, since we only register with them
962             mPolicyDirty = true;
963         }
964
965         @Override
966         public void onRestrictBackgroundChanged(boolean restrictBackground) {
967             // caller is NPMS, since we only register with them
968             mPolicyDirty = true;
969         }
970     };
971 }