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 DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info, StorageManager storageManager)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 */ userAgent()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 State(DownloadInfo info)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 run()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 */ executeDownload(State state, AndroidHttpClient client, HttpGet request)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 */ checkConnectivity()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 */ transferData( State state, InnerState innerState, byte[] data, InputStream entityStream)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 */ finalizeDestinationFile(State state)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 */ cleanupDestination(State state, int finalStatus)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 */ syncDestination(State state)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 */ closeDestination(State state)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 */ checkPausedOrCanceled(State state)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 */ reportProgress(State state, InnerState innerState)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 */ writeDataToDestination(State state, byte[] data, int bytesRead)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 */ handleEndOfStream(State state, InnerState innerState)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 cannotResume(State state)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 */ readFromResponse(State state, InnerState innerState, byte[] data, InputStream entityStream)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 */ openResponseEntity(State state, HttpResponse response)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 logNetworkState(int uid)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 */ processResponseHeaders(State state, InnerState innerState, HttpResponse response)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 */ updateDatabaseFromHeaders(State state, InnerState innerState)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 */ readResponseHeaders(State state, InnerState innerState, HttpResponse response)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 */ handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)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 */ handleOtherStatus(State state, InnerState innerState, int statusCode)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 */ handleRedirect(State state, HttpResponse response, int statusCode)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 */ handleServiceUnavailable(State state, HttpResponse response)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 */ sendRequest(State state, AndroidHttpClient client, HttpGet request)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 getFinalStatusForHttpError(State state)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 */ setupDestinationFile(State state, InnerState innerState)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 */ addRequestHeaders(State state, HttpGet request)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 */ notifyDownloadCompleted( int status, boolean countRetry, int retryAfter, boolean gotData, String filename, String uri, String mimeType, String errorMsg)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 notifyThroughDatabase( int status, boolean countRetry, int retryAfter, boolean gotData, String filename, String uri, String mimeType, String errorMsg)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