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.provider.Downloads.Impl.COLUMN_CONTROL; 20 import static android.provider.Downloads.Impl.COLUMN_DELETED; 21 import static android.provider.Downloads.Impl.COLUMN_STATUS; 22 import static android.provider.Downloads.Impl.CONTROL_PAUSED; 23 import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST; 24 import static android.provider.Downloads.Impl.STATUS_CANCELED; 25 import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME; 26 import static android.provider.Downloads.Impl.STATUS_FILE_ERROR; 27 import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR; 28 import static android.provider.Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR; 29 import static android.provider.Downloads.Impl.STATUS_PAUSED_BY_APP; 30 import static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 31 import static android.provider.Downloads.Impl.STATUS_RUNNING; 32 import static android.provider.Downloads.Impl.STATUS_SUCCESS; 33 import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS; 34 import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; 35 import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR; 36 import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK; 37 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY; 38 import static android.text.format.DateUtils.SECOND_IN_MILLIS; 39 40 import static com.android.providers.downloads.Constants.TAG; 41 42 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; 43 import static java.net.HttpURLConnection.HTTP_MOVED_PERM; 44 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; 45 import static java.net.HttpURLConnection.HTTP_OK; 46 import static java.net.HttpURLConnection.HTTP_PARTIAL; 47 import static java.net.HttpURLConnection.HTTP_PRECON_FAILED; 48 import static java.net.HttpURLConnection.HTTP_SEE_OTHER; 49 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; 50 51 import android.app.job.JobParameters; 52 import android.content.ContentValues; 53 import android.content.Context; 54 import android.content.Intent; 55 import android.drm.DrmManagerClient; 56 import android.drm.DrmOutputStream; 57 import android.net.ConnectivityManager; 58 import android.net.INetworkPolicyListener; 59 import android.net.Network; 60 import android.net.NetworkInfo; 61 import android.net.NetworkPolicyManager; 62 import android.net.TrafficStats; 63 import android.net.Uri; 64 import android.os.ParcelFileDescriptor; 65 import android.os.Process; 66 import android.os.SystemClock; 67 import android.os.storage.StorageManager; 68 import android.provider.Downloads; 69 import android.system.ErrnoException; 70 import android.system.Os; 71 import android.system.OsConstants; 72 import android.util.Log; 73 import android.util.MathUtils; 74 import android.util.Pair; 75 76 import libcore.io.IoUtils; 77 78 import java.io.File; 79 import java.io.FileDescriptor; 80 import java.io.FileNotFoundException; 81 import java.io.IOException; 82 import java.io.InputStream; 83 import java.io.OutputStream; 84 import java.net.HttpURLConnection; 85 import java.net.MalformedURLException; 86 import java.net.ProtocolException; 87 import java.net.URL; 88 import java.net.URLConnection; 89 import java.security.GeneralSecurityException; 90 91 import javax.net.ssl.HttpsURLConnection; 92 import javax.net.ssl.SSLContext; 93 94 /** 95 * Task which executes a given {@link DownloadInfo}: making network requests, 96 * persisting data to disk, and updating {@link DownloadProvider}. 97 * <p> 98 * To know if a download is successful, we need to know either the final content 99 * length to expect, or the transfer to be chunked. To resume an interrupted 100 * download, we need an ETag. 101 * <p> 102 * Failed network requests are retried several times before giving up. Local 103 * disk errors fail immediately and are not retried. 104 */ 105 public class DownloadThread extends Thread { 106 107 // TODO: bind each download to a specific network interface to avoid state 108 // checking races once we have ConnectivityManager API 109 110 // TODO: add support for saving to content:// 111 112 private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; 113 private static final int HTTP_TEMP_REDIRECT = 307; 114 115 private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS); 116 117 private final Context mContext; 118 private final SystemFacade mSystemFacade; 119 private final DownloadNotifier mNotifier; 120 private final NetworkPolicyManager mNetworkPolicy; 121 private final StorageManager mStorage; 122 123 private final DownloadJobService mJobService; 124 private final JobParameters mParams; 125 126 private final long mId; 127 128 /** 129 * Info object that should be treated as read-only. Any potentially mutated 130 * fields are tracked in {@link #mInfoDelta}. If a field exists in 131 * {@link #mInfoDelta}, it must not be read from {@link #mInfo}. 132 */ 133 private final DownloadInfo mInfo; 134 private final DownloadInfoDelta mInfoDelta; 135 136 private volatile boolean mPolicyDirty; 137 138 /** 139 * Local changes to {@link DownloadInfo}. These are kept local to avoid 140 * racing with the thread that updates based on change notifications. 141 */ 142 private class DownloadInfoDelta { 143 public String mUri; 144 public String mFileName; 145 public String mMimeType; 146 public int mStatus; 147 public int mNumFailed; 148 public int mRetryAfter; 149 public long mTotalBytes; 150 public long mCurrentBytes; 151 public String mETag; 152 153 public String mErrorMsg; 154 155 private static final String NOT_CANCELED = COLUMN_STATUS + " != '" + STATUS_CANCELED + "'"; 156 private static final String NOT_DELETED = COLUMN_DELETED + " == '0'"; 157 private static final String NOT_PAUSED = "(" + COLUMN_CONTROL + " IS NULL OR " 158 + COLUMN_CONTROL + " != '" + CONTROL_PAUSED + "')"; 159 160 private static final String SELECTION_VALID = NOT_CANCELED + " AND " + NOT_DELETED + " AND " 161 + NOT_PAUSED; 162 163 public DownloadInfoDelta(DownloadInfo info) { 164 mUri = info.mUri; 165 mFileName = info.mFileName; 166 mMimeType = info.mMimeType; 167 mStatus = info.mStatus; 168 mNumFailed = info.mNumFailed; 169 mRetryAfter = info.mRetryAfter; 170 mTotalBytes = info.mTotalBytes; 171 mCurrentBytes = info.mCurrentBytes; 172 mETag = info.mETag; 173 } 174 175 private ContentValues buildContentValues() { 176 final ContentValues values = new ContentValues(); 177 178 values.put(Downloads.Impl.COLUMN_URI, mUri); 179 values.put(Downloads.Impl._DATA, mFileName); 180 values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType); 181 values.put(Downloads.Impl.COLUMN_STATUS, mStatus); 182 values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed); 183 values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter); 184 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes); 185 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes); 186 values.put(Constants.ETAG, mETag); 187 188 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis()); 189 values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg); 190 191 return values; 192 } 193 194 /** 195 * Blindly push update of current delta values to provider. 196 */ 197 public void writeToDatabase() { 198 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(), 199 null, null); 200 } 201 202 /** 203 * Push update of current delta values to provider, asserting strongly 204 * that we haven't been paused or deleted. 205 */ 206 public void writeToDatabaseOrThrow() throws StopRequestException { 207 if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), 208 buildContentValues(), SELECTION_VALID, null) == 0) { 209 if (mInfo.queryDownloadControl() == CONTROL_PAUSED) { 210 throw new StopRequestException(STATUS_PAUSED_BY_APP, "Download paused!"); 211 } else { 212 throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!"); 213 } 214 } 215 } 216 } 217 218 /** 219 * Flag indicating if we've made forward progress transferring file data 220 * from a remote server. 221 */ 222 private boolean mMadeProgress = false; 223 224 /** 225 * Details from the last time we pushed a database update. 226 */ 227 private long mLastUpdateBytes = 0; 228 private long mLastUpdateTime = 0; 229 230 private boolean mIgnoreBlocked; 231 private Network mNetwork; 232 233 private int mNetworkType = ConnectivityManager.TYPE_NONE; 234 235 /** Historical bytes/second speed of this download. */ 236 private long mSpeed; 237 /** Time when current sample started. */ 238 private long mSpeedSampleStart; 239 /** Bytes transferred since current sample started. */ 240 private long mSpeedSampleBytes; 241 242 /** Flag indicating that thread must be halted */ 243 private volatile boolean mShutdownRequested; 244 245 public DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info) { 246 mContext = service; 247 mSystemFacade = Helpers.getSystemFacade(mContext); 248 mNotifier = Helpers.getDownloadNotifier(mContext); 249 mNetworkPolicy = mContext.getSystemService(NetworkPolicyManager.class); 250 mStorage = mContext.getSystemService(StorageManager.class); 251 252 mJobService = service; 253 mParams = params; 254 255 mId = info.mId; 256 mInfo = info; 257 mInfoDelta = new DownloadInfoDelta(info); 258 } 259 260 @Override 261 public void run() { 262 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 263 264 // Skip when download already marked as finished; this download was 265 // probably started again while racing with UpdateThread. 266 if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) { 267 logDebug("Already finished; skipping"); 268 return; 269 } 270 271 try { 272 // while performing download, register for rules updates 273 mNetworkPolicy.registerListener(mPolicyListener); 274 275 logDebug("Starting"); 276 277 mInfoDelta.mStatus = STATUS_RUNNING; 278 mInfoDelta.writeToDatabase(); 279 280 // If we're showing a foreground notification for the requesting 281 // app, the download isn't affected by the blocked status of the 282 // requesting app 283 mIgnoreBlocked = mInfo.isVisible(); 284 285 // Use the caller's default network to make this connection, since 286 // they might be subject to restrictions that we shouldn't let them 287 // circumvent 288 mNetwork = mSystemFacade.getActiveNetwork(mInfo.mUid, mIgnoreBlocked); 289 if (mNetwork == null) { 290 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, 291 "No network associated with requesting UID"); 292 } 293 294 // Remember which network this download started on; used to 295 // determine if errors were due to network changes. 296 final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid, 297 mIgnoreBlocked); 298 if (info != null) { 299 mNetworkType = info.getType(); 300 } 301 302 // Network traffic on this thread should be counted against the 303 // requesting UID, and is tagged with well-known value. 304 TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD); 305 TrafficStats.setThreadStatsUid(mInfo.mUid); 306 307 executeDownload(); 308 309 mInfoDelta.mStatus = STATUS_SUCCESS; 310 TrafficStats.incrementOperationCount(1); 311 312 // If we just finished a chunked file, record total size 313 if (mInfoDelta.mTotalBytes == -1) { 314 mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes; 315 } 316 317 } catch (StopRequestException e) { 318 mInfoDelta.mStatus = e.getFinalStatus(); 319 mInfoDelta.mErrorMsg = e.getMessage(); 320 321 logWarning("Stop requested with status " 322 + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": " 323 + mInfoDelta.mErrorMsg); 324 325 // Nobody below our level should request retries, since we handle 326 // failure counts at this level. 327 if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) { 328 throw new IllegalStateException("Execution should always throw final error codes"); 329 } 330 331 // Some errors should be retryable, unless we fail too many times. 332 if (isStatusRetryable(mInfoDelta.mStatus)) { 333 if (mMadeProgress) { 334 mInfoDelta.mNumFailed = 1; 335 } else { 336 mInfoDelta.mNumFailed += 1; 337 } 338 339 if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) { 340 final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid, 341 mIgnoreBlocked); 342 if (info != null && info.getType() == mNetworkType && info.isConnected()) { 343 // Underlying network is still intact, use normal backoff 344 mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY; 345 } else { 346 // Network changed, retry on any next available 347 mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK; 348 } 349 350 if ((mInfoDelta.mETag == null && mMadeProgress) 351 || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) { 352 // However, if we wrote data and have no ETag to verify 353 // contents against later, we can't actually resume. 354 mInfoDelta.mStatus = STATUS_CANNOT_RESUME; 355 } 356 } 357 } 358 359 // If we're waiting for a network that must be unmetered, our status 360 // is actually queued so we show relevant notifications 361 if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK 362 && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) { 363 mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI; 364 } 365 366 } catch (Throwable t) { 367 mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR; 368 mInfoDelta.mErrorMsg = t.toString(); 369 370 logError("Failed: " + mInfoDelta.mErrorMsg, t); 371 372 } finally { 373 logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus)); 374 375 mNotifier.notifyDownloadSpeed(mId, 0); 376 377 finalizeDestination(); 378 379 mInfoDelta.writeToDatabase(); 380 381 TrafficStats.clearThreadStatsTag(); 382 TrafficStats.clearThreadStatsUid(); 383 384 mNetworkPolicy.unregisterListener(mPolicyListener); 385 } 386 387 if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) { 388 if (mInfo.shouldScanFile(mInfoDelta.mStatus)) { 389 DownloadScanner.requestScanBlocking(mContext, mInfo.mId, mInfoDelta.mFileName, 390 mInfoDelta.mMimeType); 391 } 392 } else if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY 393 || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK 394 || mInfoDelta.mStatus == STATUS_QUEUED_FOR_WIFI) { 395 Helpers.scheduleJob(mContext, DownloadInfo.queryDownloadInfo(mContext, mId)); 396 } 397 398 mJobService.jobFinishedInternal(mParams, false); 399 } 400 401 public void requestShutdown() { 402 mShutdownRequested = true; 403 } 404 405 /** 406 * Fully execute a single download request. Setup and send the request, 407 * handle the response, and transfer the data to the destination file. 408 */ 409 private void executeDownload() throws StopRequestException { 410 final boolean resuming = mInfoDelta.mCurrentBytes != 0; 411 412 URL url; 413 try { 414 // TODO: migrate URL sanity checking into client side of API 415 url = new URL(mInfoDelta.mUri); 416 } catch (MalformedURLException e) { 417 throw new StopRequestException(STATUS_BAD_REQUEST, e); 418 } 419 420 boolean cleartextTrafficPermitted = mSystemFacade.isCleartextTrafficPermitted(mInfo.mUid); 421 SSLContext appContext; 422 try { 423 appContext = mSystemFacade.getSSLContextForPackage(mContext, mInfo.mPackage); 424 } catch (GeneralSecurityException e) { 425 // This should never happen. 426 throw new StopRequestException(STATUS_UNKNOWN_ERROR, "Unable to create SSLContext."); 427 } 428 int redirectionCount = 0; 429 while (redirectionCount++ < Constants.MAX_REDIRECTS) { 430 // Enforce the cleartext traffic opt-out for the UID. This cannot be enforced earlier 431 // because of HTTP redirects which can change the protocol between HTTP and HTTPS. 432 if ((!cleartextTrafficPermitted) && ("http".equalsIgnoreCase(url.getProtocol()))) { 433 throw new StopRequestException(STATUS_BAD_REQUEST, 434 "Cleartext traffic not permitted for UID " + mInfo.mUid + ": " 435 + Uri.parse(url.toString()).toSafeString()); 436 } 437 438 // Open connection and follow any redirects until we have a useful 439 // response with body. 440 HttpURLConnection conn = null; 441 try { 442 // Check that the caller is allowed to make network connections. If so, make one on 443 // their behalf to open the url. 444 checkConnectivity(); 445 conn = (HttpURLConnection) mNetwork.openConnection(url); 446 conn.setInstanceFollowRedirects(false); 447 conn.setConnectTimeout(DEFAULT_TIMEOUT); 448 conn.setReadTimeout(DEFAULT_TIMEOUT); 449 // If this is going over HTTPS configure the trust to be the same as the calling 450 // package. 451 if (conn instanceof HttpsURLConnection) { 452 ((HttpsURLConnection)conn).setSSLSocketFactory(appContext.getSocketFactory()); 453 } 454 455 addRequestHeaders(conn, resuming); 456 457 final int responseCode = conn.getResponseCode(); 458 switch (responseCode) { 459 case HTTP_OK: 460 if (resuming) { 461 throw new StopRequestException( 462 STATUS_CANNOT_RESUME, "Expected partial, but received OK"); 463 } 464 parseOkHeaders(conn); 465 transferData(conn); 466 return; 467 468 case HTTP_PARTIAL: 469 if (!resuming) { 470 throw new StopRequestException( 471 STATUS_CANNOT_RESUME, "Expected OK, but received partial"); 472 } 473 transferData(conn); 474 return; 475 476 case HTTP_MOVED_PERM: 477 case HTTP_MOVED_TEMP: 478 case HTTP_SEE_OTHER: 479 case HTTP_TEMP_REDIRECT: 480 final String location = conn.getHeaderField("Location"); 481 url = new URL(url, location); 482 if (responseCode == HTTP_MOVED_PERM) { 483 // Push updated URL back to database 484 mInfoDelta.mUri = url.toString(); 485 } 486 continue; 487 488 case HTTP_PRECON_FAILED: 489 throw new StopRequestException( 490 STATUS_CANNOT_RESUME, "Precondition failed"); 491 492 case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: 493 throw new StopRequestException( 494 STATUS_CANNOT_RESUME, "Requested range not satisfiable"); 495 496 case HTTP_UNAVAILABLE: 497 parseUnavailableHeaders(conn); 498 throw new StopRequestException( 499 HTTP_UNAVAILABLE, conn.getResponseMessage()); 500 501 case HTTP_INTERNAL_ERROR: 502 throw new StopRequestException( 503 HTTP_INTERNAL_ERROR, conn.getResponseMessage()); 504 505 default: 506 StopRequestException.throwUnhandledHttpError( 507 responseCode, conn.getResponseMessage()); 508 } 509 510 } catch (IOException e) { 511 if (e instanceof ProtocolException 512 && e.getMessage().startsWith("Unexpected status line")) { 513 throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e); 514 } else { 515 // Trouble with low-level sockets 516 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 517 } 518 519 } finally { 520 if (conn != null) conn.disconnect(); 521 } 522 } 523 524 throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects"); 525 } 526 527 /** 528 * Transfer data from the given connection to the destination file. 529 */ 530 private void transferData(HttpURLConnection conn) throws StopRequestException { 531 532 // To detect when we're really finished, we either need a length, closed 533 // connection, or chunked encoding. 534 final boolean hasLength = mInfoDelta.mTotalBytes != -1; 535 final boolean isConnectionClose = "close".equalsIgnoreCase( 536 conn.getHeaderField("Connection")); 537 final boolean isEncodingChunked = "chunked".equalsIgnoreCase( 538 conn.getHeaderField("Transfer-Encoding")); 539 540 final boolean finishKnown = hasLength || isConnectionClose || isEncodingChunked; 541 if (!finishKnown) { 542 throw new StopRequestException( 543 STATUS_CANNOT_RESUME, "can't know size of download, giving up"); 544 } 545 546 DrmManagerClient drmClient = null; 547 ParcelFileDescriptor outPfd = null; 548 FileDescriptor outFd = null; 549 InputStream in = null; 550 OutputStream out = null; 551 try { 552 try { 553 in = conn.getInputStream(); 554 } catch (IOException e) { 555 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 556 } 557 558 try { 559 outPfd = mContext.getContentResolver() 560 .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw"); 561 outFd = outPfd.getFileDescriptor(); 562 563 if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) { 564 drmClient = new DrmManagerClient(mContext); 565 out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType); 566 } else { 567 out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd); 568 } 569 570 // Move into place to begin writing 571 Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET); 572 } catch (ErrnoException e) { 573 throw new StopRequestException(STATUS_FILE_ERROR, e); 574 } catch (IOException e) { 575 throw new StopRequestException(STATUS_FILE_ERROR, e); 576 } 577 578 try { 579 // Pre-flight disk space requirements, when known 580 if (mInfoDelta.mTotalBytes > 0 && mStorage.isAllocationSupported(outFd)) { 581 mStorage.allocateBytes(outFd, mInfoDelta.mTotalBytes); 582 } 583 } catch (IOException e) { 584 throw new StopRequestException(STATUS_INSUFFICIENT_SPACE_ERROR, e); 585 } 586 587 // Start streaming data, periodically watch for pause/cancel 588 // commands and checking disk space as needed. 589 transferData(in, out, outFd); 590 591 try { 592 if (out instanceof DrmOutputStream) { 593 ((DrmOutputStream) out).finish(); 594 } 595 } catch (IOException e) { 596 throw new StopRequestException(STATUS_FILE_ERROR, e); 597 } 598 599 } finally { 600 if (drmClient != null) { 601 drmClient.close(); 602 } 603 604 IoUtils.closeQuietly(in); 605 606 try { 607 if (out != null) out.flush(); 608 if (outFd != null) outFd.sync(); 609 } catch (IOException e) { 610 } finally { 611 IoUtils.closeQuietly(out); 612 } 613 } 614 } 615 616 /** 617 * Transfer as much data as possible from the HTTP response to the 618 * destination file. 619 */ 620 private void transferData(InputStream in, OutputStream out, FileDescriptor outFd) 621 throws StopRequestException { 622 final byte buffer[] = new byte[Constants.BUFFER_SIZE]; 623 while (true) { 624 if (mPolicyDirty) checkConnectivity(); 625 626 if (mShutdownRequested) { 627 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, 628 "Local halt requested; job probably timed out"); 629 } 630 631 int len = -1; 632 try { 633 len = in.read(buffer); 634 } catch (IOException e) { 635 throw new StopRequestException( 636 STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e); 637 } 638 639 if (len == -1) { 640 break; 641 } 642 643 try { 644 out.write(buffer, 0, len); 645 646 mMadeProgress = true; 647 mInfoDelta.mCurrentBytes += len; 648 649 updateProgress(outFd); 650 651 } catch (IOException e) { 652 throw new StopRequestException(STATUS_FILE_ERROR, e); 653 } 654 } 655 656 // Finished without error; verify length if known 657 if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) { 658 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch; found " 659 + mInfoDelta.mCurrentBytes + " instead of " + mInfoDelta.mTotalBytes); 660 } 661 } 662 663 /** 664 * Called just before the thread finishes, regardless of status, to take any 665 * necessary action on the downloaded file. 666 */ 667 private void finalizeDestination() { 668 if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) { 669 // When error, free up any disk space 670 try { 671 final ParcelFileDescriptor target = mContext.getContentResolver() 672 .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw"); 673 try { 674 Os.ftruncate(target.getFileDescriptor(), 0); 675 } catch (ErrnoException ignored) { 676 } finally { 677 IoUtils.closeQuietly(target); 678 } 679 } catch (FileNotFoundException ignored) { 680 } 681 682 // Delete if local file 683 if (mInfoDelta.mFileName != null) { 684 new File(mInfoDelta.mFileName).delete(); 685 mInfoDelta.mFileName = null; 686 } 687 688 } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) { 689 // When success, open access if local file 690 if (mInfoDelta.mFileName != null) { 691 if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) { 692 try { 693 // Move into final resting place, if needed 694 final File before = new File(mInfoDelta.mFileName); 695 final File beforeDir = Helpers.getRunningDestinationDirectory( 696 mContext, mInfo.mDestination); 697 final File afterDir = Helpers.getSuccessDestinationDirectory( 698 mContext, mInfo.mDestination); 699 if (!beforeDir.equals(afterDir) 700 && before.getParentFile().equals(beforeDir)) { 701 final File after = new File(afterDir, before.getName()); 702 if (before.renameTo(after)) { 703 mInfoDelta.mFileName = after.getAbsolutePath(); 704 } 705 } 706 } catch (IOException ignored) { 707 } 708 } 709 } 710 } 711 } 712 713 /** 714 * Check if current connectivity is valid for this request. 715 */ 716 private void checkConnectivity() throws StopRequestException { 717 // checking connectivity will apply current policy 718 mPolicyDirty = false; 719 720 final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid, 721 mIgnoreBlocked); 722 if (info == null || !info.isConnected()) { 723 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected"); 724 } 725 if (info.isRoaming() && !mInfo.isRoamingAllowed()) { 726 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming"); 727 } 728 if (mSystemFacade.isNetworkMetered(mNetwork) 729 && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) { 730 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is metered"); 731 } 732 } 733 734 /** 735 * Report download progress through the database if necessary. 736 */ 737 private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException { 738 final long now = SystemClock.elapsedRealtime(); 739 final long currentBytes = mInfoDelta.mCurrentBytes; 740 741 final long sampleDelta = now - mSpeedSampleStart; 742 if (sampleDelta > 500) { 743 final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000) 744 / sampleDelta; 745 746 if (mSpeed == 0) { 747 mSpeed = sampleSpeed; 748 } else { 749 mSpeed = ((mSpeed * 3) + sampleSpeed) / 4; 750 } 751 752 // Only notify once we have a full sample window 753 if (mSpeedSampleStart != 0) { 754 mNotifier.notifyDownloadSpeed(mId, mSpeed); 755 } 756 757 mSpeedSampleStart = now; 758 mSpeedSampleBytes = currentBytes; 759 } 760 761 final long bytesDelta = currentBytes - mLastUpdateBytes; 762 final long timeDelta = now - mLastUpdateTime; 763 if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) { 764 // fsync() to ensure that current progress has been flushed to disk, 765 // so we can always resume based on latest database information. 766 outFd.sync(); 767 768 mInfoDelta.writeToDatabaseOrThrow(); 769 770 mLastUpdateBytes = currentBytes; 771 mLastUpdateTime = now; 772 } 773 } 774 775 /** 776 * Process response headers from first server response. This derives its 777 * filename, size, and ETag. 778 */ 779 private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException { 780 if (mInfoDelta.mFileName == null) { 781 final String contentDisposition = conn.getHeaderField("Content-Disposition"); 782 final String contentLocation = conn.getHeaderField("Content-Location"); 783 784 try { 785 mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri, 786 mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType, 787 mInfo.mDestination); 788 } catch (IOException e) { 789 throw new StopRequestException( 790 Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e); 791 } 792 } 793 794 if (mInfoDelta.mMimeType == null) { 795 mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType()); 796 } 797 798 final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); 799 if (transferEncoding == null) { 800 mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1); 801 } else { 802 mInfoDelta.mTotalBytes = -1; 803 } 804 805 mInfoDelta.mETag = conn.getHeaderField("ETag"); 806 807 mInfoDelta.writeToDatabaseOrThrow(); 808 809 // Check connectivity again now that we know the total size 810 checkConnectivity(); 811 } 812 813 private void parseUnavailableHeaders(HttpURLConnection conn) { 814 long retryAfter = conn.getHeaderFieldInt("Retry-After", -1); 815 retryAfter = MathUtils.constrain(retryAfter, Constants.MIN_RETRY_AFTER, 816 Constants.MAX_RETRY_AFTER); 817 mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS); 818 } 819 820 /** 821 * Add custom headers for this download to the HTTP request. 822 */ 823 private void addRequestHeaders(HttpURLConnection conn, boolean resuming) { 824 for (Pair<String, String> header : mInfo.getHeaders()) { 825 conn.addRequestProperty(header.first, header.second); 826 } 827 828 // Only splice in user agent when not already defined 829 if (conn.getRequestProperty("User-Agent") == null) { 830 conn.addRequestProperty("User-Agent", mInfo.getUserAgent()); 831 } 832 833 // Defeat transparent gzip compression, since it doesn't allow us to 834 // easily resume partial downloads. 835 conn.setRequestProperty("Accept-Encoding", "identity"); 836 837 // Defeat connection reuse, since otherwise servers may continue 838 // streaming large downloads after cancelled. 839 conn.setRequestProperty("Connection", "close"); 840 841 if (resuming) { 842 if (mInfoDelta.mETag != null) { 843 conn.addRequestProperty("If-Match", mInfoDelta.mETag); 844 } 845 conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-"); 846 } 847 } 848 849 private void logDebug(String msg) { 850 Log.d(TAG, "[" + mId + "] " + msg); 851 } 852 853 private void logWarning(String msg) { 854 Log.w(TAG, "[" + mId + "] " + msg); 855 } 856 857 private void logError(String msg, Throwable t) { 858 Log.e(TAG, "[" + mId + "] " + msg, t); 859 } 860 861 private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() { 862 @Override 863 public void onUidRulesChanged(int uid, int uidRules) { 864 // caller is NPMS, since we only register with them 865 if (uid == mInfo.mUid) { 866 mPolicyDirty = true; 867 } 868 } 869 870 @Override 871 public void onMeteredIfacesChanged(String[] meteredIfaces) { 872 // caller is NPMS, since we only register with them 873 mPolicyDirty = true; 874 } 875 876 @Override 877 public void onRestrictBackgroundChanged(boolean restrictBackground) { 878 // caller is NPMS, since we only register with them 879 mPolicyDirty = true; 880 } 881 882 @Override 883 public void onUidPoliciesChanged(int uid, int uidPolicies) { 884 // caller is NPMS, since we only register with them 885 if (uid == mInfo.mUid) { 886 mPolicyDirty = true; 887 } 888 } 889 }; 890 891 private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { 892 try { 893 return Long.parseLong(conn.getHeaderField(field)); 894 } catch (NumberFormatException e) { 895 return defaultValue; 896 } 897 } 898 899 /** 900 * Return if given status is eligible to be treated as 901 * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}. 902 */ 903 public static boolean isStatusRetryable(int status) { 904 switch (status) { 905 case STATUS_HTTP_DATA_ERROR: 906 case HTTP_UNAVAILABLE: 907 case HTTP_INTERNAL_ERROR: 908 case STATUS_FILE_ERROR: 909 return true; 910 default: 911 return false; 912 } 913 } 914 } 915