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