2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.providers.downloads;
19 import org.apache.http.client.methods.AbortableHttpRequest;
20 import org.apache.http.client.methods.HttpGet;
21 import org.apache.http.client.methods.HttpPost;
22 import org.apache.http.client.methods.HttpUriRequest;
23 import org.apache.http.client.HttpClient;
24 import org.apache.http.entity.StringEntity;
25 import org.apache.http.Header;
26 import org.apache.http.HttpResponse;
28 import android.content.ContentUris;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.database.Cursor;
33 import android.drm.mobile1.DrmRawContent;
34 import android.net.http.AndroidHttpClient;
35 import android.net.Uri;
36 import android.os.FileUtils;
37 import android.os.PowerManager;
38 import android.os.Process;
39 import android.provider.Downloads;
40 import android.provider.DrmStore;
41 import android.util.Config;
42 import android.util.Log;
45 import java.io.FileNotFoundException;
46 import java.io.FileOutputStream;
47 import java.io.InputStream;
48 import java.io.IOException;
49 import java.io.UnsupportedEncodingException;
53 * Runs an actual download
55 public class DownloadThread extends Thread {
57 private Context mContext;
58 private DownloadInfo mInfo;
60 public DownloadThread(Context context, DownloadInfo info) {
66 * Returns the user agent provided by the initiating app, or use the default one
68 private String userAgent() {
69 String userAgent = mInfo.userAgent;
70 if (userAgent != null) {
72 if (userAgent == null) {
73 userAgent = Constants.DEFAULT_USER_AGENT;
79 * Executes the download in a separate thread
82 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
84 int finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
85 boolean countRetry = false;
87 int redirectCount = mInfo.redirectCount;
89 boolean gotData = false;
90 String filename = null;
91 String mimeType = mInfo.mimetype;
92 FileOutputStream stream = null;
93 AndroidHttpClient client = null;
94 PowerManager.WakeLock wakeLock = null;
95 Uri contentUri = Uri.parse(Downloads.CONTENT_URI + "/" + mInfo.id);
98 boolean continuingDownload = false;
99 String headerAcceptRanges = null;
100 String headerContentDisposition = null;
101 String headerContentLength = null;
102 String headerContentLocation = null;
103 String headerETag = null;
104 String headerTransferEncoding = null;
106 byte data[] = new byte[Constants.BUFFER_SIZE];
110 PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
111 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
114 filename = mInfo.filename;
115 if (filename != null) {
116 if (!Helpers.isFilenameValid(filename)) {
117 finalStatus = Downloads.STATUS_FILE_ERROR;
118 notifyDownloadCompleted(
119 finalStatus, false, 0, 0, false, filename, null, mInfo.mimetype);
122 // We're resuming a download that got interrupted
123 File f = new File(filename);
125 long fileLength = f.length();
126 if (fileLength == 0) {
127 // The download hadn't actually started, we can restart from scratch
130 } else if (mInfo.etag == null && !mInfo.noIntegrity) {
131 // Tough luck, that's not a resumable download
134 "can't resume interrupted non-resumable download");
137 finalStatus = Downloads.STATUS_PRECONDITION_FAILED;
138 notifyDownloadCompleted(
139 finalStatus, false, 0, 0, false, filename, null, mInfo.mimetype);
142 // All right, we'll be able to resume this download
143 stream = new FileOutputStream(filename, true);
144 bytesSoFar = (int) fileLength;
145 if (mInfo.totalBytes != -1) {
146 headerContentLength = Integer.toString(mInfo.totalBytes);
148 headerETag = mInfo.etag;
149 continuingDownload = true;
154 int bytesNotified = bytesSoFar;
155 // starting with MIN_VALUE means that the first write will commit
156 // progress to the database
157 long timeLastNotification = 0;
159 client = AndroidHttpClient.newInstance(userAgent());
161 if (stream != null && mInfo.destination == Downloads.DESTINATION_EXTERNAL
162 && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
163 .equalsIgnoreCase(mimeType)) {
167 } catch (IOException ex) {
168 if (Constants.LOGV) {
169 Log.v(Constants.TAG, "exception when closing the file before download : " +
172 // nothing can really be done if the file can't be closed
177 * This loop is run once for every individual HTTP request that gets sent.
178 * The very first HTTP request is a "virgin" request, while every subsequent
179 * request is done with the original ETag and a byte-range.
183 // Prepares the request and fires it.
184 HttpGet request = new HttpGet(mInfo.uri);
186 if (Constants.LOGV) {
187 Log.v(Constants.TAG, "initiating download for " + mInfo.uri);
190 if (mInfo.cookies != null) {
191 request.addHeader("Cookie", mInfo.cookies);
193 if (mInfo.referer != null) {
194 request.addHeader("Referer", mInfo.referer);
196 if (continuingDownload) {
197 if (headerETag != null) {
198 request.addHeader("If-Match", headerETag);
200 request.addHeader("Range", "bytes=" + bytesSoFar + "-");
203 HttpResponse response;
205 response = client.execute(request);
206 } catch (IllegalArgumentException ex) {
207 if (Constants.LOGV) {
208 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
209 mInfo.uri + " : " + ex);
210 } else if (Config.LOGD) {
211 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
212 mInfo.id + " : " + ex);
214 finalStatus = Downloads.STATUS_BAD_REQUEST;
216 break http_request_loop;
217 } catch (IOException ex) {
218 if (!Helpers.isNetworkAvailable(mContext)) {
219 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
220 } else if (mInfo.numFailed < Constants.MAX_RETRIES) {
221 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
224 if (Constants.LOGV) {
225 Log.d(Constants.TAG, "IOException trying to execute request for " +
226 mInfo.uri + " : " + ex);
227 } else if (Config.LOGD) {
228 Log.d(Constants.TAG, "IOException trying to execute request for " +
229 mInfo.id + " : " + ex);
231 finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
234 break http_request_loop;
237 int statusCode = response.getStatusLine().getStatusCode();
238 if (statusCode == 503 && mInfo.numFailed < Constants.MAX_RETRIES) {
239 if (Constants.LOGVV) {
240 Log.v(Constants.TAG, "got HTTP response code 503");
242 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
244 Header header = response.getFirstHeader("Retry-After");
245 if (header != null) {
247 if (Constants.LOGVV) {
248 Log.v(Constants.TAG, "Retry-After :" + header.getValue());
250 retryAfter = Integer.parseInt(header.getValue());
251 if (retryAfter < 0) {
254 if (retryAfter < Constants.MIN_RETRY_AFTER) {
255 retryAfter = Constants.MIN_RETRY_AFTER;
256 } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
257 retryAfter = Constants.MAX_RETRY_AFTER;
259 retryAfter += Helpers.rnd.nextInt(Constants.MIN_RETRY_AFTER + 1);
262 } catch (NumberFormatException ex) {
263 // ignored - retryAfter stays 0 in this case.
267 break http_request_loop;
269 if (statusCode == 301 ||
273 if (Constants.LOGVV) {
274 Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
276 if (redirectCount >= Constants.MAX_REDIRECTS) {
277 if (Constants.LOGV) {
278 Log.d(Constants.TAG, "too many redirects for download " + mInfo.id +
280 } else if (Config.LOGD) {
281 Log.d(Constants.TAG, "too many redirects for download " + mInfo.id);
283 finalStatus = Downloads.STATUS_TOO_MANY_REDIRECTS;
285 break http_request_loop;
287 Header header = response.getFirstHeader("Location");
288 if (header != null) {
289 if (Constants.LOGVV) {
290 Log.v(Constants.TAG, "Location :" + header.getValue());
292 newUri = new URI(mInfo.uri).resolve(new URI(header.getValue())).toString();
294 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
296 break http_request_loop;
299 if ((!continuingDownload && statusCode != Downloads.STATUS_SUCCESS)
300 || (continuingDownload && statusCode != 206)) {
301 if (Constants.LOGV) {
302 Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.uri);
303 } else if (Config.LOGD) {
304 Log.d(Constants.TAG, "http error " + statusCode + " for download " +
307 if (Downloads.isStatusError(statusCode)) {
308 finalStatus = statusCode;
309 } else if (statusCode >= 300 && statusCode < 400) {
310 finalStatus = Downloads.STATUS_UNHANDLED_REDIRECT;
311 } else if (continuingDownload && statusCode == Downloads.STATUS_SUCCESS) {
312 finalStatus = Downloads.STATUS_PRECONDITION_FAILED;
314 finalStatus = Downloads.STATUS_UNHANDLED_HTTP_CODE;
317 break http_request_loop;
319 // Handles the response, saves the file
320 if (Constants.LOGV) {
321 Log.v(Constants.TAG, "received response for " + mInfo.uri);
324 if (!continuingDownload) {
325 Header header = response.getFirstHeader("Accept-Ranges");
326 if (header != null) {
327 headerAcceptRanges = header.getValue();
329 header = response.getFirstHeader("Content-Disposition");
330 if (header != null) {
331 headerContentDisposition = header.getValue();
333 header = response.getFirstHeader("Content-Location");
334 if (header != null) {
335 headerContentLocation = header.getValue();
337 if (mimeType == null) {
338 header = response.getFirstHeader("Content-Type");
339 if (header != null) {
340 mimeType = header.getValue();
341 final int semicolonIndex = mimeType.indexOf(';');
342 if (semicolonIndex != -1) {
343 mimeType = mimeType.substring(0, semicolonIndex);
347 header = response.getFirstHeader("ETag");
348 if (header != null) {
349 headerETag = header.getValue();
351 header = response.getFirstHeader("Transfer-Encoding");
352 if (header != null) {
353 headerTransferEncoding = header.getValue();
355 if (headerTransferEncoding == null) {
356 header = response.getFirstHeader("Content-Length");
357 if (header != null) {
358 headerContentLength = header.getValue();
361 // Ignore content-length with transfer-encoding - 2616 4.4 3
362 if (Constants.LOGVV) {
364 "ignoring content-length because of xfer-encoding");
367 if (Constants.LOGVV) {
368 Log.v(Constants.TAG, "Accept-Ranges: " + headerAcceptRanges);
369 Log.v(Constants.TAG, "Content-Disposition: " +
370 headerContentDisposition);
371 Log.v(Constants.TAG, "Content-Length: " + headerContentLength);
372 Log.v(Constants.TAG, "Content-Location: " + headerContentLocation);
373 Log.v(Constants.TAG, "Content-Type: " + mimeType);
374 Log.v(Constants.TAG, "ETag: " + headerETag);
375 Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
378 if (!mInfo.noIntegrity && headerContentLength == null &&
379 (headerTransferEncoding == null
380 || !headerTransferEncoding.equalsIgnoreCase("chunked"))
383 Log.d(Constants.TAG, "can't know size of download, giving up");
385 finalStatus = Downloads.STATUS_LENGTH_REQUIRED;
387 break http_request_loop;
390 DownloadFileInfo fileInfo = Helpers.generateSaveFile(
394 headerContentDisposition,
395 headerContentLocation,
398 (headerContentLength != null) ?
399 Integer.parseInt(headerContentLength) : 0);
400 if (fileInfo.filename == null) {
401 finalStatus = fileInfo.status;
403 break http_request_loop;
405 filename = fileInfo.filename;
406 stream = fileInfo.stream;
407 if (Constants.LOGV) {
408 Log.v(Constants.TAG, "writing " + mInfo.uri + " to " + filename);
411 ContentValues values = new ContentValues();
412 values.put(Downloads._DATA, filename);
413 if (headerETag != null) {
414 values.put(Constants.ETAG, headerETag);
416 if (mimeType != null) {
417 values.put(Downloads.MIMETYPE, mimeType);
419 int contentLength = -1;
420 if (headerContentLength != null) {
421 contentLength = Integer.parseInt(headerContentLength);
423 values.put(Downloads.TOTAL_BYTES, contentLength);
424 mContext.getContentResolver().update(contentUri, values, null, null);
427 InputStream entityStream;
429 entityStream = response.getEntity().getContent();
430 } catch (IOException ex) {
431 if (!Helpers.isNetworkAvailable(mContext)) {
432 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
433 } else if (mInfo.numFailed < Constants.MAX_RETRIES) {
434 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
437 if (Constants.LOGV) {
438 Log.d(Constants.TAG, "IOException getting entity for " + mInfo.uri +
440 } else if (Config.LOGD) {
441 Log.d(Constants.TAG, "IOException getting entity for download " +
442 mInfo.id + " : " + ex);
444 finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
447 break http_request_loop;
452 bytesRead = entityStream.read(data);
453 } catch (IOException ex) {
454 ContentValues values = new ContentValues();
455 values.put(Downloads.CURRENT_BYTES, bytesSoFar);
456 mContext.getContentResolver().update(contentUri, values, null, null);
457 if (!mInfo.noIntegrity && headerETag == null) {
458 if (Constants.LOGV) {
459 Log.v(Constants.TAG, "download IOException for " + mInfo.uri +
461 } else if (Config.LOGD) {
462 Log.d(Constants.TAG, "download IOException for download " +
463 mInfo.id + " : " + ex);
467 "can't resume interrupted download with no ETag");
469 finalStatus = Downloads.STATUS_PRECONDITION_FAILED;
470 } else if (!Helpers.isNetworkAvailable(mContext)) {
471 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
472 } else if (mInfo.numFailed < Constants.MAX_RETRIES) {
473 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
476 if (Constants.LOGV) {
477 Log.v(Constants.TAG, "download IOException for " + mInfo.uri +
479 } else if (Config.LOGD) {
480 Log.d(Constants.TAG, "download IOException for download " +
481 mInfo.id + " : " + ex);
483 finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
486 break http_request_loop;
488 if (bytesRead == -1) { // success
489 ContentValues values = new ContentValues();
490 values.put(Downloads.CURRENT_BYTES, bytesSoFar);
491 if (headerContentLength == null) {
492 values.put(Downloads.TOTAL_BYTES, bytesSoFar);
494 mContext.getContentResolver().update(contentUri, values, null, null);
495 if ((headerContentLength != null)
497 != Integer.parseInt(headerContentLength))) {
498 if (!mInfo.noIntegrity && headerETag == null) {
499 if (Constants.LOGV) {
500 Log.d(Constants.TAG, "mismatched content length " +
502 } else if (Config.LOGD) {
503 Log.d(Constants.TAG, "mismatched content length for " +
506 finalStatus = Downloads.STATUS_LENGTH_REQUIRED;
507 } else if (!Helpers.isNetworkAvailable(mContext)) {
508 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
509 } else if (mInfo.numFailed < Constants.MAX_RETRIES) {
510 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
513 if (Constants.LOGV) {
514 Log.v(Constants.TAG, "closed socket for " + mInfo.uri);
515 } else if (Config.LOGD) {
516 Log.d(Constants.TAG, "closed socket for download " +
519 finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
521 break http_request_loop;
528 if (stream == null) {
529 stream = new FileOutputStream(filename, true);
531 stream.write(data, 0, bytesRead);
532 if (mInfo.destination == Downloads.DESTINATION_EXTERNAL
533 && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
534 .equalsIgnoreCase(mimeType)) {
538 } catch (IOException ex) {
539 if (Constants.LOGV) {
541 "exception when closing the file " +
542 "during download : " + ex);
544 // nothing can really be done if the file can't be closed
548 } catch (IOException ex) {
549 if (!Helpers.discardPurgeableFiles(
550 mContext, Constants.BUFFER_SIZE)) {
551 finalStatus = Downloads.STATUS_FILE_ERROR;
552 break http_request_loop;
556 bytesSoFar += bytesRead;
557 long now = System.currentTimeMillis();
558 if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP
559 && now - timeLastNotification
560 > Constants.MIN_PROGRESS_TIME) {
561 ContentValues values = new ContentValues();
562 values.put(Downloads.CURRENT_BYTES, bytesSoFar);
563 mContext.getContentResolver().update(
564 contentUri, values, null, null);
565 bytesNotified = bytesSoFar;
566 timeLastNotification = now;
569 if (Constants.LOGVV) {
570 Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.uri);
572 synchronized(mInfo) {
573 if (mInfo.control == Downloads.CONTROL_PAUSED) {
574 if (Constants.LOGV) {
575 Log.v(Constants.TAG, "paused " + mInfo.uri);
577 finalStatus = Downloads.STATUS_RUNNING_PAUSED;
579 break http_request_loop;
582 if (mInfo.status == Downloads.STATUS_CANCELED) {
583 if (Constants.LOGV) {
584 Log.d(Constants.TAG, "canceled " + mInfo.uri);
585 } else if (Config.LOGD) {
586 // Log.d(Constants.TAG, "canceled id " + mInfo.id);
588 finalStatus = Downloads.STATUS_CANCELED;
589 break http_request_loop;
592 if (Constants.LOGV) {
593 Log.v(Constants.TAG, "download completed for " + mInfo.uri);
595 finalStatus = Downloads.STATUS_SUCCESS;
599 } catch (FileNotFoundException ex) {
601 Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " + ex);
603 finalStatus = Downloads.STATUS_FILE_ERROR;
604 // falls through to the code that reports an error
605 } catch (Exception ex) { //sometimes the socket code throws unchecked exceptions
606 if (Constants.LOGV) {
607 Log.d(Constants.TAG, "Exception for " + mInfo.uri, ex);
608 } else if (Config.LOGD) {
609 Log.d(Constants.TAG, "Exception for id " + mInfo.id, ex);
611 finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
612 // falls through to the code that reports an error
614 mInfo.hasActiveThread = false;
615 if (wakeLock != null) {
619 if (client != null) {
625 if (stream != null) {
628 } catch (IOException ex) {
629 if (Constants.LOGV) {
630 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
632 // nothing can really be done if the file can't be closed
634 if (filename != null) {
635 // if the download wasn't successful, delete the file
636 if (Downloads.isStatusError(finalStatus)) {
637 new File(filename).delete();
639 } else if (Downloads.isStatusSuccess(finalStatus) &&
640 DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
641 .equalsIgnoreCase(mimeType)) {
642 // transfer the file to the DRM content provider
643 File file = new File(filename);
644 Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
646 Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider");
647 finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
649 filename = item.getDataString();
650 mimeType = item.getType();
654 } else if (Downloads.isStatusSuccess(finalStatus)) {
655 // make sure the file is readable
656 FileUtils.setPermissions(filename, 0644, -1, -1);
659 notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount,
660 gotData, filename, newUri, mimeType);
665 * Stores information about the completed download, and notifies the initiating application.
667 private void notifyDownloadCompleted(
668 int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
669 String filename, String uri, String mimeType) {
670 notifyThroughDatabase(
671 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
672 if (Downloads.isStatusCompleted(status)) {
673 notifyThroughIntent();
677 private void notifyThroughDatabase(
678 int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
679 String filename, String uri, String mimeType) {
680 ContentValues values = new ContentValues();
681 values.put(Downloads.STATUS, status);
682 values.put(Downloads._DATA, filename);
684 values.put(Downloads.URI, uri);
686 values.put(Downloads.MIMETYPE, mimeType);
687 values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
688 values.put(Constants.RETRY_AFTER___REDIRECT_COUNT, retryAfter + (redirectCount << 28));
690 values.put(Constants.FAILED_CONNECTIONS, 0);
691 } else if (gotData) {
692 values.put(Constants.FAILED_CONNECTIONS, 1);
694 values.put(Constants.FAILED_CONNECTIONS, mInfo.numFailed + 1);
697 mContext.getContentResolver().update(
698 ContentUris.withAppendedId(Downloads.CONTENT_URI, mInfo.id), values, null, null);
702 * Notifies the initiating app if it requested it. That way, it can know that the
703 * download completed even if it's not actively watching the cursor.
705 private void notifyThroughIntent() {
706 Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + mInfo.id);
707 mInfo.sendIntentIfRequested(uri, mContext);