summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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
-rw-r--r--tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java12
-rw-r--r--tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java42
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java148
-rw-r--r--tests/src/tests/http/MockResponse.java9
8 files changed, 876 insertions, 677 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;
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
index 326d9fff..92678fe3 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
@@ -195,10 +195,16 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
* Enqueue a response from the MockWebServer.
*/
MockResponse enqueueResponse(int status, String body) {
+ return enqueueResponse(status, body, true);
+ }
+
+ MockResponse enqueueResponse(int status, String body, boolean includeContentType) {
MockResponse response = new MockResponse()
- .setResponseCode(status)
- .setBody(body)
- .addHeader("Content-type", "text/plain");
+ .setResponseCode(status)
+ .setBody(body);
+ if (includeContentType) {
+ response.addHeader("Content-type", "text/plain");
+ }
mServer.enqueue(response);
return response;
}
diff --git a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
index 3cd9cf58..822ab54d 100644
--- a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
@@ -85,18 +85,6 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
}
- public void testRedirect() throws Exception {
- enqueueEmptyResponse(301).addHeader("Location", mServer.getUrl("/other_path").toString());
- enqueueResponse(HTTP_OK, FILE_CONTENT);
- Uri downloadUri = requestDownload("/path");
- RecordedRequest request = runUntilStatus(downloadUri, Downloads.STATUS_RUNNING_PAUSED);
- assertEquals("/path", request.getPath());
-
- mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
- request = runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
- assertEquals("/other_path", request.getPath());
- }
-
public void testBasicConnectivityChanges() throws Exception {
enqueueResponse(HTTP_OK, FILE_CONTENT);
Uri downloadUri = requestDownload("/path");
@@ -134,36 +122,6 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
}
- public void testInterruptedDownload() throws Exception {
- int initialLength = 5;
- String etag = "my_etag";
- int totalLength = FILE_CONTENT.length();
- // the first response has normal headers but unexpectedly closes after initialLength bytes
- enqueueResponse(HTTP_OK, FILE_CONTENT.substring(0, initialLength))
- .addHeader("Content-length", totalLength)
- .addHeader("Etag", etag)
- .setCloseConnectionAfter(true);
- Uri downloadUri = requestDownload("/path");
-
- runUntilStatus(downloadUri, Downloads.STATUS_RUNNING_PAUSED);
-
- mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
- // the second response returns partial content for the rest of the data
- enqueueResponse(HTTP_PARTIAL_CONTENT, FILE_CONTENT.substring(initialLength))
- .addHeader("Content-range",
- "bytes " + initialLength + "-" + totalLength + "/" + totalLength)
- .addHeader("Etag", etag);
- // TODO: ideally we wouldn't need to call startService again, but there's a bug where the
- // service won't retry a download until an intent comes in
- RecordedRequest request = runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
-
- List<String> headers = request.getHeaders();
- assertTrue("No Range header: " + headers,
- headers.contains("Range: bytes=" + initialLength + "-"));
- assertTrue("No ETag header: " + headers, headers.contains("If-Match: " + etag));
- assertEquals(FILE_CONTENT, getDownloadContents(downloadUri));
- }
-
/**
* Read a downloaded file from disk.
*/
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index e34c66e6..b1ccc7ae 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -21,17 +21,24 @@ import android.net.ConnectivityManager;
import android.net.DownloadManager;
import android.net.Uri;
import android.os.Environment;
+import android.os.ParcelFileDescriptor;
import android.test.suitebuilder.annotation.LargeTest;
+import tests.http.MockResponse;
import tests.http.RecordedRequest;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
+import java.util.List;
@LargeTest
public class PublicApiFunctionalTest extends AbstractDownloadManagerFunctionalTest {
+ private static final int HTTP_NOT_ACCEPTABLE = 406;
+ private static final int HTTP_LENGTH_REQUIRED = 411;
private static final String REQUEST_PATH = "/path";
+ private static final String REDIRECTED_PATH = "/other_path";
+ private static final String ETAG = "my_etag";
class Download implements StatusReader {
final long mId;
@@ -73,8 +80,10 @@ public class PublicApiFunctionalTest extends AbstractDownloadManagerFunctionalTe
}
String getContents() throws Exception {
- InputStream stream = new FileInputStream(
- mManager.openDownloadedFile(mId).getFileDescriptor());
+ ParcelFileDescriptor downloadedFile = mManager.openDownloadedFile(mId);
+ assertTrue("Invalid file descriptor: " + downloadedFile,
+ downloadedFile.getFileDescriptor().valid());
+ InputStream stream = new FileInputStream(downloadedFile.getFileDescriptor());
try {
return readStream(stream);
} finally {
@@ -161,43 +170,53 @@ public class PublicApiFunctionalTest extends AbstractDownloadManagerFunctionalTe
public void testDownloadError() throws Exception {
enqueueEmptyResponse(HTTP_NOT_FOUND);
- Download download = enqueueRequest(getRequest());
- download.runUntilStatus(DownloadManager.STATUS_FAILED);
- assertEquals(HTTP_NOT_FOUND, download.getLongField(DownloadManager.COLUMN_ERROR_CODE));
+ runSimpleFailureTest(HTTP_NOT_FOUND);
}
public void testUnhandledHttpStatus() throws Exception {
enqueueEmptyResponse(1234); // some invalid HTTP status
- Download download = enqueueRequest(getRequest());
- download.runUntilStatus(DownloadManager.STATUS_FAILED);
- assertEquals(DownloadManager.ERROR_UNHANDLED_HTTP_CODE,
- download.getLongField(DownloadManager.COLUMN_ERROR_CODE));
+ runSimpleFailureTest(DownloadManager.ERROR_UNHANDLED_HTTP_CODE);
}
public void testInterruptedDownload() throws Exception {
int initialLength = 5;
- String etag = "my_etag";
- int totalLength = FILE_CONTENT.length();
- // the first response has normal headers but unexpectedly closes after initialLength bytes
- enqueueResponse(HTTP_OK, FILE_CONTENT.substring(0, initialLength))
- .addHeader("Content-length", totalLength)
- .addHeader("Etag", etag)
- .setCloseConnectionAfter(true);
- Download download = enqueueRequest(getRequest());
+ enqueueInterruptedDownloadResponses(initialLength);
+ Download download = enqueueRequest(getRequest());
download.runUntilStatus(DownloadManager.STATUS_PAUSED);
assertEquals(initialLength,
download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+ assertEquals(FILE_CONTENT.length(),
+ download.getLongField(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+ RecordedRequest request = download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+ assertEquals(FILE_CONTENT.length(),
+ download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+ assertEquals(FILE_CONTENT, download.getContents());
+
+ List<String> headers = request.getHeaders();
+ assertTrue("No Range header: " + headers,
+ headers.contains("Range: bytes=" + initialLength + "-"));
+ assertTrue("No ETag header: " + headers, headers.contains("If-Match: " + ETAG));
+ }
+
+ private void enqueueInterruptedDownloadResponses(int initialLength) {
+ int totalLength = FILE_CONTENT.length();
+ // the first response has normal headers but unexpectedly closes after initialLength bytes
+ enqueuePartialResponse(initialLength);
// the second response returns partial content for the rest of the data
enqueueResponse(HTTP_PARTIAL_CONTENT, FILE_CONTENT.substring(initialLength))
.addHeader("Content-range",
"bytes " + initialLength + "-" + totalLength + "/" + totalLength)
- .addHeader("Etag", etag);
- download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
- assertEquals(totalLength,
- download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+ .addHeader("Etag", ETAG);
+ }
+
+ private MockResponse enqueuePartialResponse(int initialLength) {
+ return enqueueResponse(HTTP_OK, FILE_CONTENT.substring(0, initialLength))
+ .addHeader("Content-length", FILE_CONTENT.length())
+ .addHeader("Etag", ETAG)
+ .setCloseConnectionAfter(true);
}
public void testFiltering() throws Exception {
@@ -323,6 +342,93 @@ public class PublicApiFunctionalTest extends AbstractDownloadManagerFunctionalTe
}
}
+ public void testRedirect301() throws Exception {
+ RecordedRequest lastRequest = runRedirectionTest(301);
+ // for 301, upon retry, we reuse the redirected URI
+ assertEquals(REDIRECTED_PATH, lastRequest.getPath());
+ }
+
+ // TODO: currently fails
+ public void disabledTestRedirect302() throws Exception {
+ RecordedRequest lastRequest = runRedirectionTest(302);
+ // for 302, upon retry, we use the original URI
+ assertEquals(REQUEST_PATH, lastRequest.getPath());
+ }
+
+ public void testNoEtag() throws Exception {
+ enqueuePartialResponse(5).removeHeader("Etag");
+ runSimpleFailureTest(HTTP_LENGTH_REQUIRED);
+ }
+
+ public void testSanitizeMediaType() throws Exception {
+ enqueueEmptyResponse(HTTP_OK).addHeader("Content-Type", "text/html; charset=ISO-8859-4");
+ Download download = enqueueRequest(getRequest());
+ download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+ assertEquals("text/html", download.getStringField(DownloadManager.COLUMN_MEDIA_TYPE));
+ }
+
+ public void testNoContentLength() throws Exception {
+ enqueueEmptyResponse(HTTP_OK).removeHeader("Content-Length");
+ runSimpleFailureTest(HTTP_LENGTH_REQUIRED);
+ }
+
+ public void testNoContentType() throws Exception {
+ enqueueResponse(HTTP_OK, "", false);
+ runSimpleFailureTest(HTTP_NOT_ACCEPTABLE);
+ }
+
+ public void testInsufficientSpace() throws Exception {
+ // this would be better done by stubbing the system API to check available space, but in the
+ // meantime, just use an absurdly large header value
+ enqueueEmptyResponse(HTTP_OK).addHeader("Content-Length",
+ 1024L * 1024 * 1024 * 1024 * 1024);
+ runSimpleFailureTest(DownloadManager.ERROR_INSUFFICIENT_SPACE);
+ }
+
+ public void testCancel() throws Exception {
+ enqueuePartialResponse(5);
+ Download download = enqueueRequest(getRequest());
+ download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+
+ mManager.remove(download.mId);
+ mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+ startService(null);
+ Thread.sleep(500); // TODO: eliminate this when we can run the service synchronously
+ }
+
+ private void runSimpleFailureTest(int expectedErrorCode) throws Exception {
+ Download download = enqueueRequest(getRequest());
+ download.runUntilStatus(DownloadManager.STATUS_FAILED);
+ assertEquals(expectedErrorCode,
+ download.getLongField(DownloadManager.COLUMN_ERROR_CODE));
+ }
+
+ /**
+ * Run a redirection test consisting of
+ * 1) Request to REQUEST_PATH with 3xx response redirecting to another URI
+ * 2) Request to REDIRECTED_PATH with interrupted partial response
+ * 3) Resume request to complete download
+ * @return the last request sent to the server, resuming after the interruption
+ */
+ private RecordedRequest runRedirectionTest(int status)
+ throws MalformedURLException, Exception {
+ enqueueEmptyResponse(status).addHeader("Location",
+ mServer.getUrl(REDIRECTED_PATH).toString());
+ enqueueInterruptedDownloadResponses(5);
+
+ Download download = enqueueRequest(getRequest());
+ RecordedRequest request = download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+ assertEquals(REQUEST_PATH, request.getPath());
+
+ mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+ request = download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+ assertEquals(REDIRECTED_PATH, request.getPath());
+
+ mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+ request = download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+ return request;
+ }
+
private DownloadManager.Request getRequest() throws MalformedURLException {
return getRequest(getServerUri(REQUEST_PATH));
}
diff --git a/tests/src/tests/http/MockResponse.java b/tests/src/tests/http/MockResponse.java
index 21397019..4cda92d2 100644
--- a/tests/src/tests/http/MockResponse.java
+++ b/tests/src/tests/http/MockResponse.java
@@ -69,8 +69,13 @@ public class MockResponse {
return this;
}
- public MockResponse addHeader(String header, int value) {
- return addHeader(header, Integer.toString(value));
+ public MockResponse addHeader(String header, long value) {
+ return addHeader(header, Long.toString(value));
+ }
+
+ public MockResponse removeHeader(String header) {
+ headers.remove(header.toLowerCase());
+ return this;
}
/**