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.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; 20 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; 21 import static android.provider.Downloads.Impl.COLUMN_CONTROL; 22 import static android.provider.Downloads.Impl.COLUMN_DELETED; 23 import static android.provider.Downloads.Impl.COLUMN_STATUS; 24 import static android.provider.Downloads.Impl.CONTROL_PAUSED; 25 import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST; 26 import static android.provider.Downloads.Impl.STATUS_CANCELED; 27 import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME; 28 import static android.provider.Downloads.Impl.STATUS_FILE_ERROR; 29 import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR; 30 import static android.provider.Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR; 31 import static android.provider.Downloads.Impl.STATUS_PAUSED_BY_APP; 32 import static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 33 import static android.provider.Downloads.Impl.STATUS_RUNNING; 34 import static android.provider.Downloads.Impl.STATUS_SUCCESS; 35 import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS; 36 import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; 37 import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR; 38 import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK; 39 import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY; 40 import static android.text.format.DateUtils.SECOND_IN_MILLIS; 41 42 import static com.android.providers.downloads.Constants.TAG; 43 44 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; 45 import static java.net.HttpURLConnection.HTTP_MOVED_PERM; 46 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; 47 import static java.net.HttpURLConnection.HTTP_OK; 48 import static java.net.HttpURLConnection.HTTP_PARTIAL; 49 import static java.net.HttpURLConnection.HTTP_PRECON_FAILED; 50 import static java.net.HttpURLConnection.HTTP_SEE_OTHER; 51 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; 52 53 import android.app.job.JobParameters; 54 import android.content.ContentValues; 55 import android.content.Context; 56 import android.content.Intent; 57 import android.drm.DrmManagerClient; 58 import android.drm.DrmOutputStream; 59 import android.net.ConnectivityManager; 60 import android.net.INetworkPolicyListener; 61 import android.net.Network; 62 import android.net.NetworkCapabilities; 63 import android.net.NetworkInfo; 64 import android.net.NetworkPolicyManager; 65 import android.net.TrafficStats; 66 import android.net.Uri; 67 import android.os.ParcelFileDescriptor; 68 import android.os.Process; 69 import android.os.SystemClock; 70 import android.os.storage.StorageManager; 71 import android.provider.Downloads; 72 import android.system.ErrnoException; 73 import android.system.Os; 74 import android.system.OsConstants; 75 import android.util.Log; 76 import android.util.MathUtils; 77 import android.util.Pair; 78 79 import libcore.io.IoUtils; 80 81 import java.io.File; 82 import java.io.FileDescriptor; 83 import java.io.FileNotFoundException; 84 import java.io.IOException; 85 import java.io.InputStream; 86 import java.io.OutputStream; 87 import java.net.HttpURLConnection; 88 import java.net.MalformedURLException; 89 import java.net.ProtocolException; 90 import java.net.URL; 91 import java.net.URLConnection; 92 import java.security.GeneralSecurityException; 93 94 import javax.net.ssl.HttpsURLConnection; 95 import javax.net.ssl.SSLContext; 96 97 /** 98 * Task which executes a given {@link DownloadInfo}: making network requests, 99 * persisting data to disk, and updating {@link DownloadProvider}. 100 * <p> 101 * To know if a download is successful, we need to know either the final content 102 * length to expect, or the transfer to be chunked. To resume an interrupted 103 * download, we need an ETag. 104 * <p> 105 * Failed network requests are retried several times before giving up. Local 106 * disk errors fail immediately and are not retried. 107 */ 108 public class DownloadThread extends Thread { 109 110 // TODO: bind each download to a specific network interface to avoid state 111 // checking races once we have ConnectivityManager API 112 113 // TODO: add support for saving to content:// 114 115 private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; 116 private static final int HTTP_TEMP_REDIRECT = 307; 117 118 private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS); 119 120 private final Context mContext; 121 private final SystemFacade mSystemFacade; 122 private final DownloadNotifier mNotifier; 123 private final NetworkPolicyManager mNetworkPolicy; 124 private final StorageManager mStorage; 125 126 private final DownloadJobService mJobService; 127 private final JobParameters mParams; 128 129 private final long mId; 130 131 /** 132 * Info object that should be treated as read-only. Any potentially mutated 133 * fields are tracked in {@link #mInfoDelta}. If a field exists in 134 * {@link #mInfoDelta}, it must not be read from {@link #mInfo}. 135 */ 136 private final DownloadInfo mInfo; 137 private final DownloadInfoDelta mInfoDelta; 138 139 private volatile boolean mPolicyDirty; 140 141 /** 142 * Local changes to {@link DownloadInfo}. These are kept local to avoid 143 * racing with the thread that updates based on change notifications. 144 */ 145 private class DownloadInfoDelta { 146 public String mUri; 147 public String mFileName; 148 public String mMimeType; 149 public int mStatus; 150 public int mNumFailed; 151 public int mRetryAfter; 152 public long mTotalBytes; 153 public long mCurrentBytes; 154 public String mETag; 155 156 public String mErrorMsg; 157 158 private static final String NOT_CANCELED = COLUMN_STATUS + " != '" + STATUS_CANCELED + "'"; 159 private static final String NOT_DELETED = COLUMN_DELETED + " == '0'"; 160 private static final String NOT_PAUSED = "(" + COLUMN_CONTROL + " IS NULL OR " 161 + COLUMN_CONTROL + " != '" + CONTROL_PAUSED + "')"; 162 163 private static final String SELECTION_VALID = NOT_CANCELED + " AND " + NOT_DELETED + " AND " 164 + NOT_PAUSED; 165 DownloadInfoDelta(DownloadInfo info)166 public DownloadInfoDelta(DownloadInfo info) { 167 mUri = info.mUri; 168 mFileName = info.mFileName; 169 mMimeType = info.mMimeType; 170 mStatus = info.mStatus; 171 mNumFailed = info.mNumFailed; 172 mRetryAfter = info.mRetryAfter; 173 mTotalBytes = info.mTotalBytes; 174 mCurrentBytes = info.mCurrentBytes; 175 mETag = info.mETag; 176 } 177 buildContentValues()178 private ContentValues buildContentValues() { 179 final ContentValues values = new ContentValues(); 180 181 values.put(Downloads.Impl.COLUMN_URI, mUri); 182 values.put(Downloads.Impl._DATA, mFileName); 183 values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType); 184 values.put(Downloads.Impl.COLUMN_STATUS, mStatus); 185 values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed); 186 values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter); 187 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes); 188 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes); 189 values.put(Constants.ETAG, mETag); 190 191 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis()); 192 values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg); 193 194 return values; 195 } 196 197 /** 198 * Blindly push update of current delta values to provider. 199 */ writeToDatabase()200 public void writeToDatabase() { 201 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(), 202 null, null); 203 } 204 205 /** 206 * Push update of current delta values to provider, asserting strongly 207 * that we haven't been paused or deleted. 208 */ writeToDatabaseOrThrow()209 public void writeToDatabaseOrThrow() throws StopRequestException { 210 if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), 211 buildContentValues(), SELECTION_VALID, null) == 0) { 212 if (mInfo.queryDownloadControl() == CONTROL_PAUSED) { 213 throw new StopRequestException(STATUS_PAUSED_BY_APP, "Download paused!"); 214 } else { 215 throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!"); 216 } 217 } 218 } 219 } 220 221 /** 222 * Flag indicating if we've made forward progress transferring file data 223 * from a remote server. 224 */ 225 private boolean mMadeProgress = false; 226 227 /** 228 * Details from the last time we pushed a database update. 229 */ 230 private long mLastUpdateBytes = 0; 231 private long mLastUpdateTime = 0; 232 233 private boolean mIgnoreBlocked; 234 private Network mNetwork; 235 236 private int mNetworkType = ConnectivityManager.TYPE_NONE; 237 238 /** Historical bytes/second speed of this download. */ 239 private long mSpeed; 240 /** Time when current sample started. */ 241 private long mSpeedSampleStart; 242 /** Bytes transferred since current sample started. */ 243 private long mSpeedSampleBytes; 244 245 /** Flag indicating that thread must be halted */ 246 private volatile boolean mShutdownRequested; 247 DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info)248 public DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info) { 249 mContext = service; 250 mSystemFacade = Helpers.getSystemFacade(mContext); 251 mNotifier = Helpers.getDownloadNotifier(mContext); 252 mNetworkPolicy = mContext.getSystemService(NetworkPolicyManager.class); 253 mStorage = mContext.getSystemService(StorageManager.class); 254 255 mJobService = service; 256 mParams = params; 257 258 mId = info.mId; 259 mInfo = info; 260 mInfoDelta = new DownloadInfoDelta(info); 261 } 262 263 @Override run()264 public void run() { 265 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 266 267 // Skip when download already marked as finished; this download was 268 // probably started again while racing with UpdateThread. 269 if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) { 270 logDebug("Already finished; skipping"); 271 return; 272 } 273 274 try { 275 // while performing download, register for rules updates 276 mNetworkPolicy.registerListener(mPolicyListener); 277 278 logDebug("Starting"); 279 280 mInfoDelta.mStatus = STATUS_RUNNING; 281 mInfoDelta.writeToDatabase(); 282 283 // If we're showing a foreground notification for the requesting 284 // app, the download isn't affected by the blocked status of the 285 // requesting app 286 mIgnoreBlocked = mInfo.isVisible(); 287 288 // Use the caller's default network to make this connection, since 289 // they might be subject to restrictions that we shouldn't let them 290 // circumvent 291 mNetwork = mSystemFacade.getNetwork(mParams); 292 if (mNetwork == null) { 293 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, 294 "No network associated with requesting UID"); 295 } 296 297 // Remember which network this download started on; used to 298 // determine if errors were due to network changes. 299 final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid, 300 mIgnoreBlocked); 301 if (info != null) { 302 mNetworkType = info.getType(); 303 } 304 305 // Network traffic on this thread should be counted against the 306 // requesting UID, and is tagged with well-known value. 307 TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD); 308 TrafficStats.setThreadStatsUid(mInfo.mUid); 309 310 executeDownload(); 311 312 mInfoDelta.mStatus = STATUS_SUCCESS; 313 TrafficStats.incrementOperationCount(1); 314 315 // If we just finished a chunked file, record total size 316 if (mInfoDelta.mTotalBytes == -1) { 317 mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes; 318 } 319 320 } catch (StopRequestException e) { 321 mInfoDelta.mStatus = e.getFinalStatus(); 322 mInfoDelta.mErrorMsg = e.getMessage(); 323 324 logWarning("Stop requested with status " 325 + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": " 326 + mInfoDelta.mErrorMsg); 327 328 // Nobody below our level should request retries, since we handle 329 // failure counts at this level. 330 if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) { 331 throw new IllegalStateException("Execution should always throw final error codes"); 332 } 333 334 // Some errors should be retryable, unless we fail too many times. 335 if (isStatusRetryable(mInfoDelta.mStatus)) { 336 if (mMadeProgress) { 337 mInfoDelta.mNumFailed = 1; 338 } else { 339 mInfoDelta.mNumFailed += 1; 340 } 341 342 if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) { 343 final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid, 344 mIgnoreBlocked); 345 if (info != null && info.getType() == mNetworkType && info.isConnected()) { 346 // Underlying network is still intact, use normal backoff 347 mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY; 348 } else { 349 // Network changed, retry on any next available 350 mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK; 351 } 352 353 if ((mInfoDelta.mETag == null && mMadeProgress) 354 || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) { 355 // However, if we wrote data and have no ETag to verify 356 // contents against later, we can't actually resume. 357 mInfoDelta.mStatus = STATUS_CANNOT_RESUME; 358 } 359 } 360 } 361 362 // If we're waiting for a network that must be unmetered, our status 363 // is actually queued so we show relevant notifications 364 if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK 365 && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) { 366 mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI; 367 } 368 369 } catch (Throwable t) { 370 mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR; 371 mInfoDelta.mErrorMsg = t.toString(); 372 373 logError("Failed: " + mInfoDelta.mErrorMsg, t); 374 375 } finally { 376 logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus)); 377 378 mNotifier.notifyDownloadSpeed(mId, 0); 379 380 finalizeDestination(); 381 382 mInfoDelta.writeToDatabase(); 383 384 TrafficStats.clearThreadStatsTag(); 385 TrafficStats.clearThreadStatsUid(); 386 387 mNetworkPolicy.unregisterListener(mPolicyListener); 388 } 389 390 boolean needsReschedule = false; 391 if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY 392 || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK 393 || mInfoDelta.mStatus == STATUS_QUEUED_FOR_WIFI) { 394 needsReschedule = true; 395 } 396 397 mJobService.jobFinishedInternal(mParams, needsReschedule); 398 } 399 requestShutdown()400 public void requestShutdown() { 401 mShutdownRequested = true; 402 } 403 404 /** 405 * Fully execute a single download request. Setup and send the request, 406 * handle the response, and transfer the data to the destination file. 407 */ executeDownload()408 private void executeDownload() throws StopRequestException { 409 final boolean resuming = mInfoDelta.mCurrentBytes != 0; 410 411 URL url; 412 try { 413 // TODO: migrate URL sanity checking into client side of API 414 url = new URL(mInfoDelta.mUri); 415 } catch (MalformedURLException e) { 416 throw new StopRequestException(STATUS_BAD_REQUEST, e); 417 } 418 419 boolean cleartextTrafficPermitted 420 = mSystemFacade.isCleartextTrafficPermitted(mInfo.mPackage, url.getHost()); 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 package " + mInfo.mPackage + ": " 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 */ transferData(HttpURLConnection conn)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 */ transferData(InputStream in, OutputStream out, FileDescriptor outFd)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 */ finalizeDestination()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 */ checkConnectivity()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, mIgnoreBlocked); 721 final NetworkCapabilities caps = mSystemFacade.getNetworkCapabilities(mNetwork); 722 if (info == null || !info.isConnected()) { 723 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected"); 724 } 725 if (!caps.hasCapability(NET_CAPABILITY_NOT_ROAMING) 726 && !mInfo.isRoamingAllowed()) { 727 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming"); 728 } 729 if (!caps.hasCapability(NET_CAPABILITY_NOT_METERED) 730 && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) { 731 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is metered"); 732 } 733 } 734 735 /** 736 * Report download progress through the database if necessary. 737 */ updateProgress(FileDescriptor outFd)738 private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException { 739 final long now = SystemClock.elapsedRealtime(); 740 final long currentBytes = mInfoDelta.mCurrentBytes; 741 742 final long sampleDelta = now - mSpeedSampleStart; 743 if (sampleDelta > 500) { 744 final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000) 745 / sampleDelta; 746 747 if (mSpeed == 0) { 748 mSpeed = sampleSpeed; 749 } else { 750 mSpeed = ((mSpeed * 3) + sampleSpeed) / 4; 751 } 752 753 // Only notify once we have a full sample window 754 if (mSpeedSampleStart != 0) { 755 mNotifier.notifyDownloadSpeed(mId, mSpeed); 756 } 757 758 mSpeedSampleStart = now; 759 mSpeedSampleBytes = currentBytes; 760 } 761 762 final long bytesDelta = currentBytes - mLastUpdateBytes; 763 final long timeDelta = now - mLastUpdateTime; 764 if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) { 765 // fsync() to ensure that current progress has been flushed to disk, 766 // so we can always resume based on latest database information. 767 outFd.sync(); 768 769 mInfoDelta.writeToDatabaseOrThrow(); 770 771 mLastUpdateBytes = currentBytes; 772 mLastUpdateTime = now; 773 } 774 } 775 776 /** 777 * Process response headers from first server response. This derives its 778 * filename, size, and ETag. 779 */ parseOkHeaders(HttpURLConnection conn)780 private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException { 781 if (mInfoDelta.mFileName == null) { 782 final String contentDisposition = conn.getHeaderField("Content-Disposition"); 783 final String contentLocation = conn.getHeaderField("Content-Location"); 784 785 try { 786 mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri, 787 mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType, 788 mInfo.mDestination); 789 } catch (IOException e) { 790 throw new StopRequestException( 791 Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e); 792 } 793 } 794 795 if (mInfoDelta.mMimeType == null) { 796 mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType()); 797 } 798 799 final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); 800 if (transferEncoding == null) { 801 mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1); 802 } else { 803 mInfoDelta.mTotalBytes = -1; 804 } 805 806 mInfoDelta.mETag = conn.getHeaderField("ETag"); 807 808 mInfoDelta.writeToDatabaseOrThrow(); 809 810 // Check connectivity again now that we know the total size 811 checkConnectivity(); 812 } 813 parseUnavailableHeaders(HttpURLConnection conn)814 private void parseUnavailableHeaders(HttpURLConnection conn) { 815 long retryAfter = conn.getHeaderFieldInt("Retry-After", -1); 816 retryAfter = MathUtils.constrain(retryAfter, Constants.MIN_RETRY_AFTER, 817 Constants.MAX_RETRY_AFTER); 818 mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS); 819 } 820 821 /** 822 * Add custom headers for this download to the HTTP request. 823 */ addRequestHeaders(HttpURLConnection conn, boolean resuming)824 private void addRequestHeaders(HttpURLConnection conn, boolean resuming) { 825 for (Pair<String, String> header : mInfo.getHeaders()) { 826 conn.addRequestProperty(header.first, header.second); 827 } 828 829 // Only splice in user agent when not already defined 830 if (conn.getRequestProperty("User-Agent") == null) { 831 conn.addRequestProperty("User-Agent", mInfo.getUserAgent()); 832 } 833 834 // Defeat transparent gzip compression, since it doesn't allow us to 835 // easily resume partial downloads. 836 conn.setRequestProperty("Accept-Encoding", "identity"); 837 838 // Defeat connection reuse, since otherwise servers may continue 839 // streaming large downloads after cancelled. 840 conn.setRequestProperty("Connection", "close"); 841 842 if (resuming) { 843 if (mInfoDelta.mETag != null) { 844 conn.addRequestProperty("If-Match", mInfoDelta.mETag); 845 } 846 conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-"); 847 } 848 } 849 logDebug(String msg)850 private void logDebug(String msg) { 851 Log.d(TAG, "[" + mId + "] " + msg); 852 } 853 logWarning(String msg)854 private void logWarning(String msg) { 855 Log.w(TAG, "[" + mId + "] " + msg); 856 } 857 logError(String msg, Throwable t)858 private void logError(String msg, Throwable t) { 859 Log.e(TAG, "[" + mId + "] " + msg, t); 860 } 861 862 private INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() { 863 @Override 864 public void onUidRulesChanged(int uid, int uidRules) { 865 // caller is NPMS, since we only register with them 866 if (uid == mInfo.mUid) { 867 mPolicyDirty = true; 868 } 869 } 870 871 @Override 872 public void onMeteredIfacesChanged(String[] meteredIfaces) { 873 // caller is NPMS, since we only register with them 874 mPolicyDirty = true; 875 } 876 877 @Override 878 public void onRestrictBackgroundChanged(boolean restrictBackground) { 879 // caller is NPMS, since we only register with them 880 mPolicyDirty = true; 881 } 882 883 @Override 884 public void onUidPoliciesChanged(int uid, int uidPolicies) { 885 // caller is NPMS, since we only register with them 886 if (uid == mInfo.mUid) { 887 mPolicyDirty = true; 888 } 889 } 890 }; 891 getHeaderFieldLong(URLConnection conn, String field, long defaultValue)892 private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { 893 try { 894 return Long.parseLong(conn.getHeaderField(field)); 895 } catch (NumberFormatException e) { 896 return defaultValue; 897 } 898 } 899 900 /** 901 * Return if given status is eligible to be treated as 902 * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}. 903 */ isStatusRetryable(int status)904 public static boolean isStatusRetryable(int status) { 905 switch (status) { 906 case STATUS_HTTP_DATA_ERROR: 907 case HTTP_UNAVAILABLE: 908 case HTTP_INTERNAL_ERROR: 909 case STATUS_FILE_ERROR: 910 return true; 911 default: 912 return false; 913 } 914 } 915 } 916