Major refactoring of DownloadThread.run().
Steve Howard [Fri, 16 Jul 2010 21:28:35 +0000 (14:28 -0700)]
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

src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadThread.java
src/com/android/providers/downloads/Helpers.java
tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
tests/src/tests/http/MockResponse.java

index 153e045..c99a378 100644 (file)
@@ -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;
 
index 4007e76..c210063 100644 (file)
@@ -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 "
index d8271a2..6cb409e 100644 (file)
@@ -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);
 
index 0ad4edc..dac4b55 100644 (file)
@@ -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;
index 326d9ff..92678fe 100644 (file)
@@ -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;
     }
index 3cd9cf5..822ab54 100644 (file)
@@ -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.
      */
index e34c66e..b1ccc7a 100644 (file)
@@ -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));
     }
index 2139701..4cda92d 100644 (file)
@@ -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;
     }
 
     /**