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