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