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