summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSteve Howard <showard@google.com>2010-07-16 14:28:35 -0700
committerSteve Howard <showard@google.com>2010-07-20 15:47:43 -0700
commitb5629da794cb3c1ca1970d206343743b165b9644 (patch)
tree53703ac7a0ef36f9c3eaa2fc44bb6a43754a3b7f /src
parent5180de23e77139dd6971dfd48269242e3e3562d9 (diff)
downloadandroid_packages_providers_DownloadProvider-b5629da794cb3c1ca1970d206343743b165b9644.tar.gz
android_packages_providers_DownloadProvider-b5629da794cb3c1ca1970d206343743b165b9644.tar.bz2
android_packages_providers_DownloadProvider-b5629da794cb3c1ca1970d206343743b165b9644.zip
Major refactoring of DownloadThread.run().
Motivation: I need to fix the handling of 302s, so that after a disconnect, subsequent retries will use the original URI, not the redirected one. Rather than store extra information in the DB, I'd like to just keep the redirected URI in memory and make the redirected request within the same DownloadThread. This involves working with the large-scale structure of DownloadThread.run(). Since run() was a ~700 line method, I didn't feel comfortable making such changes. So this change refactors run() into a ~80 line method which calls into a collection of ~20 other short methods. The state previously kept in local variables has been pulled into a couple of state-only inner classes. The error-handling control flow, formerly handled by "break http_request_loop" statements, is now handled by throwing a "StopRequest" exception. The remaining structure of run() has been simplified -- the outermost for loop, for example, could never actually repeat and has been removed for now. Some other bits of code have been cleaned up a bit, but the functionality has not been modified. There are many good next steps to this refactoring. Besides various other cleanup bits, a major improvement would be to consolidate the State/InnerState classes, move some functionality to this new class (there are many functions of the form "void foo(State)" which would be good candidates), and promote it to a top-level class. But I want to take things one step at a time, and I think what I've got here is a major improvement and should be enough to allow me to safely implement the changes to redirection handling. In the process of doing this refactoring I added many test cases to PublicApiFunctionalTest to exercise some of the pieces of code I was moving around. I also moved some test cases from DownloadManagerFunctionalTest. Over time I'd like to move everything over to use the PublicApiFunctionalTest approach, and then I may break that into some smaller suites. Other minor changes: * use longs instead of ints to track file sizes, as these may be getting quite large in the future * provide a default DB value of -1 for COLUMN_TOTAL_BYTES, as this simplifies some logic in DownloadThread * small extensions to MockResponse to faciliate new test cases Change-Id: If7862349296ad79ff6cdc97e554ad14c01ce1f49
Diffstat (limited to 'src')
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java4
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java1
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java1331
-rw-r--r--src/com/android/providers/downloads/Helpers.java6
4 files changed, 733 insertions, 609 deletions
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 153e0454..c99a3784 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -55,8 +55,8 @@ public class DownloadInfo {
public String mCookies;
public String mUserAgent;
public String mReferer;
- public int mTotalBytes;
- public int mCurrentBytes;
+ public long mTotalBytes;
+ public long mCurrentBytes;
public String mETag;
public boolean mMediaScanned;
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index 4007e761..c210063a 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -374,6 +374,7 @@ public final class DownloadProvider extends ContentProvider {
}
copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
+ filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "initiating download with UID "
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index d8271a2b..6cb409ef 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -75,671 +75,794 @@ public class DownloadThread extends Thread {
}
/**
+ * State for the entire run() method.
+ */
+ private static class State {
+ public String mFilename;
+ public int mFinalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+ public FileOutputStream mStream;
+ public String mMimeType;
+ public boolean mCountRetry = false;
+ public int mRetryAfter = 0;
+ public int mRedirectCount = 0;
+ public String mNewUri;
+ public Uri mContentUri;
+ public boolean mGotData = false;
+
+ public State(DownloadInfo info) {
+ mMimeType = sanitizeMimeType(info.mMimeType);
+ mRedirectCount = info.mRedirectCount;
+ mContentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + info.mId);
+ }
+ }
+
+ /**
+ * State within the outer try block of the run() method.
+ */
+ private static class InnerState {
+ public int mBytesSoFar = 0;
+ public String mHeaderETag;
+ public boolean mContinuingDownload = false;
+ public String mHeaderContentLength;
+ public String mHeaderContentDisposition;
+ public String mHeaderContentLocation;
+ public int mBytesNotified = 0;
+ public long mTimeLastNotification = 0;
+ }
+
+ /**
+ * Raised from methods called by run() to indicate that the current request should be stopped
+ * immediately.
+ */
+ private class StopRequest extends Exception {}
+
+ /**
* Executes the download in a separate thread
*/
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
- boolean countRetry = false;
- int retryAfter = 0;
- int redirectCount = mInfo.mRedirectCount;
- String newUri = null;
- boolean gotData = false;
- String filename = null;
- String mimeType = sanitizeMimeType(mInfo.mMimeType);
- FileOutputStream stream = null;
+ State state = new State(mInfo);
AndroidHttpClient client = null;
PowerManager.WakeLock wakeLock = null;
- Uri contentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
+ HttpGet request = null;
try {
- boolean continuingDownload = false;
- String headerAcceptRanges = null;
- String headerContentDisposition = null;
- String headerContentLength = null;
- String headerContentLocation = null;
- String headerETag = null;
- String headerTransferEncoding = null;
-
+ InnerState innerState = new InnerState();
byte data[] = new byte[Constants.BUFFER_SIZE];
- int bytesSoFar = 0;
-
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.acquire();
- filename = mInfo.mFileName;
- if (filename != null) {
- if (!Helpers.isFilenameValid(filename)) {
- finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
- notifyDownloadCompleted(
- finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
- return;
- }
- // We're resuming a download that got interrupted
- File f = new File(filename);
- if (f.exists()) {
- long fileLength = f.length();
- if (fileLength == 0) {
- // The download hadn't actually started, we can restart from scratch
- f.delete();
- filename = null;
- } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
- // Tough luck, that's not a resumable download
- if (Config.LOGD) {
- Log.d(Constants.TAG,
- "can't resume interrupted non-resumable download");
- }
- f.delete();
- finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
- notifyDownloadCompleted(
- finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
- return;
- } else {
- // All right, we'll be able to resume this download
- stream = new FileOutputStream(filename, true);
- bytesSoFar = (int) fileLength;
- if (mInfo.mTotalBytes != -1) {
- headerContentLength = Integer.toString(mInfo.mTotalBytes);
- }
- headerETag = mInfo.mETag;
- continuingDownload = true;
- }
- }
+
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
}
+ setupDestinationFile(state, innerState);
+ client = AndroidHttpClient.newInstance(userAgent(), mContext);
+ request = new HttpGet(mInfo.mUri);
+ addRequestHeaders(innerState, request);
- int bytesNotified = bytesSoFar;
- // starting with MIN_VALUE means that the first write will commit
- // progress to the database
- long timeLastNotification = 0;
+ // check connectivity just before sending
+ if (!mInfo.canUseNetwork()) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ return;
+ }
- client = AndroidHttpClient.newInstance(userAgent(), mContext);
+ HttpResponse response = sendRequest(state, client, request);
+ handleExceptionalStatus(state, innerState, response);
+
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "received response for " + mInfo.mUri);
+ }
+
+ processResponseHeaders(state, innerState, response);
+ InputStream entityStream = openResponseEntity(state, response);
+ transferData(state, innerState, data, entityStream);
+
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
+ }
+ state.mFinalStatus = Downloads.Impl.STATUS_SUCCESS;
+ } catch (StopRequest error) {
+ if (request != null) {
+ request.abort();
+ }
+ } catch (FileNotFoundException ex) {
+ Log.d(Constants.TAG, "FileNotFoundException for " + state.mFilename + " : " + ex);
+ state.mFinalStatus = Downloads.Impl.STATUS_FILE_ERROR;
+ // falls through to the code that reports an error
+ } catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "Exception for " + mInfo.mUri, ex);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "Exception for id " + mInfo.mId, ex);
+ }
+ state.mFinalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+ // falls through to the code that reports an error
+ } finally {
+ mInfo.mHasActiveThread = false;
+ if (wakeLock != null) {
+ wakeLock.release();
+ wakeLock = null;
+ }
+ if (client != null) {
+ client.close();
+ client = null;
+ }
+ closeDestination(state);
+ finalizeDestinationFile(state);
+ notifyDownloadCompleted(state.mFinalStatus, state.mCountRetry, state.mRetryAfter,
+ state.mRedirectCount, state.mGotData, state.mFilename,
+ state.mNewUri, state.mMimeType);
+ }
+ }
- if (stream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
- && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
- .equalsIgnoreCase(mimeType)) {
+ /**
+ * Transfer as much data as possible from the HTTP response to the destination file.
+ * @param data buffer to use to read data
+ * @param entityStream stream for reading the HTTP response entity
+ */
+ private void transferData(State state, InnerState innerState, byte[] data,
+ InputStream entityStream) throws StopRequest {
+ for (;;) {
+ int bytesRead = readFromResponse(state, innerState, data, entityStream);
+ if (bytesRead == -1) { // success, end of stream already reached
+ handleEndOfStream(state, innerState);
+ return;
+ }
+
+ state.mGotData = true;
+ writeDataToDestination(state, data, bytesRead);
+ innerState.mBytesSoFar += bytesRead;
+ reportProgress(state, innerState);
+
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar + " for "
+ + mInfo.mUri);
+ }
+
+ checkPausedOrCanceled(state);
+ }
+ }
+
+ /**
+ * Called after a download transfer has just completed to take any necessary action on the
+ * downloaded file.
+ */
+ private void finalizeDestinationFile(State state) {
+ if (state.mFilename == null) {
+ return;
+ }
+
+ if (Downloads.Impl.isStatusError(state.mFinalStatus)) {
+ new File(state.mFilename).delete();
+ state.mFilename = null;
+ return;
+ }
+
+ if (!Downloads.Impl.isStatusSuccess(state.mFinalStatus)) {
+ // not yet complete
+ return;
+ }
+
+ if (isDrmFile(state)) {
+ transferToDrm(state);
+ return;
+ }
+
+ // make sure the file is readable
+ FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
+ syncDestination(state);
+ }
+
+ /**
+ * Sync the destination file to storage.
+ */
+ private void syncDestination(State state) {
+ FileOutputStream downloadedFileStream = null;
+ try {
+ downloadedFileStream = new FileOutputStream(state.mFilename, true);
+ downloadedFileStream.getFD().sync();
+ } catch (FileNotFoundException ex) {
+ Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
+ } catch (SyncFailedException ex) {
+ Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
+ } catch (IOException ex) {
+ Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
+ } catch (RuntimeException ex) {
+ Log.w(Constants.TAG, "exception while syncing file: ", ex);
+ } finally {
+ if(downloadedFileStream != null) {
try {
- stream.close();
- stream = null;
+ downloadedFileStream.close();
} catch (IOException ex) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "exception when closing the file before download : " +
- ex);
- }
- // nothing can really be done if the file can't be closed
+ Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
+ } catch (RuntimeException ex) {
+ Log.w(Constants.TAG, "exception while closing file: ", ex);
}
}
+ }
+ }
+
+ /**
+ * @return true if the current download is a DRM file
+ */
+ private boolean isDrmFile(State state) {
+ return DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(state.mMimeType);
+ }
- /*
- * This loop is run once for every individual HTTP request that gets sent.
- * The very first HTTP request is a "virgin" request, while every subsequent
- * request is done with the original ETag and a byte-range.
- */
-http_request_loop:
- while (true) {
- // Prepares the request and fires it.
- HttpGet request = new HttpGet(mInfo.mUri);
+ /**
+ * Transfer the downloaded destination file to the DRM store.
+ */
+ private void transferToDrm(State state) {
+ File file = new File(state.mFilename);
+ Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
+ if (item == null) {
+ Log.w(Constants.TAG, "unable to add file " + state.mFilename + " to DrmProvider");
+ state.mFinalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+ } else {
+ state.mFilename = item.getDataString();
+ state.mMimeType = item.getType();
+ }
+ file.delete();
+ }
+
+ /**
+ * Close the destination output stream.
+ */
+ private void closeDestination(State state) {
+ try {
+ // close the file
+ if (state.mStream != null) {
+ state.mStream.close();
+ }
+ } catch (IOException ex) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+ }
+ // nothing can really be done if the file can't be closed
+ }
+ }
+
+ /**
+ * Check if the download has been paused or canceled, stopping the request appropriately if it
+ * has been.
+ */
+ private void checkPausedOrCanceled(State state) throws StopRequest {
+ synchronized (mInfo) {
+ if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
if (Constants.LOGV) {
- Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
+ Log.v(Constants.TAG, "paused " + mInfo.mUri);
}
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ throw new StopRequest();
+ }
+ }
+ if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "canceled " + mInfo.mUri);
+ }
+ state.mFinalStatus = Downloads.Impl.STATUS_CANCELED;
+ throw new StopRequest();
+ }
+ }
- addRequestHeaders(request);
+ /**
+ * Report download progress through the database if necessary.
+ */
+ private void reportProgress(State state, InnerState innerState) {
+ long now = mSystemFacade.currentTimeMillis();
+ if (innerState.mBytesSoFar - innerState.mBytesNotified
+ > Constants.MIN_PROGRESS_STEP
+ && now - innerState.mTimeLastNotification
+ > Constants.MIN_PROGRESS_TIME) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
+ mContext.getContentResolver().update(
+ state.mContentUri, values, null, null);
+ innerState.mBytesNotified = innerState.mBytesSoFar;
+ innerState.mTimeLastNotification = now;
+ }
+ }
- if (continuingDownload) {
- if (headerETag != null) {
- request.addHeader("If-Match", headerETag);
- }
- request.addHeader("Range", "bytes=" + bytesSoFar + "-");
+ /**
+ * Write a data buffer to the destination file.
+ * @param data buffer containing the data to write
+ * @param bytesRead how many bytes to write from the buffer
+ */
+ private void writeDataToDestination(State state, byte[] data, int bytesRead)
+ throws StopRequest {
+ for (;;) {
+ try {
+ if (state.mStream == null) {
+ state.mStream = new FileOutputStream(state.mFilename, true);
+ }
+ state.mStream.write(data, 0, bytesRead);
+ if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+ && !isDrmFile(state)) {
+ closeDestination(state);
+ }
+ return;
+ } catch (IOException ex) {
+ if (!Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
+ state.mFinalStatus = Downloads.Impl.STATUS_FILE_ERROR;
+ throw new StopRequest();
}
+ }
+ }
+ }
+
+ /**
+ * Called when we've reached the end of the HTTP response stream, to update the database and
+ * check for consistency.
+ */
+ private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
+ if (innerState.mHeaderContentLength == null) {
+ values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
+ }
+ mContext.getContentResolver().update(state.mContentUri, values, null, null);
- // check connectivity just before sending
- if (!mInfo.canUseNetwork()) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- break http_request_loop;
+ boolean lengthMismatched = (innerState.mHeaderContentLength != null)
+ && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
+ if (lengthMismatched) {
+ if (!mInfo.mNoIntegrity && innerState.mHeaderETag == null) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "mismatched content length " +
+ mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "mismatched content length for " +
+ mInfo.mId);
+ }
+ state.mFinalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
+ } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ state.mCountRetry = true;
+ } else {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "closed socket for " + mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "closed socket for download " +
+ mInfo.mId);
}
+ state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
+ }
+ throw new StopRequest();
+ }
+ }
- HttpResponse response;
- try {
- response = client.execute(request);
- } catch (IllegalArgumentException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "Arg exception trying to execute request for " +
- mInfo.mUri + " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "Arg exception trying to execute request for " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
- request.abort();
- break http_request_loop;
- } catch (IOException ex) {
- if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(mSystemFacade)) {
- Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Up");
- } else {
- Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Down");
- }
- }
- if (!Helpers.isNetworkAvailable(mSystemFacade)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "IOException trying to execute request for " +
- mInfo.mUri + " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "IOException trying to execute request for " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- request.abort();
- break http_request_loop;
+ /**
+ * Read some data from the HTTP response stream, handling I/O errors.
+ * @param data buffer to use to read data
+ * @param entityStream stream for reading the HTTP response entity
+ * @return the number of bytes actually read or -1 if the end of the stream has been reached
+ */
+ private int readFromResponse(State state, InnerState innerState, byte[] data,
+ InputStream entityStream) throws StopRequest {
+ try {
+ return entityStream.read(data);
+ } catch (IOException ex) {
+ if (Constants.LOGX) {
+ if (Helpers.isNetworkAvailable(mSystemFacade)) {
+ Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Up");
+ } else {
+ Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Down");
}
+ }
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
+ mContext.getContentResolver().update(state.mContentUri, values, null, null);
+ if (!mInfo.mNoIntegrity && innerState.mHeaderETag == null) {
+ Log.d(Constants.TAG, "download IOException for download " + mInfo.mId + " : " + ex);
+ Log.d(Constants.TAG, "can't resume interrupted download with no ETag");
+ state.mFinalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
+ } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ state.mCountRetry = true;
+ } else {
+ Log.d(Constants.TAG, "download IOException for download " + mInfo.mId + " : " + ex);
+ state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
+ }
+ throw new StopRequest();
+ }
+ }
- int statusCode = response.getStatusLine().getStatusCode();
- if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "got HTTP response code 503");
- }
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- Header header = response.getFirstHeader("Retry-After");
- if (header != null) {
- try {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Retry-After :" + header.getValue());
- }
- retryAfter = Integer.parseInt(header.getValue());
- if (retryAfter < 0) {
- retryAfter = 0;
- } else {
- if (retryAfter < Constants.MIN_RETRY_AFTER) {
- retryAfter = Constants.MIN_RETRY_AFTER;
- } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
- retryAfter = Constants.MAX_RETRY_AFTER;
- }
- retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
- retryAfter *= 1000;
- }
- } catch (NumberFormatException ex) {
- // ignored - retryAfter stays 0 in this case.
- }
- }
- request.abort();
- break http_request_loop;
+ /**
+ * Open a stream for the HTTP response entity, handling I/O errors.
+ * @return an InputStream to read the response entity
+ */
+ private InputStream openResponseEntity(State state, HttpResponse response)
+ throws StopRequest {
+ try {
+ return response.getEntity().getContent();
+ } catch (IOException ex) {
+ if (Constants.LOGX) {
+ if (Helpers.isNetworkAvailable(mSystemFacade)) {
+ Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Up");
+ } else {
+ Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Down");
}
- if (statusCode == 301 ||
- statusCode == 302 ||
- statusCode == 303 ||
- statusCode == 307) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
- }
- if (redirectCount >= Constants.MAX_REDIRECTS) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
- " at " + mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
- request.abort();
- break http_request_loop;
- }
- Header header = response.getFirstHeader("Location");
- if (header != null) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Location :" + header.getValue());
- }
- try {
- newUri = new URI(mInfo.mUri).
- resolve(new URI(header.getValue())).
- toString();
- } catch(URISyntaxException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG,
- "Couldn't resolve redirect URI " +
- header.getValue() +
- " for " +
- mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG,
- "Couldn't resolve redirect URI for download " +
- mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
- request.abort();
- break http_request_loop;
- }
- ++redirectCount;
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- request.abort();
- break http_request_loop;
- }
+ }
+ if (!Helpers.isNetworkAvailable(mSystemFacade)) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ state.mCountRetry = true;
+ } else {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG,
+ "IOException getting entity for " +
+ mInfo.mUri +
+ " : " +
+ ex);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "IOException getting entity for download " +
+ mInfo.mId + " : " + ex);
}
- if ((!continuingDownload && statusCode != Downloads.Impl.STATUS_SUCCESS)
- || (continuingDownload && statusCode != 206)) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "http error " + statusCode + " for download " +
- mInfo.mId);
- }
- if (Downloads.Impl.isStatusError(statusCode)) {
- finalStatus = statusCode;
- } else if (statusCode >= 300 && statusCode < 400) {
- finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
- } else if (continuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
- finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
- } else {
- finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
- }
- request.abort();
- break http_request_loop;
- } else {
- // Handles the response, saves the file
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "received response for " + mInfo.mUri);
- }
+ state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
+ }
+ throw new StopRequest();
+ }
+ }
- if (!continuingDownload) {
- Header header = response.getFirstHeader("Accept-Ranges");
- if (header != null) {
- headerAcceptRanges = header.getValue();
- }
- header = response.getFirstHeader("Content-Disposition");
- if (header != null) {
- headerContentDisposition = header.getValue();
- }
- header = response.getFirstHeader("Content-Location");
- if (header != null) {
- headerContentLocation = header.getValue();
- }
- if (mimeType == null) {
- header = response.getFirstHeader("Content-Type");
- if (header != null) {
- mimeType = sanitizeMimeType(header.getValue());
- }
- }
- header = response.getFirstHeader("ETag");
- if (header != null) {
- headerETag = header.getValue();
- }
- header = response.getFirstHeader("Transfer-Encoding");
- if (header != null) {
- headerTransferEncoding = header.getValue();
- }
- if (headerTransferEncoding == null) {
- header = response.getFirstHeader("Content-Length");
- if (header != null) {
- headerContentLength = header.getValue();
- }
- } else {
- // Ignore content-length with transfer-encoding - 2616 4.4 3
- if (Constants.LOGVV) {
- Log.v(Constants.TAG,
- "ignoring content-length because of xfer-encoding");
- }
- }
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Accept-Ranges: " + headerAcceptRanges);
- Log.v(Constants.TAG, "Content-Disposition: " +
- headerContentDisposition);
- Log.v(Constants.TAG, "Content-Length: " + headerContentLength);
- Log.v(Constants.TAG, "Content-Location: " + headerContentLocation);
- Log.v(Constants.TAG, "Content-Type: " + mimeType);
- Log.v(Constants.TAG, "ETag: " + headerETag);
- Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
- }
-
- if (!mInfo.mNoIntegrity && headerContentLength == null &&
- (headerTransferEncoding == null
- || !headerTransferEncoding.equalsIgnoreCase("chunked"))
- ) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "can't know size of download, giving up");
- }
- finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
- request.abort();
- break http_request_loop;
- }
-
- DownloadFileInfo fileInfo = Helpers.generateSaveFile(
- mContext,
- mInfo.mUri,
- mInfo.mHint,
- headerContentDisposition,
- headerContentLocation,
- mimeType,
- mInfo.mDestination,
- (headerContentLength != null) ?
- Integer.parseInt(headerContentLength) : 0);
- if (fileInfo.mFileName == null) {
- finalStatus = fileInfo.mStatus;
- request.abort();
- break http_request_loop;
- }
- filename = fileInfo.mFileName;
- stream = fileInfo.mStream;
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + filename);
- }
-
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl._DATA, filename);
- if (headerETag != null) {
- values.put(Constants.ETAG, headerETag);
- }
- if (mimeType != null) {
- values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
- }
- int contentLength = -1;
- if (headerContentLength != null) {
- contentLength = Integer.parseInt(headerContentLength);
- }
- mInfo.mTotalBytes = contentLength;
- values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, contentLength);
- mContext.getContentResolver().update(contentUri, values, null, null);
- // check connectivity again now that we know the total size
- if (!mInfo.canUseNetwork()) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- request.abort();
- break http_request_loop;
- }
- }
+ /**
+ * Read HTTP response headers and take appropriate action, including setting up the destination
+ * file and updating the database.
+ */
+ private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
+ throws StopRequest, FileNotFoundException {
+ if (innerState.mContinuingDownload) {
+ // ignore response headers on resume requests
+ return;
+ }
- InputStream entityStream;
- try {
- entityStream = response.getEntity().getContent();
- } catch (IOException ex) {
- if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(mSystemFacade)) {
- Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Up");
- } else {
- Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Down");
- }
- }
- if (!Helpers.isNetworkAvailable(mSystemFacade)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.d(Constants.TAG,
- "IOException getting entity for " +
- mInfo.mUri +
- " : " +
- ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "IOException getting entity for download " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- request.abort();
- break http_request_loop;
- }
- for (;;) {
- int bytesRead;
- try {
- bytesRead = entityStream.read(data);
- } catch (IOException ex) {
- if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(mSystemFacade)) {
- Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Up");
- } else {
- Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Down");
- }
- }
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
- mContext.getContentResolver().update(contentUri, values, null, null);
- if (!mInfo.mNoIntegrity && headerETag == null) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
- " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "download IOException for download " +
- mInfo.mId + " : " + ex);
- }
- if (Config.LOGD) {
- Log.d(Constants.TAG,
- "can't resume interrupted download with no ETag");
- }
- finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
- } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
- " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "download IOException for download " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- request.abort();
- break http_request_loop;
- }
- if (bytesRead == -1) { // success
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
- if (headerContentLength == null) {
- values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, bytesSoFar);
- }
- mContext.getContentResolver().update(contentUri, values, null, null);
- if ((headerContentLength != null)
- && (bytesSoFar
- != Integer.parseInt(headerContentLength))) {
- if (!mInfo.mNoIntegrity && headerETag == null) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "mismatched content length " +
- mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "mismatched content length for " +
- mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
- } else if (!Helpers.isNetworkAvailable(mSystemFacade)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "closed socket for " + mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "closed socket for download " +
- mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- break http_request_loop;
- }
- break;
- }
- gotData = true;
- for (;;) {
- try {
- if (stream == null) {
- stream = new FileOutputStream(filename, true);
- }
- stream.write(data, 0, bytesRead);
- if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
- && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
- .equalsIgnoreCase(mimeType)) {
- try {
- stream.close();
- stream = null;
- } catch (IOException ex) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG,
- "exception when closing the file " +
- "during download : " + ex);
- }
- // nothing can really be done if the file can't be closed
- }
- }
- break;
- } catch (IOException ex) {
- if (!Helpers.discardPurgeableFiles(
- mContext, Constants.BUFFER_SIZE)) {
- finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
- break http_request_loop;
- }
- }
- }
- bytesSoFar += bytesRead;
- long now = mSystemFacade.currentTimeMillis();
- if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP
- && now - timeLastNotification
- > Constants.MIN_PROGRESS_TIME) {
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
- mContext.getContentResolver().update(
- contentUri, values, null, null);
- bytesNotified = bytesSoFar;
- timeLastNotification = now;
- }
-
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.mUri);
- }
- synchronized (mInfo) {
- if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "paused " + mInfo.mUri);
- }
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- request.abort();
- break http_request_loop;
- }
- }
- if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "canceled " + mInfo.mUri);
- } else if (Config.LOGD) {
- // Log.d(Constants.TAG, "canceled id " + mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_CANCELED;
- break http_request_loop;
- }
- }
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
- }
- finalStatus = Downloads.Impl.STATUS_SUCCESS;
- }
- break;
+ readResponseHeaders(state, innerState, response);
+
+ DownloadFileInfo fileInfo = Helpers.generateSaveFile(
+ mContext,
+ mInfo.mUri,
+ mInfo.mHint,
+ innerState.mHeaderContentDisposition,
+ innerState.mHeaderContentLocation,
+ state.mMimeType,
+ mInfo.mDestination,
+ (innerState.mHeaderContentLength != null) ?
+ Long.parseLong(innerState.mHeaderContentLength) : 0);
+ if (fileInfo.mFileName == null) {
+ state.mFinalStatus = fileInfo.mStatus;
+ throw new StopRequest();
+ }
+ state.mFilename = fileInfo.mFileName;
+ state.mStream = fileInfo.mStream;
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
+ }
+
+ updateDatabaseFromHeaders(state, innerState);
+
+ // check connectivity again now that we know the total size
+ if (!mInfo.canUseNetwork()) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ throw new StopRequest();
+ }
+ }
+
+ /**
+ * Update necessary database fields based on values of HTTP response headers that have been
+ * read.
+ */
+ private void updateDatabaseFromHeaders(State state, InnerState innerState) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl._DATA, state.mFilename);
+ if (innerState.mHeaderETag != null) {
+ values.put(Constants.ETAG, innerState.mHeaderETag);
+ }
+ if (state.mMimeType != null) {
+ values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
+ }
+ values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
+ mContext.getContentResolver().update(state.mContentUri, values, null, null);
+ }
+
+ /**
+ * Read headers from the HTTP response and store them into local state.
+ */
+ private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
+ throws StopRequest {
+ Header header = response.getFirstHeader("Content-Disposition");
+ if (header != null) {
+ innerState.mHeaderContentDisposition = header.getValue();
+ }
+ header = response.getFirstHeader("Content-Location");
+ if (header != null) {
+ innerState.mHeaderContentLocation = header.getValue();
+ }
+ if (state.mMimeType == null) {
+ header = response.getFirstHeader("Content-Type");
+ if (header != null) {
+ state.mMimeType = sanitizeMimeType(header.getValue());
}
- } catch (FileNotFoundException ex) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " + ex);
+ }
+ header = response.getFirstHeader("ETag");
+ if (header != null) {
+ innerState.mHeaderETag = header.getValue();
+ }
+ String headerTransferEncoding = null;
+ header = response.getFirstHeader("Transfer-Encoding");
+ if (header != null) {
+ headerTransferEncoding = header.getValue();
+ }
+ if (headerTransferEncoding == null) {
+ header = response.getFirstHeader("Content-Length");
+ if (header != null) {
+ innerState.mHeaderContentLength = header.getValue();
+ mInfo.mTotalBytes = Long.parseLong(innerState.mHeaderContentLength);
}
- finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
- // falls through to the code that reports an error
- } catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
+ } else {
+ // Ignore content-length with transfer-encoding - 2616 4.4 3
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG,
+ "ignoring content-length because of xfer-encoding");
+ }
+ }
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Content-Disposition: " +
+ innerState.mHeaderContentDisposition);
+ Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
+ Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
+ Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
+ Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
+ Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
+ }
+
+ boolean noSizeInfo = innerState.mHeaderContentLength == null
+ && (headerTransferEncoding == null
+ || !headerTransferEncoding.equalsIgnoreCase("chunked"));
+ if (!mInfo.mNoIntegrity && noSizeInfo) {
+ Log.d(Constants.TAG, "can't know size of download, giving up");
+ state.mFinalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
+ throw new StopRequest();
+ }
+ }
+
+ /**
+ * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
+ */
+ private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
+ throws StopRequest {
+ int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
+ handleServiceUnavailable(state, response);
+ }
+ if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
+ handleRedirect(state, response, statusCode);
+ }
+
+ int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
+ if (statusCode != expectedStatus) {
+ handleOtherStatus(state, innerState, statusCode);
+ }
+ }
+
+ /**
+ * Handle a status that we don't know how to deal with properly.
+ */
+ private void handleOtherStatus(State state, InnerState innerState, int statusCode)
+ throws StopRequest {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "http error " + statusCode + " for download " +
+ mInfo.mId);
+ }
+ if (Downloads.Impl.isStatusError(statusCode)) {
+ state.mFinalStatus = statusCode;
+ } else if (statusCode >= 300 && statusCode < 400) {
+ state.mFinalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
+ } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
+ state.mFinalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
+ } else {
+ state.mFinalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
+ }
+ throw new StopRequest();
+ }
+
+ /**
+ * Handle a 3xx redirect status.
+ */
+ private void handleRedirect(State state, HttpResponse response, int statusCode)
+ throws StopRequest {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
+ }
+ if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
if (Constants.LOGV) {
- Log.d(Constants.TAG, "Exception for " + mInfo.mUri, ex);
+ Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
+ " at " + mInfo.mUri);
} else if (Config.LOGD) {
- Log.d(Constants.TAG, "Exception for id " + mInfo.mId, ex);
+ Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
}
- finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
- // falls through to the code that reports an error
- } finally {
- mInfo.mHasActiveThread = false;
- if (wakeLock != null) {
- wakeLock.release();
- wakeLock = null;
- }
- if (client != null) {
- client.close();
- client = null;
+ state.mFinalStatus = Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
+ throw new StopRequest();
+ }
+ Header header = response.getFirstHeader("Location");
+ if (header != null) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Location :" + header.getValue());
}
try {
- // close the file
- if (stream != null) {
- stream.close();
+ state.mNewUri = new URI(mInfo.mUri).
+ resolve(new URI(header.getValue())).
+ toString();
+ } catch(URISyntaxException ex) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG,
+ "Couldn't resolve redirect URI " +
+ header.getValue() +
+ " for " +
+ mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG,
+ "Couldn't resolve redirect URI for download " +
+ mInfo.mId);
}
- } catch (IOException ex) {
+ state.mFinalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
+ throw new StopRequest();
+ }
+ ++state.mRedirectCount;
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ throw new StopRequest();
+ }
+ }
+
+ /**
+ * Handle a 503 Service Unavailable status by processing the Retry-After header.
+ */
+ private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "got HTTP response code 503");
+ }
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ state.mCountRetry = true;
+ Header header = response.getFirstHeader("Retry-After");
+ if (header != null) {
+ try {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Retry-After :" + header.getValue());
+ }
+ state.mRetryAfter = Integer.parseInt(header.getValue());
+ if (state.mRetryAfter < 0) {
+ state.mRetryAfter = 0;
+ } else {
+ if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
+ state.mRetryAfter = Constants.MIN_RETRY_AFTER;
+ } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
+ state.mRetryAfter = Constants.MAX_RETRY_AFTER;
+ }
+ state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
+ state.mRetryAfter *= 1000;
+ }
+ } catch (NumberFormatException ex) {
+ // ignored - retryAfter stays 0 in this case.
+ }
+ }
+ throw new StopRequest();
+ }
+
+ /**
+ * Send the request to the server, handling any I/O exceptions.
+ */
+ private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
+ throws StopRequest {
+ try {
+ return client.execute(request);
+ } catch (IllegalArgumentException ex) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "Arg exception trying to execute request for " +
+ mInfo.mUri + " : " + ex);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "Arg exception trying to execute request for " +
+ mInfo.mId + " : " + ex);
+ }
+ state.mFinalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
+ throw new StopRequest();
+ } catch (IOException ex) {
+ if (Constants.LOGX) {
+ if (Helpers.isNetworkAvailable(mSystemFacade)) {
+ Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Up");
+ } else {
+ Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Down");
+ }
+ }
+ if (!Helpers.isNetworkAvailable(mSystemFacade)) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+ state.mFinalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
+ state.mCountRetry = true;
+ } else {
if (Constants.LOGV) {
- Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+ Log.d(Constants.TAG, "IOException trying to execute request for " +
+ mInfo.mUri + " : " + ex);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "IOException trying to execute request for " +
+ mInfo.mId + " : " + ex);
}
- // nothing can really be done if the file can't be closed
- }
- if (filename != null) {
- // if the download wasn't successful, delete the file
- if (Downloads.Impl.isStatusError(finalStatus)) {
- new File(filename).delete();
- filename = null;
- } else if (Downloads.Impl.isStatusSuccess(finalStatus) &&
- DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
- .equalsIgnoreCase(mimeType)) {
- // transfer the file to the DRM content provider
- File file = new File(filename);
- Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
- if (item == null) {
- Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider");
- finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
- } else {
- filename = item.getDataString();
- mimeType = item.getType();
- }
+ state.mFinalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
+ }
+ throw new StopRequest();
+ }
+ }
- file.delete();
- } else if (Downloads.Impl.isStatusSuccess(finalStatus)) {
- // make sure the file is readable
- FileUtils.setPermissions(filename, 0644, -1, -1);
-
- // Sync to storage after completion
- FileOutputStream downloadedFileStream = null;
- try {
- downloadedFileStream = new FileOutputStream(filename, true);
- downloadedFileStream.getFD().sync();
- } catch (FileNotFoundException ex) {
- Log.w(Constants.TAG, "file " + filename + " not found: " + ex);
- } catch (SyncFailedException ex) {
- Log.w(Constants.TAG, "file " + filename + " sync failed: " + ex);
- } catch (IOException ex) {
- Log.w(Constants.TAG, "IOException trying to sync " + filename + ": " + ex);
- } catch (RuntimeException ex) {
- Log.w(Constants.TAG, "exception while syncing file: ", ex);
- } finally {
- if(downloadedFileStream != null) {
- try {
- downloadedFileStream.close();
- } catch (IOException ex) {
- Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
- } catch (RuntimeException ex) {
- Log.w(Constants.TAG, "exception while closing file: ", ex);
- }
- }
+ /**
+ * Prepare the destination file to receive data. If the file already exists, we'll set up
+ * appropriately for resumption.
+ */
+ private void setupDestinationFile(State state, InnerState innerState)
+ throws StopRequest, FileNotFoundException {
+ state.mFilename = mInfo.mFileName;
+ if (state.mFilename != null) {
+ if (!Helpers.isFilenameValid(state.mFilename)) {
+ state.mFinalStatus = Downloads.Impl.STATUS_FILE_ERROR;
+ throw new StopRequest();
+ }
+ // We're resuming a download that got interrupted
+ File f = new File(state.mFilename);
+ if (f.exists()) {
+ long fileLength = f.length();
+ if (fileLength == 0) {
+ // The download hadn't actually started, we can restart from scratch
+ f.delete();
+ state.mFilename = null;
+ } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
+ // Tough luck, that's not a resumable download
+ if (Config.LOGD) {
+ Log.d(Constants.TAG,
+ "can't resume interrupted non-resumable download");
+ }
+ f.delete();
+ state.mFinalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
+ throw new StopRequest();
+ } else {
+ // All right, we'll be able to resume this download
+ state.mStream = new FileOutputStream(state.mFilename, true);
+ innerState.mBytesSoFar = (int) fileLength;
+ if (mInfo.mTotalBytes != -1) {
+ innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
}
+ innerState.mHeaderETag = mInfo.mETag;
+ innerState.mContinuingDownload = true;
}
}
- notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount,
- gotData, filename, newUri, mimeType);
+ }
+
+ if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+ && !isDrmFile(state)) {
+ closeDestination(state);
}
}
/**
* Add custom headers for this download to the HTTP request.
*/
- private void addRequestHeaders(HttpGet request) {
+ private void addRequestHeaders(InnerState innerState, HttpGet request) {
for (Map.Entry<String, String> header : mInfo.getHeaders().entrySet()) {
request.addHeader(header.getKey(), header.getValue());
}
+
+ if (innerState.mContinuingDownload) {
+ if (innerState.mHeaderETag != null) {
+ request.addHeader("If-Match", innerState.mHeaderETag);
+ }
+ request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
+ }
}
/**
@@ -795,7 +918,7 @@ http_request_loop:
* @return null if mimeType was null. Otherwise a string which represents a
* single mimetype in lowercase and with surrounding whitespaces trimmed.
*/
- private String sanitizeMimeType(String mimeType) {
+ private static String sanitizeMimeType(String mimeType) {
try {
mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 0ad4edc0..dac4b55e 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -94,7 +94,7 @@ public class Helpers {
String contentLocation,
String mimeType,
int destination,
- int contentLength) throws FileNotFoundException {
+ long contentLength) throws FileNotFoundException {
if (!canHandleDownload(context, mimeType, destination)) {
return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
@@ -134,7 +134,7 @@ public class Helpers {
private static String chooseFullPath(Context context, String url, String hint,
String contentDisposition, String contentLocation,
- String mimeType, int destination, int contentLength)
+ String mimeType, int destination, long contentLength)
throws GenerateSaveFileError {
File base = locateDestinationDirectory(context, mimeType, destination, contentLength);
String filename = chooseFilename(url, hint, contentDisposition, contentLocation,
@@ -201,7 +201,7 @@ public class Helpers {
}
private static File locateDestinationDirectory(Context context, String mimeType,
- int destination, int contentLength)
+ int destination, long contentLength)
throws GenerateSaveFileError {
File base = null;
StatFs stat = null;