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