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