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