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 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 DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info)247 public DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info) { 248 mContext = service; 249 mSystemFacade = Helpers.getSystemFacade(mContext); 250 mNotifier = Helpers.getDownloadNotifier(mContext); 251 mNetworkPolicy = mContext.getSystemService(NetworkPolicyManager.class); 252 mStorage = mContext.getSystemService(StorageManager.class); 253 254 mJobService = service; 255 mParams = params; 256 257 mId = info.mId; 258 mInfo = info; 259 mInfoDelta = new DownloadInfoDelta(info); 260 } 261 262 @Override run()263 public void run() { 264 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 265 266 // Skip when download already marked as finished; this download was 267 // probably started again while racing with UpdateThread. 268 if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) { 269 logDebug("Already finished; skipping"); 270 return; 271 } 272 273 try { 274 // while performing download, register for rules updates 275 mNetworkPolicy.registerListener(mPolicyListener); 276 277 logDebug("Starting"); 278 279 mInfoDelta.mStatus = STATUS_RUNNING; 280 mInfoDelta.writeToDatabase(); 281 282 // If we're showing a foreground notification for the requesting 283 // app, the download isn't affected by the blocked status of the 284 // requesting app 285 mIgnoreBlocked = mInfo.isVisible(); 286 287 // Use the caller's default network to make this connection, since 288 // they might be subject to restrictions that we shouldn't let them 289 // circumvent 290 mNetwork = mSystemFacade.getNetwork(mParams); 291 if (mNetwork == null) { 292 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, 293 "No network associated with requesting UID"); 294 } 295 296 // Network traffic on this thread should be counted against the 297 // requesting UID, and is tagged with well-known value. 298 TrafficStats.setThreadStatsTagDownload(); 299 TrafficStats.setThreadStatsUid(mInfo.mUid); 300 301 executeDownload(); 302 303 mInfoDelta.mStatus = STATUS_SUCCESS; 304 TrafficStats.incrementOperationCount(1); 305 306 // If we just finished a chunked file, record total size 307 if (mInfoDelta.mTotalBytes == -1) { 308 mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes; 309 } 310 311 } catch (StopRequestException e) { 312 mInfoDelta.mStatus = e.getFinalStatus(); 313 mInfoDelta.mErrorMsg = e.getMessage(); 314 315 logWarning("Stop requested with status " 316 + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": " 317 + mInfoDelta.mErrorMsg); 318 319 // Nobody below our level should request retries, since we handle 320 // failure counts at this level. 321 if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) { 322 throw new IllegalStateException("Execution should always throw final error codes"); 323 } 324 325 // Some errors should be retryable, unless we fail too many times. 326 if (isStatusRetryable(mInfoDelta.mStatus)) { 327 if (mMadeProgress) { 328 mInfoDelta.mNumFailed = 1; 329 } else { 330 mInfoDelta.mNumFailed += 1; 331 } 332 333 if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) { 334 if (null != mSystemFacade.getNetworkCapabilities(mNetwork)) { 335 // Underlying network is still intact, use normal backoff 336 mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY; 337 } else { 338 // Network unavailable, retry on any next available 339 mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK; 340 } 341 342 if ((mInfoDelta.mETag == null && mMadeProgress) 343 || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) { 344 // However, if we wrote data and have no ETag to verify 345 // contents against later, we can't actually resume. 346 mInfoDelta.mStatus = STATUS_CANNOT_RESUME; 347 } 348 } 349 } 350 351 // If we're waiting for a network that must be unmetered, our status 352 // is actually queued so we show relevant notifications 353 if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK 354 && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) { 355 mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI; 356 } 357 358 } catch (Throwable t) { 359 mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR; 360 mInfoDelta.mErrorMsg = t.toString(); 361 362 logError("Failed: " + mInfoDelta.mErrorMsg, t); 363 364 } finally { 365 logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus)); 366 367 mNotifier.notifyDownloadSpeed(mId, 0); 368 369 finalizeDestination(); 370 371 mInfoDelta.writeToDatabase(); 372 373 TrafficStats.clearThreadStatsTag(); 374 TrafficStats.clearThreadStatsUid(); 375 376 mNetworkPolicy.unregisterListener(mPolicyListener); 377 } 378 379 boolean needsReschedule = false; 380 if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY 381 || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK 382 || mInfoDelta.mStatus == STATUS_QUEUED_FOR_WIFI) { 383 logDebug("rescheduling the job id:" + mId); 384 needsReschedule = true; 385 } 386 387 mJobService.jobFinishedInternal(mParams, needsReschedule); 388 } 389 requestShutdown()390 public void requestShutdown() { 391 mShutdownRequested = true; 392 } 393 394 /** 395 * Fully execute a single download request. Setup and send the request, 396 * handle the response, and transfer the data to the destination file. 397 */ executeDownload()398 private void executeDownload() throws StopRequestException { 399 final boolean resuming = mInfoDelta.mCurrentBytes != 0; 400 401 URL url; 402 try { 403 // TODO: migrate URL sanity checking into client side of API 404 url = new URL(mInfoDelta.mUri); 405 } catch (MalformedURLException e) { 406 throw new StopRequestException(STATUS_BAD_REQUEST, e); 407 } 408 409 boolean cleartextTrafficPermitted 410 = mSystemFacade.isCleartextTrafficPermitted(mInfo.mPackage, url.getHost()); 411 SSLContext appContext; 412 try { 413 appContext = mSystemFacade.getSSLContextForPackage(mContext, mInfo.mPackage); 414 } catch (GeneralSecurityException e) { 415 // This should never happen. 416 throw new StopRequestException(STATUS_UNKNOWN_ERROR, "Unable to create SSLContext."); 417 } 418 int redirectionCount = 0; 419 while (redirectionCount++ < Constants.MAX_REDIRECTS) { 420 // Enforce the cleartext traffic opt-out for the UID. This cannot be enforced earlier 421 // because of HTTP redirects which can change the protocol between HTTP and HTTPS. 422 if ((!cleartextTrafficPermitted) && ("http".equalsIgnoreCase(url.getProtocol()))) { 423 throw new StopRequestException(STATUS_BAD_REQUEST, 424 "Cleartext traffic not permitted for package " + mInfo.mPackage + ": " 425 + Uri.parse(url.toString()).toSafeString()); 426 } 427 428 // Open connection and follow any redirects until we have a useful 429 // response with body. 430 HttpURLConnection conn = null; 431 try { 432 // Check that the caller is allowed to make network connections. If so, make one on 433 // their behalf to open the url. 434 checkConnectivity(); 435 conn = (HttpURLConnection) mNetwork.openConnection(url); 436 conn.setInstanceFollowRedirects(false); 437 conn.setConnectTimeout(DEFAULT_TIMEOUT); 438 conn.setReadTimeout(DEFAULT_TIMEOUT); 439 // If this is going over HTTPS configure the trust to be the same as the calling 440 // package. 441 if (conn instanceof HttpsURLConnection) { 442 ((HttpsURLConnection)conn).setSSLSocketFactory(appContext.getSocketFactory()); 443 } 444 445 addRequestHeaders(conn, resuming); 446 447 final int responseCode = conn.getResponseCode(); 448 switch (responseCode) { 449 case HTTP_OK: 450 if (resuming) { 451 throw new StopRequestException( 452 STATUS_CANNOT_RESUME, "Expected partial, but received OK"); 453 } 454 parseOkHeaders(conn); 455 transferData(conn); 456 return; 457 458 case HTTP_PARTIAL: 459 if (!resuming) { 460 throw new StopRequestException( 461 STATUS_CANNOT_RESUME, "Expected OK, but received partial"); 462 } 463 transferData(conn); 464 return; 465 466 case HTTP_MOVED_PERM: 467 case HTTP_MOVED_TEMP: 468 case HTTP_SEE_OTHER: 469 case HTTP_TEMP_REDIRECT: 470 final String location = conn.getHeaderField("Location"); 471 url = new URL(url, location); 472 if (responseCode == HTTP_MOVED_PERM) { 473 // Push updated URL back to database 474 mInfoDelta.mUri = url.toString(); 475 } 476 continue; 477 478 case HTTP_PRECON_FAILED: 479 throw new StopRequestException( 480 STATUS_CANNOT_RESUME, "Precondition failed"); 481 482 case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: 483 throw new StopRequestException( 484 STATUS_CANNOT_RESUME, "Requested range not satisfiable"); 485 486 case HTTP_UNAVAILABLE: 487 parseUnavailableHeaders(conn); 488 throw new StopRequestException( 489 HTTP_UNAVAILABLE, conn.getResponseMessage()); 490 491 case HTTP_INTERNAL_ERROR: 492 throw new StopRequestException( 493 HTTP_INTERNAL_ERROR, conn.getResponseMessage()); 494 495 default: 496 StopRequestException.throwUnhandledHttpError( 497 responseCode, conn.getResponseMessage()); 498 } 499 500 } catch (IOException e) { 501 if (e instanceof ProtocolException 502 && e.getMessage().startsWith("Unexpected status line")) { 503 throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e); 504 } else { 505 // Trouble with low-level sockets 506 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 507 } 508 509 } finally { 510 if (conn != null) conn.disconnect(); 511 } 512 } 513 514 throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects"); 515 } 516 517 /** 518 * Transfer data from the given connection to the destination file. 519 */ transferData(HttpURLConnection conn)520 private void transferData(HttpURLConnection conn) throws StopRequestException { 521 522 // To detect when we're really finished, we either need a length, closed 523 // connection, or chunked encoding. 524 final boolean hasLength = mInfoDelta.mTotalBytes != -1; 525 final boolean isConnectionClose = "close".equalsIgnoreCase( 526 conn.getHeaderField("Connection")); 527 final boolean isEncodingChunked = "chunked".equalsIgnoreCase( 528 conn.getHeaderField("Transfer-Encoding")); 529 530 final boolean finishKnown = hasLength || isConnectionClose || isEncodingChunked; 531 if (!finishKnown) { 532 throw new StopRequestException( 533 STATUS_CANNOT_RESUME, "can't know size of download, giving up"); 534 } 535 536 DrmManagerClient drmClient = null; 537 ParcelFileDescriptor outPfd = null; 538 FileDescriptor outFd = null; 539 InputStream in = null; 540 OutputStream out = null; 541 try { 542 try { 543 in = conn.getInputStream(); 544 } catch (IOException e) { 545 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 546 } 547 548 try { 549 outPfd = mContext.getContentResolver() 550 .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw"); 551 outFd = outPfd.getFileDescriptor(); 552 553 if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) { 554 drmClient = new DrmManagerClient(mContext); 555 out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType); 556 } else { 557 out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd); 558 } 559 560 // Move into place to begin writing 561 Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET); 562 } catch (ErrnoException e) { 563 throw new StopRequestException(STATUS_FILE_ERROR, e); 564 } catch (IOException e) { 565 throw new StopRequestException(STATUS_FILE_ERROR, e); 566 } 567 568 try { 569 // Pre-flight disk space requirements, when known 570 if (mInfoDelta.mTotalBytes > 0 && mStorage.isAllocationSupported(outFd)) { 571 mStorage.allocateBytes(outFd, mInfoDelta.mTotalBytes); 572 } 573 } catch (IOException e) { 574 throw new StopRequestException(STATUS_INSUFFICIENT_SPACE_ERROR, e); 575 } 576 577 // Start streaming data, periodically watch for pause/cancel 578 // commands and checking disk space as needed. 579 transferData(in, out, outFd); 580 581 try { 582 if (out instanceof DrmOutputStream) { 583 ((DrmOutputStream) out).finish(); 584 } 585 } catch (IOException e) { 586 throw new StopRequestException(STATUS_FILE_ERROR, e); 587 } 588 589 } finally { 590 if (drmClient != null) { 591 drmClient.close(); 592 } 593 594 IoUtils.closeQuietly(in); 595 596 try { 597 if (out != null) out.flush(); 598 if (outFd != null) outFd.sync(); 599 } catch (IOException e) { 600 } finally { 601 IoUtils.closeQuietly(out); 602 } 603 } 604 } 605 606 /** 607 * Transfer as much data as possible from the HTTP response to the 608 * destination file. 609 */ transferData(InputStream in, OutputStream out, FileDescriptor outFd)610 private void transferData(InputStream in, OutputStream out, FileDescriptor outFd) 611 throws StopRequestException { 612 final byte buffer[] = new byte[Constants.BUFFER_SIZE]; 613 while (true) { 614 if (mPolicyDirty) checkConnectivity(); 615 616 if (mShutdownRequested) { 617 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, 618 "Local halt requested; job probably timed out"); 619 } 620 621 int len = -1; 622 try { 623 len = in.read(buffer); 624 } catch (IOException e) { 625 throw new StopRequestException( 626 STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e); 627 } 628 629 if (len == -1) { 630 break; 631 } 632 633 try { 634 out.write(buffer, 0, len); 635 636 mMadeProgress = true; 637 mInfoDelta.mCurrentBytes += len; 638 639 updateProgress(outFd); 640 641 } catch (IOException e) { 642 throw new StopRequestException(STATUS_FILE_ERROR, e); 643 } 644 } 645 646 // Finished without error; verify length if known 647 if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) { 648 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch; found " 649 + mInfoDelta.mCurrentBytes + " instead of " + mInfoDelta.mTotalBytes); 650 } 651 } 652 653 /** 654 * Called just before the thread finishes, regardless of status, to take any 655 * necessary action on the downloaded file. 656 */ finalizeDestination()657 private void finalizeDestination() { 658 if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) { 659 // When error, free up any disk space 660 try { 661 final ParcelFileDescriptor target = mContext.getContentResolver() 662 .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw"); 663 try { 664 Os.ftruncate(target.getFileDescriptor(), 0); 665 } catch (ErrnoException ignored) { 666 } finally { 667 IoUtils.closeQuietly(target); 668 } 669 } catch (FileNotFoundException ignored) { 670 } 671 672 // Delete if local file 673 if (mInfoDelta.mFileName != null) { 674 new File(mInfoDelta.mFileName).delete(); 675 mInfoDelta.mFileName = null; 676 } 677 678 } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) { 679 // When success, open access if local file 680 if (mInfoDelta.mFileName != null) { 681 if (Helpers.isFileInExternalAndroidDirs(mInfoDelta.mFileName)) { 682 // Files that are downloaded in Android/ may need fixing up 683 // of permissions on devices without sdcardfs; do so here, 684 // before we give the file back to the client 685 File file = new File(mInfoDelta.mFileName); 686 mStorage.fixupAppDir(file.getParentFile()); 687 } 688 if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) { 689 try { 690 // Move into final resting place, if needed 691 final File before = new File(mInfoDelta.mFileName); 692 final File beforeDir = Helpers.getRunningDestinationDirectory( 693 mContext, mInfo.mDestination); 694 final File afterDir = Helpers.getSuccessDestinationDirectory( 695 mContext, mInfo.mDestination); 696 if (!beforeDir.equals(afterDir) 697 && before.getParentFile().equals(beforeDir)) { 698 final File after = new File(afterDir, before.getName()); 699 if (before.renameTo(after)) { 700 mInfoDelta.mFileName = after.getAbsolutePath(); 701 } 702 } 703 } catch (IOException ignored) { 704 } 705 } 706 } 707 } 708 } 709 710 /** 711 * Check if current connectivity is valid for this request. 712 */ checkConnectivity()713 private void checkConnectivity() throws StopRequestException { 714 // checking connectivity will apply current policy 715 mPolicyDirty = false; 716 717 final NetworkCapabilities caps = mSystemFacade.getNetworkCapabilities(mNetwork); 718 if (caps == null) { 719 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected"); 720 } 721 if (!caps.hasCapability(NET_CAPABILITY_NOT_ROAMING) 722 && !mInfo.isRoamingAllowed()) { 723 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming"); 724 } 725 if (!caps.hasCapability(NET_CAPABILITY_NOT_METERED) 726 && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) { 727 throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is metered"); 728 } 729 } 730 731 /** 732 * Report download progress through the database if necessary. 733 */ updateProgress(FileDescriptor outFd)734 private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException { 735 final long now = SystemClock.elapsedRealtime(); 736 final long currentBytes = mInfoDelta.mCurrentBytes; 737 738 final long sampleDelta = now - mSpeedSampleStart; 739 if (sampleDelta > 500) { 740 final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000) 741 / sampleDelta; 742 743 if (mSpeed == 0) { 744 mSpeed = sampleSpeed; 745 } else { 746 mSpeed = ((mSpeed * 3) + sampleSpeed) / 4; 747 } 748 749 // Only notify once we have a full sample window 750 if (mSpeedSampleStart != 0) { 751 mNotifier.notifyDownloadSpeed(mId, mSpeed); 752 } 753 754 mSpeedSampleStart = now; 755 mSpeedSampleBytes = currentBytes; 756 } 757 758 final long bytesDelta = currentBytes - mLastUpdateBytes; 759 final long timeDelta = now - mLastUpdateTime; 760 if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) { 761 // fsync() to ensure that current progress has been flushed to disk, 762 // so we can always resume based on latest database information. 763 outFd.sync(); 764 765 mInfoDelta.writeToDatabaseOrThrow(); 766 767 mLastUpdateBytes = currentBytes; 768 mLastUpdateTime = now; 769 } 770 } 771 772 /** 773 * Process response headers from first server response. This derives its 774 * filename, size, and ETag. 775 */ parseOkHeaders(HttpURLConnection conn)776 private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException { 777 if (mInfoDelta.mFileName == null) { 778 final String contentDisposition = conn.getHeaderField("Content-Disposition"); 779 final String contentLocation = conn.getHeaderField("Content-Location"); 780 781 try { 782 mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri, 783 mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType, 784 mInfo.mDestination); 785 } catch (IOException e) { 786 throw new StopRequestException( 787 Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e); 788 } 789 } 790 791 if (mInfoDelta.mMimeType == null) { 792 mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType()); 793 } 794 795 final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); 796 if (transferEncoding == null) { 797 mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1); 798 } else { 799 mInfoDelta.mTotalBytes = -1; 800 } 801 802 mInfoDelta.mETag = conn.getHeaderField("ETag"); 803 804 mInfoDelta.writeToDatabaseOrThrow(); 805 806 // Check connectivity again now that we know the total size 807 checkConnectivity(); 808 } 809 parseUnavailableHeaders(HttpURLConnection conn)810 private void parseUnavailableHeaders(HttpURLConnection conn) { 811 long retryAfter = conn.getHeaderFieldInt("Retry-After", -1); 812 retryAfter = MathUtils.constrain(retryAfter, Constants.MIN_RETRY_AFTER, 813 Constants.MAX_RETRY_AFTER); 814 mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS); 815 } 816 817 /** 818 * Add custom headers for this download to the HTTP request. 819 */ addRequestHeaders(HttpURLConnection conn, boolean resuming)820 private void addRequestHeaders(HttpURLConnection conn, boolean resuming) { 821 for (Pair<String, String> header : mInfo.getHeaders()) { 822 conn.addRequestProperty(header.first, header.second); 823 } 824 825 // Only splice in user agent when not already defined 826 if (conn.getRequestProperty("User-Agent") == null) { 827 conn.addRequestProperty("User-Agent", mInfo.getUserAgent()); 828 } 829 830 // Defeat transparent gzip compression, since it doesn't allow us to 831 // easily resume partial downloads. 832 conn.setRequestProperty("Accept-Encoding", "identity"); 833 834 // Defeat connection reuse, since otherwise servers may continue 835 // streaming large downloads after cancelled. 836 conn.setRequestProperty("Connection", "close"); 837 838 if (resuming) { 839 if (mInfoDelta.mETag != null) { 840 conn.addRequestProperty("If-Match", mInfoDelta.mETag); 841 } 842 conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-"); 843 } 844 } 845 logDebug(String msg)846 private void logDebug(String msg) { 847 Log.d(TAG, "[" + mId + "] " + msg); 848 } 849 logWarning(String msg)850 private void logWarning(String msg) { 851 Log.w(TAG, "[" + mId + "] " + msg); 852 } 853 logError(String msg, Throwable t)854 private void logError(String msg, Throwable t) { 855 Log.e(TAG, "[" + mId + "] " + msg, t); 856 } 857 858 private INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() { 859 @Override 860 public void onUidRulesChanged(int uid, int uidRules) { 861 // caller is NPMS, since we only register with them 862 if (uid == mInfo.mUid) { 863 mPolicyDirty = true; 864 } 865 } 866 867 @Override 868 public void onMeteredIfacesChanged(String[] meteredIfaces) { 869 // caller is NPMS, since we only register with them 870 mPolicyDirty = true; 871 } 872 873 @Override 874 public void onRestrictBackgroundChanged(boolean restrictBackground) { 875 // caller is NPMS, since we only register with them 876 mPolicyDirty = true; 877 } 878 879 @Override 880 public void onUidPoliciesChanged(int uid, int uidPolicies) { 881 // caller is NPMS, since we only register with them 882 if (uid == mInfo.mUid) { 883 mPolicyDirty = true; 884 } 885 } 886 }; 887 getHeaderFieldLong(URLConnection conn, String field, long defaultValue)888 private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { 889 try { 890 return Long.parseLong(conn.getHeaderField(field)); 891 } catch (NumberFormatException e) { 892 return defaultValue; 893 } 894 } 895 896 /** 897 * Return if given status is eligible to be treated as 898 * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}. 899 */ isStatusRetryable(int status)900 public static boolean isStatusRetryable(int status) { 901 switch (status) { 902 case STATUS_HTTP_DATA_ERROR: 903 case HTTP_UNAVAILABLE: 904 case HTTP_INTERNAL_ERROR: 905 case STATUS_FILE_ERROR: 906 return true; 907 default: 908 return false; 909 } 910 } 911 } 912