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.Header;
20 import org.apache.http.HttpResponse;
21 import org.apache.http.client.methods.HttpGet;
22 import org.apache.http.client.HttpClient;
23 import org.apache.http.entity.StringEntity;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.drm.mobile1.DrmRawContent;
30 import android.net.Uri;
31 import android.net.http.AndroidHttpClient;
32 import android.os.FileUtils;
33 import android.os.PowerManager;
34 import android.os.Process;
35 import android.provider.Downloads;
36 import android.provider.DrmStore;
37 import android.util.Config;
38 import android.util.Log;
42 import java.io.FileDescriptor;
43 import java.io.FileNotFoundException;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.SyncFailedException;
49 import java.net.URISyntaxException;
50 import java.util.Locale;
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.mUserAgent;
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.Impl.STATUS_UNKNOWN_ERROR;
85 boolean countRetry = false;
87 int redirectCount = mInfo.mRedirectCount;
89 boolean gotData = false;
90 String filename = null;
91 String mimeType = sanitizeMimeType(mInfo.mMimeType);
92 FileOutputStream stream = null;
93 AndroidHttpClient client = null;
94 PowerManager.WakeLock wakeLock = null;
95 Uri contentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
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.mFileName;
115 if (filename != null) {
116 if (!Helpers.isFilenameValid(filename)) {
117 finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
118 notifyDownloadCompleted(
119 finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
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.mETag == null && !mInfo.mNoIntegrity) {
131 // Tough luck, that's not a resumable download
134 "can't resume interrupted non-resumable download");
137 finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
138 notifyDownloadCompleted(
139 finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
142 // All right, we'll be able to resume this download
143 stream = new FileOutputStream(filename, true);
144 bytesSoFar = (int) fileLength;
145 if (mInfo.mTotalBytes != -1) {
146 headerContentLength = Integer.toString(mInfo.mTotalBytes);
148 headerETag = mInfo.mETag;
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(), mContext);
161 if (stream != null && mInfo.mDestination == Downloads.Impl.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.mUri);
186 if (Constants.LOGV) {
187 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
190 if (mInfo.mCookies != null) {
191 request.addHeader("Cookie", mInfo.mCookies);
193 if (mInfo.mReferer != null) {
194 request.addHeader("Referer", mInfo.mReferer);
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.mUri + " : " + ex);
210 } else if (Config.LOGD) {
211 Log.d(Constants.TAG, "Arg exception trying to execute request for " +
212 mInfo.mId + " : " + ex);
214 finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
216 break http_request_loop;
217 } catch (IOException ex) {
218 if (Constants.LOGX) {
219 if (Helpers.isNetworkAvailable(mContext)) {
220 Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Up");
222 Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Down");
225 if (!Helpers.isNetworkAvailable(mContext)) {
226 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
227 } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
228 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
231 if (Constants.LOGV) {
232 Log.d(Constants.TAG, "IOException trying to execute request for " +
233 mInfo.mUri + " : " + ex);
234 } else if (Config.LOGD) {
235 Log.d(Constants.TAG, "IOException trying to execute request for " +
236 mInfo.mId + " : " + ex);
238 finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
241 break http_request_loop;
244 int statusCode = response.getStatusLine().getStatusCode();
245 if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
246 if (Constants.LOGVV) {
247 Log.v(Constants.TAG, "got HTTP response code 503");
249 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
251 Header header = response.getFirstHeader("Retry-After");
252 if (header != null) {
254 if (Constants.LOGVV) {
255 Log.v(Constants.TAG, "Retry-After :" + header.getValue());
257 retryAfter = Integer.parseInt(header.getValue());
258 if (retryAfter < 0) {
261 if (retryAfter < Constants.MIN_RETRY_AFTER) {
262 retryAfter = Constants.MIN_RETRY_AFTER;
263 } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
264 retryAfter = Constants.MAX_RETRY_AFTER;
266 retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
269 } catch (NumberFormatException ex) {
270 // ignored - retryAfter stays 0 in this case.
274 break http_request_loop;
276 if (statusCode == 301 ||
280 if (Constants.LOGVV) {
281 Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
283 if (redirectCount >= Constants.MAX_REDIRECTS) {
284 if (Constants.LOGV) {
285 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
286 " at " + mInfo.mUri);
287 } else if (Config.LOGD) {
288 Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
290 finalStatus = Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
292 break http_request_loop;
294 Header header = response.getFirstHeader("Location");
295 if (header != null) {
296 if (Constants.LOGVV) {
297 Log.v(Constants.TAG, "Location :" + header.getValue());
300 newUri = new URI(mInfo.mUri).
301 resolve(new URI(header.getValue())).
303 } catch(URISyntaxException ex) {
304 if (Constants.LOGV) {
306 "Couldn't resolve redirect URI " +
310 } else if (Config.LOGD) {
312 "Couldn't resolve redirect URI for download " +
315 finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
317 break http_request_loop;
320 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
322 break http_request_loop;
325 if ((!continuingDownload && statusCode != Downloads.Impl.STATUS_SUCCESS)
326 || (continuingDownload && statusCode != 206)) {
327 if (Constants.LOGV) {
328 Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
329 } else if (Config.LOGD) {
330 Log.d(Constants.TAG, "http error " + statusCode + " for download " +
333 if (Downloads.Impl.isStatusError(statusCode)) {
334 finalStatus = statusCode;
335 } else if (statusCode >= 300 && statusCode < 400) {
336 finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
337 } else if (continuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
338 finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
340 finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
343 break http_request_loop;
345 // Handles the response, saves the file
346 if (Constants.LOGV) {
347 Log.v(Constants.TAG, "received response for " + mInfo.mUri);
350 if (!continuingDownload) {
351 Header header = response.getFirstHeader("Accept-Ranges");
352 if (header != null) {
353 headerAcceptRanges = header.getValue();
355 header = response.getFirstHeader("Content-Disposition");
356 if (header != null) {
357 headerContentDisposition = header.getValue();
359 header = response.getFirstHeader("Content-Location");
360 if (header != null) {
361 headerContentLocation = header.getValue();
363 if (mimeType == null) {
364 header = response.getFirstHeader("Content-Type");
365 if (header != null) {
366 mimeType = sanitizeMimeType(header.getValue());
369 header = response.getFirstHeader("ETag");
370 if (header != null) {
371 headerETag = header.getValue();
373 header = response.getFirstHeader("Transfer-Encoding");
374 if (header != null) {
375 headerTransferEncoding = header.getValue();
377 if (headerTransferEncoding == null) {
378 header = response.getFirstHeader("Content-Length");
379 if (header != null) {
380 headerContentLength = header.getValue();
383 // Ignore content-length with transfer-encoding - 2616 4.4 3
384 if (Constants.LOGVV) {
386 "ignoring content-length because of xfer-encoding");
389 if (Constants.LOGVV) {
390 Log.v(Constants.TAG, "Accept-Ranges: " + headerAcceptRanges);
391 Log.v(Constants.TAG, "Content-Disposition: " +
392 headerContentDisposition);
393 Log.v(Constants.TAG, "Content-Length: " + headerContentLength);
394 Log.v(Constants.TAG, "Content-Location: " + headerContentLocation);
395 Log.v(Constants.TAG, "Content-Type: " + mimeType);
396 Log.v(Constants.TAG, "ETag: " + headerETag);
397 Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
400 if (!mInfo.mNoIntegrity && headerContentLength == null &&
401 (headerTransferEncoding == null
402 || !headerTransferEncoding.equalsIgnoreCase("chunked"))
405 Log.d(Constants.TAG, "can't know size of download, giving up");
407 finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
409 break http_request_loop;
412 DownloadFileInfo fileInfo = Helpers.generateSaveFile(
416 headerContentDisposition,
417 headerContentLocation,
420 (headerContentLength != null) ?
421 Integer.parseInt(headerContentLength) : 0);
422 if (fileInfo.mFileName == null) {
423 finalStatus = fileInfo.mStatus;
425 break http_request_loop;
427 filename = fileInfo.mFileName;
428 stream = fileInfo.mStream;
429 if (Constants.LOGV) {
430 Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + filename);
433 ContentValues values = new ContentValues();
434 values.put(Downloads.Impl._DATA, filename);
435 if (headerETag != null) {
436 values.put(Constants.ETAG, headerETag);
438 if (mimeType != null) {
439 values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
441 int contentLength = -1;
442 if (headerContentLength != null) {
443 contentLength = Integer.parseInt(headerContentLength);
445 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, contentLength);
446 mContext.getContentResolver().update(contentUri, values, null, null);
449 InputStream entityStream;
451 entityStream = response.getEntity().getContent();
452 } catch (IOException ex) {
453 if (Constants.LOGX) {
454 if (Helpers.isNetworkAvailable(mContext)) {
455 Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Up");
457 Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Down");
460 if (!Helpers.isNetworkAvailable(mContext)) {
461 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
462 } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
463 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
466 if (Constants.LOGV) {
468 "IOException getting entity for " +
472 } else if (Config.LOGD) {
473 Log.d(Constants.TAG, "IOException getting entity for download " +
474 mInfo.mId + " : " + ex);
476 finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
479 break http_request_loop;
484 bytesRead = entityStream.read(data);
485 } catch (IOException ex) {
486 if (Constants.LOGX) {
487 if (Helpers.isNetworkAvailable(mContext)) {
488 Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Up");
490 Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Down");
493 ContentValues values = new ContentValues();
494 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
495 mContext.getContentResolver().update(contentUri, values, null, null);
496 if (!mInfo.mNoIntegrity && headerETag == null) {
497 if (Constants.LOGV) {
498 Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
500 } else if (Config.LOGD) {
501 Log.d(Constants.TAG, "download IOException for download " +
502 mInfo.mId + " : " + ex);
506 "can't resume interrupted download with no ETag");
508 finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
509 } else if (!Helpers.isNetworkAvailable(mContext)) {
510 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
511 } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
512 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
515 if (Constants.LOGV) {
516 Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
518 } else if (Config.LOGD) {
519 Log.d(Constants.TAG, "download IOException for download " +
520 mInfo.mId + " : " + ex);
522 finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
525 break http_request_loop;
527 if (bytesRead == -1) { // success
528 ContentValues values = new ContentValues();
529 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
530 if (headerContentLength == null) {
531 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, bytesSoFar);
533 mContext.getContentResolver().update(contentUri, values, null, null);
534 if ((headerContentLength != null)
536 != Integer.parseInt(headerContentLength))) {
537 if (!mInfo.mNoIntegrity && headerETag == null) {
538 if (Constants.LOGV) {
539 Log.d(Constants.TAG, "mismatched content length " +
541 } else if (Config.LOGD) {
542 Log.d(Constants.TAG, "mismatched content length for " +
545 finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
546 } else if (!Helpers.isNetworkAvailable(mContext)) {
547 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
548 } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
549 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
552 if (Constants.LOGV) {
553 Log.v(Constants.TAG, "closed socket for " + mInfo.mUri);
554 } else if (Config.LOGD) {
555 Log.d(Constants.TAG, "closed socket for download " +
558 finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
560 break http_request_loop;
567 if (stream == null) {
568 stream = new FileOutputStream(filename, true);
570 stream.write(data, 0, bytesRead);
571 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
572 && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
573 .equalsIgnoreCase(mimeType)) {
577 } catch (IOException ex) {
578 if (Constants.LOGV) {
580 "exception when closing the file " +
581 "during download : " + ex);
583 // nothing can really be done if the file can't be closed
587 } catch (IOException ex) {
588 if (!Helpers.discardPurgeableFiles(
589 mContext, Constants.BUFFER_SIZE)) {
590 finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
591 break http_request_loop;
595 bytesSoFar += bytesRead;
596 long now = System.currentTimeMillis();
597 if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP
598 && now - timeLastNotification
599 > Constants.MIN_PROGRESS_TIME) {
600 ContentValues values = new ContentValues();
601 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
602 mContext.getContentResolver().update(
603 contentUri, values, null, null);
604 bytesNotified = bytesSoFar;
605 timeLastNotification = now;
608 if (Constants.LOGVV) {
609 Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.mUri);
611 synchronized (mInfo) {
612 if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
613 if (Constants.LOGV) {
614 Log.v(Constants.TAG, "paused " + mInfo.mUri);
616 finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
618 break http_request_loop;
621 if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
622 if (Constants.LOGV) {
623 Log.d(Constants.TAG, "canceled " + mInfo.mUri);
624 } else if (Config.LOGD) {
625 // Log.d(Constants.TAG, "canceled id " + mInfo.mId);
627 finalStatus = Downloads.Impl.STATUS_CANCELED;
628 break http_request_loop;
631 if (Constants.LOGV) {
632 Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
634 finalStatus = Downloads.Impl.STATUS_SUCCESS;
638 } catch (FileNotFoundException ex) {
640 Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " + ex);
642 finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
643 // falls through to the code that reports an error
644 } catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
645 if (Constants.LOGV) {
646 Log.d(Constants.TAG, "Exception for " + mInfo.mUri, ex);
647 } else if (Config.LOGD) {
648 Log.d(Constants.TAG, "Exception for id " + mInfo.mId, ex);
650 finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
651 // falls through to the code that reports an error
653 mInfo.mHasActiveThread = false;
654 if (wakeLock != null) {
658 if (client != null) {
664 if (stream != null) {
667 } catch (IOException ex) {
668 if (Constants.LOGV) {
669 Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
671 // nothing can really be done if the file can't be closed
673 if (filename != null) {
674 // if the download wasn't successful, delete the file
675 if (Downloads.Impl.isStatusError(finalStatus)) {
676 new File(filename).delete();
678 } else if (Downloads.Impl.isStatusSuccess(finalStatus) &&
679 DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
680 .equalsIgnoreCase(mimeType)) {
681 // transfer the file to the DRM content provider
682 File file = new File(filename);
683 Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
685 Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider");
686 finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
688 filename = item.getDataString();
689 mimeType = item.getType();
693 } else if (Downloads.Impl.isStatusSuccess(finalStatus)) {
694 // make sure the file is readable
695 FileUtils.setPermissions(filename, 0644, -1, -1);
697 // Sync to storage after completion
699 new FileOutputStream(filename, true).getFD().sync();
700 } catch (FileNotFoundException ex) {
701 Log.w(Constants.TAG, "file " + filename + " not found: " + ex);
702 } catch (SyncFailedException ex) {
703 Log.w(Constants.TAG, "file " + filename + " sync failed: " + ex);
704 } catch (IOException ex) {
705 Log.w(Constants.TAG, "IOException trying to sync " + filename + ": " + ex);
706 } catch (RuntimeException ex) {
707 Log.w(Constants.TAG, "exception while syncing file: ", ex);
711 notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount,
712 gotData, filename, newUri, mimeType);
717 * Stores information about the completed download, and notifies the initiating application.
719 private void notifyDownloadCompleted(
720 int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
721 String filename, String uri, String mimeType) {
722 notifyThroughDatabase(
723 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
724 if (Downloads.Impl.isStatusCompleted(status)) {
725 notifyThroughIntent();
729 private void notifyThroughDatabase(
730 int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
731 String filename, String uri, String mimeType) {
732 ContentValues values = new ContentValues();
733 values.put(Downloads.Impl.COLUMN_STATUS, status);
734 values.put(Downloads.Impl._DATA, filename);
736 values.put(Downloads.Impl.COLUMN_URI, uri);
738 values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
739 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, System.currentTimeMillis());
740 values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
742 values.put(Constants.FAILED_CONNECTIONS, 0);
743 } else if (gotData) {
744 values.put(Constants.FAILED_CONNECTIONS, 1);
746 values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
749 mContext.getContentResolver().update(ContentUris.withAppendedId(
750 Downloads.Impl.CONTENT_URI, mInfo.mId), values, null, null);
754 * Notifies the initiating app if it requested it. That way, it can know that the
755 * download completed even if it's not actively watching the cursor.
757 private void notifyThroughIntent() {
758 Uri uri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
759 mInfo.sendIntentIfRequested(uri, mContext);
763 * Clean up a mimeType string so it can be used to dispatch an intent to
764 * view a downloaded asset.
765 * @param mimeType either null or one or more mime types (semi colon separated).
766 * @return null if mimeType was null. Otherwise a string which represents a
767 * single mimetype in lowercase and with surrounding whitespaces trimmed.
769 private String sanitizeMimeType(String mimeType) {
771 mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
773 final int semicolonIndex = mimeType.indexOf(';');
774 if (semicolonIndex != -1) {
775 mimeType = mimeType.substring(0, semicolonIndex);
778 } catch (NullPointerException npe) {