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