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