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_WAITING_FOR_NETWORK; 26import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY; 27import static android.text.format.DateUtils.SECOND_IN_MILLIS; 28import static com.android.providers.downloads.Constants.TAG; 29import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; 30import static java.net.HttpURLConnection.HTTP_MOVED_PERM; 31import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; 32import static java.net.HttpURLConnection.HTTP_OK; 33import static java.net.HttpURLConnection.HTTP_PARTIAL; 34import static java.net.HttpURLConnection.HTTP_SEE_OTHER; 35import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; 36 37import android.content.ContentValues; 38import android.content.Context; 39import android.content.Intent; 40import android.drm.DrmManagerClient; 41import android.drm.DrmOutputStream; 42import android.net.ConnectivityManager; 43import android.net.INetworkPolicyListener; 44import android.net.NetworkInfo; 45import android.net.NetworkPolicyManager; 46import android.net.TrafficStats; 47import android.os.FileUtils; 48import android.os.PowerManager; 49import android.os.Process; 50import android.os.SystemClock; 51import android.os.WorkSource; 52import android.provider.Downloads; 53import android.text.TextUtils; 54import android.util.Log; 55import android.util.Pair; 56 57import com.android.providers.downloads.DownloadInfo.NetworkState; 58 59import libcore.io.IoUtils; 60 61import java.io.File; 62import java.io.FileDescriptor; 63import java.io.FileOutputStream; 64import java.io.IOException; 65import java.io.InputStream; 66import java.io.OutputStream; 67import java.io.RandomAccessFile; 68import java.net.HttpURLConnection; 69import java.net.MalformedURLException; 70import java.net.URL; 71import java.net.URLConnection; 72 73/** 74 * Task which executes a given {@link DownloadInfo}: making network requests, 75 * persisting data to disk, and updating {@link DownloadProvider}. 76 */ 77public class DownloadThread implements Runnable { 78 79 // TODO: bind each download to a specific network interface to avoid state 80 // checking races once we have ConnectivityManager API 81 82 private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; 83 private static final int HTTP_TEMP_REDIRECT = 307; 84 85 private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS); 86 87 private final Context mContext; 88 private final DownloadInfo mInfo; 89 private final SystemFacade mSystemFacade; 90 private final StorageManager mStorageManager; 91 private final DownloadNotifier mNotifier; 92 93 private volatile boolean mPolicyDirty; 94 95 public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info, 96 StorageManager storageManager, DownloadNotifier notifier) { 97 mContext = context; 98 mSystemFacade = systemFacade; 99 mInfo = info; 100 mStorageManager = storageManager; 101 mNotifier = notifier; 102 } 103 104 /** 105 * Returns the user agent provided by the initiating app, or use the default one 106 */ 107 private String userAgent() { 108 String userAgent = mInfo.mUserAgent; 109 if (userAgent == null) { 110 userAgent = Constants.DEFAULT_USER_AGENT; 111 } 112 return userAgent; 113 } 114 115 /** 116 * State for the entire run() method. 117 */ 118 static class State { 119 public String mFilename; 120 public String mMimeType; 121 public int mRetryAfter = 0; 122 public boolean mGotData = false; 123 public String mRequestUri; 124 public long mTotalBytes = -1; 125 public long mCurrentBytes = 0; 126 public String mHeaderETag; 127 public boolean mContinuingDownload = false; 128 public long mBytesNotified = 0; 129 public long mTimeLastNotification = 0; 130 public int mNetworkType = ConnectivityManager.TYPE_NONE; 131 132 /** Historical bytes/second speed of this download. */ 133 public long mSpeed; 134 /** Time when current sample started. */ 135 public long mSpeedSampleStart; 136 /** Bytes transferred since current sample started. */ 137 public long mSpeedSampleBytes; 138 139 public long mContentLength = -1; 140 public String mContentDisposition; 141 public String mContentLocation; 142 143 public int mRedirectionCount; 144 public URL mUrl; 145 146 public State(DownloadInfo info) { 147 mMimeType = Intent.normalizeMimeType(info.mMimeType); 148 mRequestUri = info.mUri; 149 mFilename = info.mFileName; 150 mTotalBytes = info.mTotalBytes; 151 mCurrentBytes = info.mCurrentBytes; 152 } 153 154 public void resetBeforeExecute() { 155 // Reset any state from previous execution 156 mContentLength = -1; 157 mContentDisposition = null; 158 mContentLocation = null; 159 mRedirectionCount = 0; 160 } 161 } 162 163 @Override 164 public void run() { 165 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 166 try { 167 runInternal(); 168 } finally { 169 mNotifier.notifyDownloadSpeed(mInfo.mId, 0); 170 } 171 } 172 173 private void runInternal() { 174 // Skip when download already marked as finished; this download was 175 // probably started again while racing with UpdateThread. 176 if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId) 177 == Downloads.Impl.STATUS_SUCCESS) { 178 Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping"); 179 return; 180 } 181 182 State state = new State(mInfo); 183 PowerManager.WakeLock wakeLock = null; 184 int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; 185 int numFailed = mInfo.mNumFailed; 186 String errorMsg = null; 187 188 final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext); 189 final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 190 191 try { 192 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); 193 wakeLock.setWorkSource(new WorkSource(mInfo.mUid)); 194 wakeLock.acquire(); 195 196 // while performing download, register for rules updates 197 netPolicy.registerListener(mPolicyListener); 198 199 Log.i(Constants.TAG, "Download " + mInfo.mId + " starting"); 200 201 // Remember which network this download started on; used to 202 // determine if errors were due to network changes. 203 final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); 204 if (info != null) { 205 state.mNetworkType = info.getType(); 206 } 207 208 // Network traffic on this thread should be counted against the 209 // requesting UID, and is tagged with well-known value. 210 TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD); 211 TrafficStats.setThreadStatsUid(mInfo.mUid); 212 213 try { 214 // TODO: migrate URL sanity checking into client side of API 215 state.mUrl = new URL(state.mRequestUri); 216 } catch (MalformedURLException e) { 217 throw new StopRequestException(STATUS_BAD_REQUEST, e); 218 } 219 220 executeDownload(state); 221 222 finalizeDestinationFile(state); 223 finalStatus = Downloads.Impl.STATUS_SUCCESS; 224 } catch (StopRequestException error) { 225 // remove the cause before printing, in case it contains PII 226 errorMsg = error.getMessage(); 227 String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg; 228 Log.w(Constants.TAG, msg); 229 if (Constants.LOGV) { 230 Log.w(Constants.TAG, msg, error); 231 } 232 finalStatus = error.getFinalStatus(); 233 234 // Nobody below our level should request retries, since we handle 235 // failure counts at this level. 236 if (finalStatus == STATUS_WAITING_TO_RETRY) { 237 throw new IllegalStateException("Execution should always throw final error codes"); 238 } 239 240 // Some errors should be retryable, unless we fail too many times. 241 if (isStatusRetryable(finalStatus)) { 242 if (state.mGotData) { 243 numFailed = 1; 244 } else { 245 numFailed += 1; 246 } 247 248 if (numFailed < Constants.MAX_RETRIES) { 249 final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); 250 if (info != null && info.getType() == state.mNetworkType 251 && info.isConnected()) { 252 // Underlying network is still intact, use normal backoff 253 finalStatus = STATUS_WAITING_TO_RETRY; 254 } else { 255 // Network changed, retry on any next available 256 finalStatus = STATUS_WAITING_FOR_NETWORK; 257 } 258 } 259 } 260 261 // fall through to finally block 262 } catch (Throwable ex) { 263 errorMsg = ex.getMessage(); 264 String msg = "Exception for id " + mInfo.mId + ": " + errorMsg; 265 Log.w(Constants.TAG, msg, ex); 266 finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; 267 // falls through to the code that reports an error 268 } finally { 269 if (finalStatus == STATUS_SUCCESS) { 270 TrafficStats.incrementOperationCount(1); 271 } 272 273 TrafficStats.clearThreadStatsTag(); 274 TrafficStats.clearThreadStatsUid(); 275 276 cleanupDestination(state, finalStatus); 277 notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed); 278 279 Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status " 280 + Downloads.Impl.statusToString(finalStatus)); 281 282 netPolicy.unregisterListener(mPolicyListener); 283 284 if (wakeLock != null) { 285 wakeLock.release(); 286 wakeLock = null; 287 } 288 } 289 mStorageManager.incrementNumDownloadsSoFar(); 290 } 291 292 /** 293 * Fully execute a single download request. Setup and send the request, 294 * handle the response, and transfer the data to the destination file. 295 */ 296 private void executeDownload(State state) throws StopRequestException { 297 state.resetBeforeExecute(); 298 setupDestinationFile(state); 299 300 // skip when already finished; remove after fixing race in 5217390 301 if (state.mCurrentBytes == state.mTotalBytes) { 302 Log.i(Constants.TAG, "Skipping initiating request for download " + 303 mInfo.mId + "; already completed"); 304 return; 305 } 306 307 while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) { 308 // Open connection and follow any redirects until we have a useful 309 // response with body. 310 HttpURLConnection conn = null; 311 try { 312 checkConnectivity(); 313 conn = (HttpURLConnection) state.mUrl.openConnection(); 314 conn.setInstanceFollowRedirects(false); 315 conn.setConnectTimeout(DEFAULT_TIMEOUT); 316 conn.setReadTimeout(DEFAULT_TIMEOUT); 317 318 addRequestHeaders(state, conn); 319 320 final int responseCode = conn.getResponseCode(); 321 switch (responseCode) { 322 case HTTP_OK: 323 if (state.mContinuingDownload) { 324 throw new StopRequestException( 325 STATUS_CANNOT_RESUME, "Expected partial, but received OK"); 326 } 327 processResponseHeaders(state, conn); 328 transferData(state, conn); 329 return; 330 331 case HTTP_PARTIAL: 332 if (!state.mContinuingDownload) { 333 throw new StopRequestException( 334 STATUS_CANNOT_RESUME, "Expected OK, but received partial"); 335 } 336 transferData(state, conn); 337 return; 338 339 case HTTP_MOVED_PERM: 340 case HTTP_MOVED_TEMP: 341 case HTTP_SEE_OTHER: 342 case HTTP_TEMP_REDIRECT: 343 final String location = conn.getHeaderField("Location"); 344 state.mUrl = new URL(state.mUrl, location); 345 if (responseCode == HTTP_MOVED_PERM) { 346 // Push updated URL back to database 347 state.mRequestUri = state.mUrl.toString(); 348 } 349 continue; 350 351 case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: 352 throw new StopRequestException( 353 STATUS_CANNOT_RESUME, "Requested range not satisfiable"); 354 355 case HTTP_UNAVAILABLE: 356 parseRetryAfterHeaders(state, conn); 357 throw new StopRequestException( 358 HTTP_UNAVAILABLE, conn.getResponseMessage()); 359 360 case HTTP_INTERNAL_ERROR: 361 throw new StopRequestException( 362 HTTP_INTERNAL_ERROR, conn.getResponseMessage()); 363 364 default: 365 StopRequestException.throwUnhandledHttpError( 366 responseCode, conn.getResponseMessage()); 367 } 368 } catch (IOException e) { 369 // Trouble with low-level sockets 370 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 371 372 } finally { 373 if (conn != null) conn.disconnect(); 374 } 375 } 376 377 throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects"); 378 } 379 380 /** 381 * Transfer data from the given connection to the destination file. 382 */ 383 private void transferData(State state, HttpURLConnection conn) throws StopRequestException { 384 DrmManagerClient drmClient = null; 385 InputStream in = null; 386 OutputStream out = null; 387 FileDescriptor outFd = null; 388 try { 389 try { 390 in = conn.getInputStream(); 391 } catch (IOException e) { 392 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 393 } 394 395 try { 396 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) { 397 drmClient = new DrmManagerClient(mContext); 398 final RandomAccessFile file = new RandomAccessFile( 399 new File(state.mFilename), "rw"); 400 out = new DrmOutputStream(drmClient, file, state.mMimeType); 401 outFd = file.getFD(); 402 } else { 403 out = new FileOutputStream(state.mFilename, true); 404 outFd = ((FileOutputStream) out).getFD(); 405 } 406 } catch (IOException e) { 407 throw new StopRequestException(STATUS_FILE_ERROR, e); 408 } 409 410 // Start streaming data, periodically watch for pause/cancel 411 // commands and checking disk space as needed. 412 transferData(state, in, out); 413 414 try { 415 if (out instanceof DrmOutputStream) { 416 ((DrmOutputStream) out).finish(); 417 } 418 } catch (IOException e) { 419 throw new StopRequestException(STATUS_FILE_ERROR, e); 420 } 421 422 } finally { 423 if (drmClient != null) { 424 drmClient.release(); 425 } 426 427 IoUtils.closeQuietly(in); 428 429 try { 430 if (out != null) out.flush(); 431 if (outFd != null) outFd.sync(); 432 } catch (IOException e) { 433 } finally { 434 IoUtils.closeQuietly(out); 435 } 436 } 437 } 438 439 /** 440 * Check if current connectivity is valid for this request. 441 */ 442 private void checkConnectivity() throws StopRequestException { 443 // checking connectivity will apply current policy 444 mPolicyDirty = false; 445 446 final NetworkState networkUsable = mInfo.checkCanUseNetwork(); 447 if (networkUsable != NetworkState.OK) { 448 int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; 449 if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) { 450 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 451 mInfo.notifyPauseDueToSize(true); 452 } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { 453 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 454 mInfo.notifyPauseDueToSize(false); 455 } 456 throw new StopRequestException(status, networkUsable.name()); 457 } 458 } 459 460 /** 461 * Transfer as much data as possible from the HTTP response to the 462 * destination file. 463 */ 464 private void transferData(State state, InputStream in, OutputStream out) 465 throws StopRequestException { 466 final byte data[] = new byte[Constants.BUFFER_SIZE]; 467 for (;;) { 468 int bytesRead = readFromResponse(state, data, in); 469 if (bytesRead == -1) { // success, end of stream already reached 470 handleEndOfStream(state); 471 return; 472 } 473 474 state.mGotData = true; 475 writeDataToDestination(state, data, bytesRead, out); 476 state.mCurrentBytes += bytesRead; 477 reportProgress(state); 478 479 if (Constants.LOGVV) { 480 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for " 481 + mInfo.mUri); 482 } 483 484 checkPausedOrCanceled(state); 485 } 486 } 487 488 /** 489 * Called after a successful completion to take any necessary action on the downloaded file. 490 */ 491 private void finalizeDestinationFile(State state) { 492 if (state.mFilename != null) { 493 // make sure the file is readable 494 FileUtils.setPermissions(state.mFilename, 0644, -1, -1); 495 } 496 } 497 498 /** 499 * Called just before the thread finishes, regardless of status, to take any necessary action on 500 * the downloaded file. 501 */ 502 private void cleanupDestination(State state, int finalStatus) { 503 if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) { 504 if (Constants.LOGVV) { 505 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename); 506 } 507 new File(state.mFilename).delete(); 508 state.mFilename = null; 509 } 510 } 511 512 /** 513 * Check if the download has been paused or canceled, stopping the request appropriately if it 514 * has been. 515 */ 516 private void checkPausedOrCanceled(State state) throws StopRequestException { 517 synchronized (mInfo) { 518 if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) { 519 throw new StopRequestException( 520 Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner"); 521 } 522 if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) { 523 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled"); 524 } 525 } 526 527 // if policy has been changed, trigger connectivity check 528 if (mPolicyDirty) { 529 checkConnectivity(); 530 } 531 } 532 533 /** 534 * Report download progress through the database if necessary. 535 */ 536 private void reportProgress(State state) { 537 final long now = SystemClock.elapsedRealtime(); 538 539 final long sampleDelta = now - state.mSpeedSampleStart; 540 if (sampleDelta > 500) { 541 final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000) 542 / sampleDelta; 543 544 if (state.mSpeed == 0) { 545 state.mSpeed = sampleSpeed; 546 } else { 547 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4; 548 } 549 550 // Only notify once we have a full sample window 551 if (state.mSpeedSampleStart != 0) { 552 mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed); 553 } 554 555 state.mSpeedSampleStart = now; 556 state.mSpeedSampleBytes = state.mCurrentBytes; 557 } 558 559 if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP && 560 now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) { 561 ContentValues values = new ContentValues(); 562 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); 563 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 564 state.mBytesNotified = state.mCurrentBytes; 565 state.mTimeLastNotification = now; 566 } 567 } 568 569 /** 570 * Write a data buffer to the destination file. 571 * @param data buffer containing the data to write 572 * @param bytesRead how many bytes to write from the buffer 573 */ 574 private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out) 575 throws StopRequestException { 576 mStorageManager.verifySpaceBeforeWritingToFile( 577 mInfo.mDestination, state.mFilename, bytesRead); 578 579 boolean forceVerified = false; 580 while (true) { 581 try { 582 out.write(data, 0, bytesRead); 583 return; 584 } catch (IOException ex) { 585 // TODO: better differentiate between DRM and disk failures 586 if (!forceVerified) { 587 // couldn't write to file. are we out of space? check. 588 mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead); 589 forceVerified = true; 590 } else { 591 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, 592 "Failed to write data: " + ex); 593 } 594 } 595 } 596 } 597 598 /** 599 * Called when we've reached the end of the HTTP response stream, to update the database and 600 * check for consistency. 601 */ 602 private void handleEndOfStream(State state) throws StopRequestException { 603 ContentValues values = new ContentValues(); 604 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); 605 if (state.mContentLength == -1) { 606 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes); 607 } 608 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 609 610 final boolean lengthMismatched = (state.mContentLength != -1) 611 && (state.mCurrentBytes != state.mContentLength); 612 if (lengthMismatched) { 613 if (cannotResume(state)) { 614 throw new StopRequestException(STATUS_CANNOT_RESUME, 615 "mismatched content length; unable to resume"); 616 } else { 617 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, 618 "closed socket before end of file"); 619 } 620 } 621 } 622 623 private boolean cannotResume(State state) { 624 return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null) 625 || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType); 626 } 627 628 /** 629 * Read some data from the HTTP response stream, handling I/O errors. 630 * @param data buffer to use to read data 631 * @param entityStream stream for reading the HTTP response entity 632 * @return the number of bytes actually read or -1 if the end of the stream has been reached 633 */ 634 private int readFromResponse(State state, byte[] data, InputStream entityStream) 635 throws StopRequestException { 636 try { 637 return entityStream.read(data); 638 } catch (IOException ex) { 639 // TODO: handle stream errors the same as other retries 640 if ("unexpected end of stream".equals(ex.getMessage())) { 641 return -1; 642 } 643 644 ContentValues values = new ContentValues(); 645 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); 646 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 647 if (cannotResume(state)) { 648 throw new StopRequestException(STATUS_CANNOT_RESUME, 649 "Failed reading response: " + ex + "; unable to resume", ex); 650 } else { 651 throw new StopRequestException(STATUS_HTTP_DATA_ERROR, 652 "Failed reading response: " + ex, ex); 653 } 654 } 655 } 656 657 /** 658 * Prepare target file based on given network response. Derives filename and 659 * target size as needed. 660 */ 661 private void processResponseHeaders(State state, HttpURLConnection conn) 662 throws StopRequestException { 663 // TODO: fallocate the entire file if header gave us specific length 664 665 readResponseHeaders(state, conn); 666 667 state.mFilename = Helpers.generateSaveFile( 668 mContext, 669 mInfo.mUri, 670 mInfo.mHint, 671 state.mContentDisposition, 672 state.mContentLocation, 673 state.mMimeType, 674 mInfo.mDestination, 675 state.mContentLength, 676 mStorageManager); 677 678 updateDatabaseFromHeaders(state); 679 // check connectivity again now that we know the total size 680 checkConnectivity(); 681 } 682 683 /** 684 * Update necessary database fields based on values of HTTP response headers that have been 685 * read. 686 */ 687 private void updateDatabaseFromHeaders(State state) { 688 ContentValues values = new ContentValues(); 689 values.put(Downloads.Impl._DATA, state.mFilename); 690 if (state.mHeaderETag != null) { 691 values.put(Constants.ETAG, state.mHeaderETag); 692 } 693 if (state.mMimeType != null) { 694 values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType); 695 } 696 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes); 697 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 698 } 699 700 /** 701 * Read headers from the HTTP response and store them into local state. 702 */ 703 private void readResponseHeaders(State state, HttpURLConnection conn) 704 throws StopRequestException { 705 state.mContentDisposition = conn.getHeaderField("Content-Disposition"); 706 state.mContentLocation = conn.getHeaderField("Content-Location"); 707 708 if (state.mMimeType == null) { 709 state.mMimeType = Intent.normalizeMimeType(conn.getContentType()); 710 } 711 712 state.mHeaderETag = conn.getHeaderField("ETag"); 713 714 final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); 715 if (transferEncoding == null) { 716 state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1); 717 } else { 718 Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined"); 719 state.mContentLength = -1; 720 } 721 722 state.mTotalBytes = state.mContentLength; 723 mInfo.mTotalBytes = state.mContentLength; 724 725 final boolean noSizeInfo = state.mContentLength == -1 726 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked")); 727 if (!mInfo.mNoIntegrity && noSizeInfo) { 728 throw new StopRequestException(STATUS_CANNOT_RESUME, 729 "can't know size of download, giving up"); 730 } 731 } 732 733 private void parseRetryAfterHeaders(State state, HttpURLConnection conn) { 734 state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1); 735 if (state.mRetryAfter < 0) { 736 state.mRetryAfter = 0; 737 } else { 738 if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { 739 state.mRetryAfter = Constants.MIN_RETRY_AFTER; 740 } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { 741 state.mRetryAfter = Constants.MAX_RETRY_AFTER; 742 } 743 state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); 744 state.mRetryAfter *= 1000; 745 } 746 } 747 748 /** 749 * Prepare the destination file to receive data. If the file already exists, we'll set up 750 * appropriately for resumption. 751 */ 752 private void setupDestinationFile(State state) throws StopRequestException { 753 if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download 754 if (Constants.LOGV) { 755 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId + 756 ", and state.mFilename: " + state.mFilename); 757 } 758 if (!Helpers.isFilenameValid(state.mFilename, 759 mStorageManager.getDownloadDataDirectory())) { 760 // this should never happen 761 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, 762 "found invalid internal destination filename"); 763 } 764 // We're resuming a download that got interrupted 765 File f = new File(state.mFilename); 766 if (f.exists()) { 767 if (Constants.LOGV) { 768 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 769 ", and state.mFilename: " + state.mFilename); 770 } 771 long fileLength = f.length(); 772 if (fileLength == 0) { 773 // The download hadn't actually started, we can restart from scratch 774 if (Constants.LOGVV) { 775 Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting " 776 + state.mFilename); 777 } 778 f.delete(); 779 state.mFilename = null; 780 if (Constants.LOGV) { 781 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 782 ", BUT starting from scratch again: "); 783 } 784 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) { 785 // This should've been caught upon failure 786 if (Constants.LOGVV) { 787 Log.d(TAG, "setupDestinationFile() unable to resume download, deleting " 788 + state.mFilename); 789 } 790 f.delete(); 791 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, 792 "Trying to resume a download that can't be resumed"); 793 } else { 794 // All right, we'll be able to resume this download 795 if (Constants.LOGV) { 796 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 797 ", and starting with file of length: " + fileLength); 798 } 799 state.mCurrentBytes = (int) fileLength; 800 if (mInfo.mTotalBytes != -1) { 801 state.mContentLength = mInfo.mTotalBytes; 802 } 803 state.mHeaderETag = mInfo.mETag; 804 state.mContinuingDownload = true; 805 if (Constants.LOGV) { 806 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 807 ", state.mCurrentBytes: " + state.mCurrentBytes + 808 ", and setting mContinuingDownload to true: "); 809 } 810 } 811 } 812 } 813 } 814 815 /** 816 * Add custom headers for this download to the HTTP request. 817 */ 818 private void addRequestHeaders(State state, HttpURLConnection conn) { 819 for (Pair<String, String> header : mInfo.getHeaders()) { 820 conn.addRequestProperty(header.first, header.second); 821 } 822 823 // Only splice in user agent when not already defined 824 if (conn.getRequestProperty("User-Agent") == null) { 825 conn.addRequestProperty("User-Agent", userAgent()); 826 } 827 828 // Defeat transparent gzip compression, since it doesn't allow us to 829 // easily resume partial downloads. 830 conn.setRequestProperty("Accept-Encoding", "identity"); 831 832 if (state.mContinuingDownload) { 833 if (state.mHeaderETag != null) { 834 conn.addRequestProperty("If-Match", state.mHeaderETag); 835 } 836 conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-"); 837 } 838 } 839 840 /** 841 * Stores information about the completed download, and notifies the initiating application. 842 */ 843 private void notifyDownloadCompleted( 844 State state, int finalStatus, String errorMsg, int numFailed) { 845 notifyThroughDatabase(state, finalStatus, errorMsg, numFailed); 846 if (Downloads.Impl.isStatusCompleted(finalStatus)) { 847 mInfo.sendIntentIfRequested(); 848 } 849 } 850 851 private void notifyThroughDatabase( 852 State state, int finalStatus, String errorMsg, int numFailed) { 853 ContentValues values = new ContentValues(); 854 values.put(Downloads.Impl.COLUMN_STATUS, finalStatus); 855 values.put(Downloads.Impl._DATA, state.mFilename); 856 values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType); 857 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis()); 858 values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed); 859 values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter); 860 861 if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) { 862 values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri); 863 } 864 865 // save the error message. could be useful to developers. 866 if (!TextUtils.isEmpty(errorMsg)) { 867 values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg); 868 } 869 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 870 } 871 872 private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() { 873 @Override 874 public void onUidRulesChanged(int uid, int uidRules) { 875 // caller is NPMS, since we only register with them 876 if (uid == mInfo.mUid) { 877 mPolicyDirty = true; 878 } 879 } 880 881 @Override 882 public void onMeteredIfacesChanged(String[] meteredIfaces) { 883 // caller is NPMS, since we only register with them 884 mPolicyDirty = true; 885 } 886 887 @Override 888 public void onRestrictBackgroundChanged(boolean restrictBackground) { 889 // caller is NPMS, since we only register with them 890 mPolicyDirty = true; 891 } 892 }; 893 894 public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { 895 try { 896 return Long.parseLong(conn.getHeaderField(field)); 897 } catch (NumberFormatException e) { 898 return defaultValue; 899 } 900 } 901 902 /** 903 * Return if given status is eligible to be treated as 904 * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}. 905 */ 906 public static boolean isStatusRetryable(int status) { 907 switch (status) { 908 case STATUS_HTTP_DATA_ERROR: 909 case HTTP_UNAVAILABLE: 910 case HTTP_INTERNAL_ERROR: 911 return true; 912 default: 913 return false; 914 } 915 } 916} 917