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 com.android.providers.downloads.Constants.TAG; 20 21import android.content.ContentValues; 22import android.content.Context; 23import android.content.Intent; 24import android.net.INetworkPolicyListener; 25import android.net.NetworkPolicyManager; 26import android.net.Proxy; 27import android.net.TrafficStats; 28import android.net.http.AndroidHttpClient; 29import android.os.FileUtils; 30import android.os.PowerManager; 31import android.os.Process; 32import android.os.SystemClock; 33import android.provider.Downloads; 34import android.text.TextUtils; 35import android.util.Log; 36import android.util.Pair; 37 38import org.apache.http.Header; 39import org.apache.http.HttpResponse; 40import org.apache.http.client.methods.HttpGet; 41import org.apache.http.conn.params.ConnRouteParams; 42 43import java.io.File; 44import java.io.FileNotFoundException; 45import java.io.FileOutputStream; 46import java.io.IOException; 47import java.io.InputStream; 48import java.io.SyncFailedException; 49import java.net.URI; 50import java.net.URISyntaxException; 51 52/** 53 * Runs an actual download 54 */ 55public class DownloadThread extends Thread { 56 57 private final Context mContext; 58 private final DownloadInfo mInfo; 59 private final SystemFacade mSystemFacade; 60 private final StorageManager mStorageManager; 61 private DrmConvertSession mDrmConvertSession; 62 63 private volatile boolean mPolicyDirty; 64 65 public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info, 66 StorageManager storageManager) { 67 mContext = context; 68 mSystemFacade = systemFacade; 69 mInfo = info; 70 mStorageManager = storageManager; 71 } 72 73 /** 74 * Returns the user agent provided by the initiating app, or use the default one 75 */ 76 private String userAgent() { 77 String userAgent = mInfo.mUserAgent; 78 if (userAgent == null) { 79 userAgent = Constants.DEFAULT_USER_AGENT; 80 } 81 return userAgent; 82 } 83 84 /** 85 * State for the entire run() method. 86 */ 87 static class State { 88 public String mFilename; 89 public FileOutputStream mStream; 90 public String mMimeType; 91 public boolean mCountRetry = false; 92 public int mRetryAfter = 0; 93 public int mRedirectCount = 0; 94 public String mNewUri; 95 public boolean mGotData = false; 96 public String mRequestUri; 97 public long mTotalBytes = -1; 98 public long mCurrentBytes = 0; 99 public String mHeaderETag; 100 public boolean mContinuingDownload = false; 101 public long mBytesNotified = 0; 102 public long mTimeLastNotification = 0; 103 104 /** Historical bytes/second speed of this download. */ 105 public long mSpeed; 106 /** Time when current sample started. */ 107 public long mSpeedSampleStart; 108 /** Bytes transferred since current sample started. */ 109 public long mSpeedSampleBytes; 110 111 public State(DownloadInfo info) { 112 mMimeType = Intent.normalizeMimeType(info.mMimeType); 113 mRequestUri = info.mUri; 114 mFilename = info.mFileName; 115 mTotalBytes = info.mTotalBytes; 116 mCurrentBytes = info.mCurrentBytes; 117 } 118 } 119 120 /** 121 * State within executeDownload() 122 */ 123 private static class InnerState { 124 public String mHeaderContentLength; 125 public String mHeaderContentDisposition; 126 public String mHeaderContentLocation; 127 } 128 129 /** 130 * Raised from methods called by executeDownload() to indicate that the download should be 131 * retried immediately. 132 */ 133 private class RetryDownload extends Throwable {} 134 135 /** 136 * Executes the download in a separate thread 137 */ 138 @Override 139 public void run() { 140 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 141 try { 142 runInternal(); 143 } finally { 144 DownloadHandler.getInstance().dequeueDownload(mInfo.mId); 145 } 146 } 147 148 private void runInternal() { 149 // Skip when download already marked as finished; this download was 150 // probably started again while racing with UpdateThread. 151 if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId) 152 == Downloads.Impl.STATUS_SUCCESS) { 153 Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping"); 154 return; 155 } 156 157 State state = new State(mInfo); 158 AndroidHttpClient client = null; 159 PowerManager.WakeLock wakeLock = null; 160 int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; 161 String errorMsg = null; 162 163 final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext); 164 final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 165 166 try { 167 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); 168 wakeLock.acquire(); 169 170 // while performing download, register for rules updates 171 netPolicy.registerListener(mPolicyListener); 172 173 if (Constants.LOGV) { 174 Log.v(Constants.TAG, "initiating download for " + mInfo.mUri); 175 } 176 177 client = AndroidHttpClient.newInstance(userAgent(), mContext); 178 179 // network traffic on this thread should be counted against the 180 // requesting uid, and is tagged with well-known value. 181 TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD); 182 TrafficStats.setThreadStatsUid(mInfo.mUid); 183 184 boolean finished = false; 185 while(!finished) { 186 Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId); 187 // Set or unset proxy, which may have changed since last GET request. 188 // setDefaultProxy() supports null as proxy parameter. 189 ConnRouteParams.setDefaultProxy(client.getParams(), 190 Proxy.getPreferredHttpHost(mContext, state.mRequestUri)); 191 HttpGet request = new HttpGet(state.mRequestUri); 192 try { 193 executeDownload(state, client, request); 194 finished = true; 195 } catch (RetryDownload exc) { 196 // fall through 197 } finally { 198 request.abort(); 199 request = null; 200 } 201 } 202 203 if (Constants.LOGV) { 204 Log.v(Constants.TAG, "download completed for " + mInfo.mUri); 205 } 206 finalizeDestinationFile(state); 207 finalStatus = Downloads.Impl.STATUS_SUCCESS; 208 } catch (StopRequestException error) { 209 // remove the cause before printing, in case it contains PII 210 errorMsg = error.getMessage(); 211 String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg; 212 Log.w(Constants.TAG, msg); 213 if (Constants.LOGV) { 214 Log.w(Constants.TAG, msg, error); 215 } 216 finalStatus = error.mFinalStatus; 217 // fall through to finally block 218 } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions 219 errorMsg = ex.getMessage(); 220 String msg = "Exception for id " + mInfo.mId + ": " + errorMsg; 221 Log.w(Constants.TAG, msg, ex); 222 finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; 223 // falls through to the code that reports an error 224 } finally { 225 TrafficStats.clearThreadStatsTag(); 226 TrafficStats.clearThreadStatsUid(); 227 228 if (client != null) { 229 client.close(); 230 client = null; 231 } 232 cleanupDestination(state, finalStatus); 233 notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, 234 state.mGotData, state.mFilename, 235 state.mNewUri, state.mMimeType, errorMsg); 236 237 netPolicy.unregisterListener(mPolicyListener); 238 239 if (wakeLock != null) { 240 wakeLock.release(); 241 wakeLock = null; 242 } 243 } 244 mStorageManager.incrementNumDownloadsSoFar(); 245 } 246 247 /** 248 * Fully execute a single download request - setup and send the request, handle the response, 249 * and transfer the data to the destination file. 250 */ 251 private void executeDownload(State state, AndroidHttpClient client, HttpGet request) 252 throws StopRequestException, RetryDownload { 253 InnerState innerState = new InnerState(); 254 byte data[] = new byte[Constants.BUFFER_SIZE]; 255 256 setupDestinationFile(state, innerState); 257 addRequestHeaders(state, request); 258 259 // skip when already finished; remove after fixing race in 5217390 260 if (state.mCurrentBytes == state.mTotalBytes) { 261 Log.i(Constants.TAG, "Skipping initiating request for download " + 262 mInfo.mId + "; already completed"); 263 return; 264 } 265 266 // check just before sending the request to avoid using an invalid connection at all 267 checkConnectivity(); 268 269 HttpResponse response = sendRequest(state, client, request); 270 handleExceptionalStatus(state, innerState, response); 271 272 if (Constants.LOGV) { 273 Log.v(Constants.TAG, "received response for " + mInfo.mUri); 274 } 275 276 processResponseHeaders(state, innerState, response); 277 InputStream entityStream = openResponseEntity(state, response); 278 transferData(state, innerState, data, entityStream); 279 } 280 281 /** 282 * Check if current connectivity is valid for this request. 283 */ 284 private void checkConnectivity() throws StopRequestException { 285 // checking connectivity will apply current policy 286 mPolicyDirty = false; 287 288 int networkUsable = mInfo.checkCanUseNetwork(); 289 if (networkUsable != DownloadInfo.NETWORK_OK) { 290 int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; 291 if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) { 292 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 293 mInfo.notifyPauseDueToSize(true); 294 } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { 295 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 296 mInfo.notifyPauseDueToSize(false); 297 } 298 throw new StopRequestException(status, 299 mInfo.getLogMessageForNetworkError(networkUsable)); 300 } 301 } 302 303 /** 304 * Transfer as much data as possible from the HTTP response to the destination file. 305 * @param data buffer to use to read data 306 * @param entityStream stream for reading the HTTP response entity 307 */ 308 private void transferData( 309 State state, InnerState innerState, byte[] data, InputStream entityStream) 310 throws StopRequestException { 311 for (;;) { 312 int bytesRead = readFromResponse(state, innerState, data, entityStream); 313 if (bytesRead == -1) { // success, end of stream already reached 314 handleEndOfStream(state, innerState); 315 return; 316 } 317 318 state.mGotData = true; 319 writeDataToDestination(state, data, bytesRead); 320 state.mCurrentBytes += bytesRead; 321 reportProgress(state, innerState); 322 323 if (Constants.LOGVV) { 324 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for " 325 + mInfo.mUri); 326 } 327 328 checkPausedOrCanceled(state); 329 } 330 } 331 332 /** 333 * Called after a successful completion to take any necessary action on the downloaded file. 334 */ 335 private void finalizeDestinationFile(State state) throws StopRequestException { 336 if (state.mFilename != null) { 337 // make sure the file is readable 338 FileUtils.setPermissions(state.mFilename, 0644, -1, -1); 339 syncDestination(state); 340 } 341 } 342 343 /** 344 * Called just before the thread finishes, regardless of status, to take any necessary action on 345 * the downloaded file. 346 */ 347 private void cleanupDestination(State state, int finalStatus) { 348 if (mDrmConvertSession != null) { 349 finalStatus = mDrmConvertSession.close(state.mFilename); 350 } 351 352 closeDestination(state); 353 if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) { 354 if (Constants.LOGVV) { 355 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename); 356 } 357 new File(state.mFilename).delete(); 358 state.mFilename = null; 359 } 360 } 361 362 /** 363 * Sync the destination file to storage. 364 */ 365 private void syncDestination(State state) { 366 FileOutputStream downloadedFileStream = null; 367 try { 368 downloadedFileStream = new FileOutputStream(state.mFilename, true); 369 downloadedFileStream.getFD().sync(); 370 } catch (FileNotFoundException ex) { 371 Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex); 372 } catch (SyncFailedException ex) { 373 Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex); 374 } catch (IOException ex) { 375 Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex); 376 } catch (RuntimeException ex) { 377 Log.w(Constants.TAG, "exception while syncing file: ", ex); 378 } finally { 379 if(downloadedFileStream != null) { 380 try { 381 downloadedFileStream.close(); 382 } catch (IOException ex) { 383 Log.w(Constants.TAG, "IOException while closing synced file: ", ex); 384 } catch (RuntimeException ex) { 385 Log.w(Constants.TAG, "exception while closing file: ", ex); 386 } 387 } 388 } 389 } 390 391 /** 392 * Close the destination output stream. 393 */ 394 private void closeDestination(State state) { 395 try { 396 // close the file 397 if (state.mStream != null) { 398 state.mStream.close(); 399 state.mStream = null; 400 } 401 } catch (IOException ex) { 402 if (Constants.LOGV) { 403 Log.v(Constants.TAG, "exception when closing the file after download : " + ex); 404 } 405 // nothing can really be done if the file can't be closed 406 } 407 } 408 409 /** 410 * Check if the download has been paused or canceled, stopping the request appropriately if it 411 * has been. 412 */ 413 private void checkPausedOrCanceled(State state) throws StopRequestException { 414 synchronized (mInfo) { 415 if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) { 416 throw new StopRequestException( 417 Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner"); 418 } 419 if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) { 420 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled"); 421 } 422 } 423 424 // if policy has been changed, trigger connectivity check 425 if (mPolicyDirty) { 426 checkConnectivity(); 427 } 428 } 429 430 /** 431 * Report download progress through the database if necessary. 432 */ 433 private void reportProgress(State state, InnerState innerState) { 434 final long now = SystemClock.elapsedRealtime(); 435 436 final long sampleDelta = now - state.mSpeedSampleStart; 437 if (sampleDelta > 500) { 438 final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000) 439 / sampleDelta; 440 441 if (state.mSpeed == 0) { 442 state.mSpeed = sampleSpeed; 443 } else { 444 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4; 445 } 446 447 state.mSpeedSampleStart = now; 448 state.mSpeedSampleBytes = state.mCurrentBytes; 449 450 DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed); 451 } 452 453 if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP && 454 now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) { 455 ContentValues values = new ContentValues(); 456 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); 457 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 458 state.mBytesNotified = state.mCurrentBytes; 459 state.mTimeLastNotification = now; 460 } 461 } 462 463 /** 464 * Write a data buffer to the destination file. 465 * @param data buffer containing the data to write 466 * @param bytesRead how many bytes to write from the buffer 467 */ 468 private void writeDataToDestination(State state, byte[] data, int bytesRead) 469 throws StopRequestException { 470 for (;;) { 471 try { 472 if (state.mStream == null) { 473 state.mStream = new FileOutputStream(state.mFilename, true); 474 } 475 mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename, 476 bytesRead); 477 if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) { 478 state.mStream.write(data, 0, bytesRead); 479 } else { 480 byte[] convertedData = mDrmConvertSession.convert(data, bytesRead); 481 if (convertedData != null) { 482 state.mStream.write(convertedData, 0, convertedData.length); 483 } else { 484 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, 485 "Error converting drm data."); 486 } 487 } 488 return; 489 } catch (IOException ex) { 490 // couldn't write to file. are we out of space? check. 491 // TODO this check should only be done once. why is this being done 492 // in a while(true) loop (see the enclosing statement: for(;;) 493 if (state.mStream != null) { 494 mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead); 495 } 496 } finally { 497 if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) { 498 closeDestination(state); 499 } 500 } 501 } 502 } 503 504 /** 505 * Called when we've reached the end of the HTTP response stream, to update the database and 506 * check for consistency. 507 */ 508 private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException { 509 ContentValues values = new ContentValues(); 510 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); 511 if (innerState.mHeaderContentLength == null) { 512 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes); 513 } 514 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 515 516 boolean lengthMismatched = (innerState.mHeaderContentLength != null) 517 && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength)); 518 if (lengthMismatched) { 519 if (cannotResume(state)) { 520 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, 521 "mismatched content length"); 522 } else { 523 throw new StopRequestException(getFinalStatusForHttpError(state), 524 "closed socket before end of file"); 525 } 526 } 527 } 528 529 private boolean cannotResume(State state) { 530 return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null; 531 } 532 533 /** 534 * Read some data from the HTTP response stream, handling I/O errors. 535 * @param data buffer to use to read data 536 * @param entityStream stream for reading the HTTP response entity 537 * @return the number of bytes actually read or -1 if the end of the stream has been reached 538 */ 539 private int readFromResponse(State state, InnerState innerState, byte[] data, 540 InputStream entityStream) throws StopRequestException { 541 try { 542 return entityStream.read(data); 543 } catch (IOException ex) { 544 logNetworkState(mInfo.mUid); 545 ContentValues values = new ContentValues(); 546 values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); 547 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 548 if (cannotResume(state)) { 549 String message = "while reading response: " + ex.toString() 550 + ", can't resume interrupted download with no ETag"; 551 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, 552 message, ex); 553 } else { 554 throw new StopRequestException(getFinalStatusForHttpError(state), 555 "while reading response: " + ex.toString(), ex); 556 } 557 } 558 } 559 560 /** 561 * Open a stream for the HTTP response entity, handling I/O errors. 562 * @return an InputStream to read the response entity 563 */ 564 private InputStream openResponseEntity(State state, HttpResponse response) 565 throws StopRequestException { 566 try { 567 return response.getEntity().getContent(); 568 } catch (IOException ex) { 569 logNetworkState(mInfo.mUid); 570 throw new StopRequestException(getFinalStatusForHttpError(state), 571 "while getting entity: " + ex.toString(), ex); 572 } 573 } 574 575 private void logNetworkState(int uid) { 576 if (Constants.LOGX) { 577 Log.i(Constants.TAG, 578 "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down")); 579 } 580 } 581 582 /** 583 * Read HTTP response headers and take appropriate action, including setting up the destination 584 * file and updating the database. 585 */ 586 private void processResponseHeaders(State state, InnerState innerState, HttpResponse response) 587 throws StopRequestException { 588 if (state.mContinuingDownload) { 589 // ignore response headers on resume requests 590 return; 591 } 592 593 readResponseHeaders(state, innerState, response); 594 if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) { 595 mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType); 596 if (mDrmConvertSession == null) { 597 throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype " 598 + state.mMimeType + " can not be converted."); 599 } 600 } 601 602 state.mFilename = Helpers.generateSaveFile( 603 mContext, 604 mInfo.mUri, 605 mInfo.mHint, 606 innerState.mHeaderContentDisposition, 607 innerState.mHeaderContentLocation, 608 state.mMimeType, 609 mInfo.mDestination, 610 (innerState.mHeaderContentLength != null) ? 611 Long.parseLong(innerState.mHeaderContentLength) : 0, 612 mInfo.mIsPublicApi, mStorageManager); 613 try { 614 state.mStream = new FileOutputStream(state.mFilename); 615 } catch (FileNotFoundException exc) { 616 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, 617 "while opening destination file: " + exc.toString(), exc); 618 } 619 if (Constants.LOGV) { 620 Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename); 621 } 622 623 updateDatabaseFromHeaders(state, innerState); 624 // check connectivity again now that we know the total size 625 checkConnectivity(); 626 } 627 628 /** 629 * Update necessary database fields based on values of HTTP response headers that have been 630 * read. 631 */ 632 private void updateDatabaseFromHeaders(State state, InnerState innerState) { 633 ContentValues values = new ContentValues(); 634 values.put(Downloads.Impl._DATA, state.mFilename); 635 if (state.mHeaderETag != null) { 636 values.put(Constants.ETAG, state.mHeaderETag); 637 } 638 if (state.mMimeType != null) { 639 values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType); 640 } 641 values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes); 642 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 643 } 644 645 /** 646 * Read headers from the HTTP response and store them into local state. 647 */ 648 private void readResponseHeaders(State state, InnerState innerState, HttpResponse response) 649 throws StopRequestException { 650 Header header = response.getFirstHeader("Content-Disposition"); 651 if (header != null) { 652 innerState.mHeaderContentDisposition = header.getValue(); 653 } 654 header = response.getFirstHeader("Content-Location"); 655 if (header != null) { 656 innerState.mHeaderContentLocation = header.getValue(); 657 } 658 if (state.mMimeType == null) { 659 header = response.getFirstHeader("Content-Type"); 660 if (header != null) { 661 state.mMimeType = Intent.normalizeMimeType(header.getValue()); 662 } 663 } 664 header = response.getFirstHeader("ETag"); 665 if (header != null) { 666 state.mHeaderETag = header.getValue(); 667 } 668 String headerTransferEncoding = null; 669 header = response.getFirstHeader("Transfer-Encoding"); 670 if (header != null) { 671 headerTransferEncoding = header.getValue(); 672 } 673 if (headerTransferEncoding == null) { 674 header = response.getFirstHeader("Content-Length"); 675 if (header != null) { 676 innerState.mHeaderContentLength = header.getValue(); 677 state.mTotalBytes = mInfo.mTotalBytes = 678 Long.parseLong(innerState.mHeaderContentLength); 679 } 680 } else { 681 // Ignore content-length with transfer-encoding - 2616 4.4 3 682 if (Constants.LOGVV) { 683 Log.v(Constants.TAG, 684 "ignoring content-length because of xfer-encoding"); 685 } 686 } 687 if (Constants.LOGVV) { 688 Log.v(Constants.TAG, "Content-Disposition: " + 689 innerState.mHeaderContentDisposition); 690 Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength); 691 Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation); 692 Log.v(Constants.TAG, "Content-Type: " + state.mMimeType); 693 Log.v(Constants.TAG, "ETag: " + state.mHeaderETag); 694 Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding); 695 } 696 697 boolean noSizeInfo = innerState.mHeaderContentLength == null 698 && (headerTransferEncoding == null 699 || !headerTransferEncoding.equalsIgnoreCase("chunked")); 700 if (!mInfo.mNoIntegrity && noSizeInfo) { 701 throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, 702 "can't know size of download, giving up"); 703 } 704 } 705 706 /** 707 * Check the HTTP response status and handle anything unusual (e.g. not 200/206). 708 */ 709 private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response) 710 throws StopRequestException, RetryDownload { 711 int statusCode = response.getStatusLine().getStatusCode(); 712 if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { 713 handleServiceUnavailable(state, response); 714 } 715 if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) { 716 handleRedirect(state, response, statusCode); 717 } 718 719 if (Constants.LOGV) { 720 Log.i(Constants.TAG, "recevd_status = " + statusCode + 721 ", mContinuingDownload = " + state.mContinuingDownload); 722 } 723 int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS; 724 if (statusCode != expectedStatus) { 725 handleOtherStatus(state, innerState, statusCode); 726 } 727 } 728 729 /** 730 * Handle a status that we don't know how to deal with properly. 731 */ 732 private void handleOtherStatus(State state, InnerState innerState, int statusCode) 733 throws StopRequestException { 734 if (statusCode == 416) { 735 // range request failed. it should never fail. 736 throw new IllegalStateException("Http Range request failure: totalBytes = " + 737 state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes); 738 } 739 int finalStatus; 740 if (Downloads.Impl.isStatusError(statusCode)) { 741 finalStatus = statusCode; 742 } else if (statusCode >= 300 && statusCode < 400) { 743 finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT; 744 } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) { 745 finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME; 746 } else { 747 finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; 748 } 749 throw new StopRequestException(finalStatus, "http error " + 750 statusCode + ", mContinuingDownload: " + state.mContinuingDownload); 751 } 752 753 /** 754 * Handle a 3xx redirect status. 755 */ 756 private void handleRedirect(State state, HttpResponse response, int statusCode) 757 throws StopRequestException, RetryDownload { 758 if (Constants.LOGVV) { 759 Log.v(Constants.TAG, "got HTTP redirect " + statusCode); 760 } 761 if (state.mRedirectCount >= Constants.MAX_REDIRECTS) { 762 throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS, 763 "too many redirects"); 764 } 765 Header header = response.getFirstHeader("Location"); 766 if (header == null) { 767 return; 768 } 769 if (Constants.LOGVV) { 770 Log.v(Constants.TAG, "Location :" + header.getValue()); 771 } 772 773 String newUri; 774 try { 775 newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString(); 776 } catch(URISyntaxException ex) { 777 if (Constants.LOGV) { 778 Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue() 779 + " for " + mInfo.mUri); 780 } 781 throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, 782 "Couldn't resolve redirect URI"); 783 } 784 ++state.mRedirectCount; 785 state.mRequestUri = newUri; 786 if (statusCode == 301 || statusCode == 303) { 787 // use the new URI for all future requests (should a retry/resume be necessary) 788 state.mNewUri = newUri; 789 } 790 throw new RetryDownload(); 791 } 792 793 /** 794 * Handle a 503 Service Unavailable status by processing the Retry-After header. 795 */ 796 private void handleServiceUnavailable(State state, HttpResponse response) 797 throws StopRequestException { 798 if (Constants.LOGVV) { 799 Log.v(Constants.TAG, "got HTTP response code 503"); 800 } 801 state.mCountRetry = true; 802 Header header = response.getFirstHeader("Retry-After"); 803 if (header != null) { 804 try { 805 if (Constants.LOGVV) { 806 Log.v(Constants.TAG, "Retry-After :" + header.getValue()); 807 } 808 state.mRetryAfter = Integer.parseInt(header.getValue()); 809 if (state.mRetryAfter < 0) { 810 state.mRetryAfter = 0; 811 } else { 812 if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { 813 state.mRetryAfter = Constants.MIN_RETRY_AFTER; 814 } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { 815 state.mRetryAfter = Constants.MAX_RETRY_AFTER; 816 } 817 state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); 818 state.mRetryAfter *= 1000; 819 } 820 } catch (NumberFormatException ex) { 821 // ignored - retryAfter stays 0 in this case. 822 } 823 } 824 throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY, 825 "got 503 Service Unavailable, will retry later"); 826 } 827 828 /** 829 * Send the request to the server, handling any I/O exceptions. 830 */ 831 private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request) 832 throws StopRequestException { 833 try { 834 return client.execute(request); 835 } catch (IllegalArgumentException ex) { 836 throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, 837 "while trying to execute request: " + ex.toString(), ex); 838 } catch (IOException ex) { 839 logNetworkState(mInfo.mUid); 840 throw new StopRequestException(getFinalStatusForHttpError(state), 841 "while trying to execute request: " + ex.toString(), ex); 842 } 843 } 844 845 private int getFinalStatusForHttpError(State state) { 846 int networkUsable = mInfo.checkCanUseNetwork(); 847 if (networkUsable != DownloadInfo.NETWORK_OK) { 848 switch (networkUsable) { 849 case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE: 850 case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: 851 return Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 852 default: 853 return Downloads.Impl.STATUS_WAITING_FOR_NETWORK; 854 } 855 } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) { 856 state.mCountRetry = true; 857 return Downloads.Impl.STATUS_WAITING_TO_RETRY; 858 } else { 859 Log.w(Constants.TAG, "reached max retries for " + mInfo.mId); 860 return Downloads.Impl.STATUS_HTTP_DATA_ERROR; 861 } 862 } 863 864 /** 865 * Prepare the destination file to receive data. If the file already exists, we'll set up 866 * appropriately for resumption. 867 */ 868 private void setupDestinationFile(State state, InnerState innerState) 869 throws StopRequestException { 870 if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download 871 if (Constants.LOGV) { 872 Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId + 873 ", and state.mFilename: " + state.mFilename); 874 } 875 if (!Helpers.isFilenameValid(state.mFilename, 876 mStorageManager.getDownloadDataDirectory())) { 877 // this should never happen 878 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, 879 "found invalid internal destination filename"); 880 } 881 // We're resuming a download that got interrupted 882 File f = new File(state.mFilename); 883 if (f.exists()) { 884 if (Constants.LOGV) { 885 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 886 ", and state.mFilename: " + state.mFilename); 887 } 888 long fileLength = f.length(); 889 if (fileLength == 0) { 890 // The download hadn't actually started, we can restart from scratch 891 if (Constants.LOGVV) { 892 Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting " 893 + state.mFilename); 894 } 895 f.delete(); 896 state.mFilename = null; 897 if (Constants.LOGV) { 898 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 899 ", BUT starting from scratch again: "); 900 } 901 } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) { 902 // This should've been caught upon failure 903 if (Constants.LOGVV) { 904 Log.d(TAG, "setupDestinationFile() unable to resume download, deleting " 905 + state.mFilename); 906 } 907 f.delete(); 908 throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, 909 "Trying to resume a download that can't be resumed"); 910 } else { 911 // All right, we'll be able to resume this download 912 if (Constants.LOGV) { 913 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 914 ", and starting with file of length: " + fileLength); 915 } 916 try { 917 state.mStream = new FileOutputStream(state.mFilename, true); 918 } catch (FileNotFoundException exc) { 919 throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, 920 "while opening destination for resuming: " + exc.toString(), exc); 921 } 922 state.mCurrentBytes = (int) fileLength; 923 if (mInfo.mTotalBytes != -1) { 924 innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes); 925 } 926 state.mHeaderETag = mInfo.mETag; 927 state.mContinuingDownload = true; 928 if (Constants.LOGV) { 929 Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + 930 ", state.mCurrentBytes: " + state.mCurrentBytes + 931 ", and setting mContinuingDownload to true: "); 932 } 933 } 934 } 935 } 936 937 if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) { 938 closeDestination(state); 939 } 940 } 941 942 /** 943 * Add custom headers for this download to the HTTP request. 944 */ 945 private void addRequestHeaders(State state, HttpGet request) { 946 for (Pair<String, String> header : mInfo.getHeaders()) { 947 request.addHeader(header.first, header.second); 948 } 949 950 if (state.mContinuingDownload) { 951 if (state.mHeaderETag != null) { 952 request.addHeader("If-Match", state.mHeaderETag); 953 } 954 request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-"); 955 if (Constants.LOGV) { 956 Log.i(Constants.TAG, "Adding Range header: " + 957 "bytes=" + state.mCurrentBytes + "-"); 958 Log.i(Constants.TAG, " totalBytes = " + state.mTotalBytes); 959 } 960 } 961 } 962 963 /** 964 * Stores information about the completed download, and notifies the initiating application. 965 */ 966 private void notifyDownloadCompleted( 967 int status, boolean countRetry, int retryAfter, boolean gotData, 968 String filename, String uri, String mimeType, String errorMsg) { 969 notifyThroughDatabase( 970 status, countRetry, retryAfter, gotData, filename, uri, mimeType, 971 errorMsg); 972 if (Downloads.Impl.isStatusCompleted(status)) { 973 mInfo.sendIntentIfRequested(); 974 } 975 } 976 977 private void notifyThroughDatabase( 978 int status, boolean countRetry, int retryAfter, boolean gotData, 979 String filename, String uri, String mimeType, String errorMsg) { 980 ContentValues values = new ContentValues(); 981 values.put(Downloads.Impl.COLUMN_STATUS, status); 982 values.put(Downloads.Impl._DATA, filename); 983 if (uri != null) { 984 values.put(Downloads.Impl.COLUMN_URI, uri); 985 } 986 values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType); 987 values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis()); 988 values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter); 989 if (!countRetry) { 990 values.put(Constants.FAILED_CONNECTIONS, 0); 991 } else if (gotData) { 992 values.put(Constants.FAILED_CONNECTIONS, 1); 993 } else { 994 values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1); 995 } 996 // save the error message. could be useful to developers. 997 if (!TextUtils.isEmpty(errorMsg)) { 998 values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg); 999 } 1000 mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); 1001 } 1002 1003 private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() { 1004 @Override 1005 public void onUidRulesChanged(int uid, int uidRules) { 1006 // caller is NPMS, since we only register with them 1007 if (uid == mInfo.mUid) { 1008 mPolicyDirty = true; 1009 } 1010 } 1011 1012 @Override 1013 public void onMeteredIfacesChanged(String[] meteredIfaces) { 1014 // caller is NPMS, since we only register with them 1015 mPolicyDirty = true; 1016 } 1017 1018 @Override 1019 public void onRestrictBackgroundChanged(boolean restrictBackground) { 1020 // caller is NPMS, since we only register with them 1021 mPolicyDirty = true; 1022 } 1023 }; 1024} 1025