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