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