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