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