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