]> nv-tegra.nvidia Code Review - android/platform/packages/providers/DownloadProvider.git/blobdiff - src/com/android/providers/downloads/DownloadThread.java
Implement multi-network downloads
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadThread.java
index 0f1d5f100b5d68d2c7c4df2dab5a09b06c8e222c..8d51909ef47e88b47296302059aefd6aafdf0c41 100644 (file)
 
 package com.android.providers.downloads;
 
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
+import static android.provider.Downloads.Impl.STATUS_CANCELED;
+import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME;
+import static android.provider.Downloads.Impl.STATUS_FILE_ERROR;
+import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR;
+import static android.provider.Downloads.Impl.STATUS_SUCCESS;
+import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
+import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
+import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR;
+import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
+import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
 import static com.android.providers.downloads.Constants.TAG;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
 import static java.net.HttpURLConnection.HTTP_OK;
 import static java.net.HttpURLConnection.HTTP_PARTIAL;
+import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
+import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
 
 import android.content.ContentValues;
@@ -27,200 +43,291 @@ import android.content.Context;
 import android.content.Intent;
 import android.drm.DrmManagerClient;
 import android.drm.DrmOutputStream;
+import android.net.ConnectivityManager;
 import android.net.INetworkPolicyListener;
+import android.net.Network;
+import android.net.NetworkInfo;
 import android.net.NetworkPolicyManager;
 import android.net.TrafficStats;
-import android.os.FileUtils;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.SystemClock;
+import android.os.WorkSource;
 import android.provider.Downloads;
-import android.text.TextUtils;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
 import android.util.Log;
 import android.util.Pair;
 
+import com.android.providers.downloads.DownloadInfo.NetworkState;
+
+import libcore.io.IoUtils;
+
 import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.RandomAccessFile;
 import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
 import java.net.URL;
 import java.net.URLConnection;
 
-import libcore.io.IoUtils;
-
 /**
- * Thread which executes a given {@link DownloadInfo}: making network requests,
+ * Task which executes a given {@link DownloadInfo}: making network requests,
  * persisting data to disk, and updating {@link DownloadProvider}.
+ * <p>
+ * To know if a download is successful, we need to know either the final content
+ * length to expect, or the transfer to be chunked. To resume an interrupted
+ * download, we need an ETag.
+ * <p>
+ * Failed network requests are retried several times before giving up. Local
+ * disk errors fail immediately and are not retried.
  */
-public class DownloadThread extends Thread {
+public class DownloadThread implements Runnable {
+
+    // TODO: bind each download to a specific network interface to avoid state
+    // checking races once we have ConnectivityManager API
+
+    // TODO: add support for saving to content://
 
     private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+    private static final int HTTP_TEMP_REDIRECT = 307;
 
-    private static final int DEFAULT_TIMEOUT = (int) MINUTE_IN_MILLIS;
+    private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
 
     private final Context mContext;
-    private final DownloadInfo mInfo;
     private final SystemFacade mSystemFacade;
-    private final StorageManager mStorageManager;
+    private final DownloadNotifier mNotifier;
 
-    private volatile boolean mPolicyDirty;
-
-    public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
-            StorageManager storageManager) {
-        mContext = context;
-        mSystemFacade = systemFacade;
-        mInfo = info;
-        mStorageManager = storageManager;
-    }
+    private final long mId;
 
     /**
-     * Returns the user agent provided by the initiating app, or use the default one
+     * Info object that should be treated as read-only. Any potentially mutated
+     * fields are tracked in {@link #mInfoDelta}. If a field exists in
+     * {@link #mInfoDelta}, it must not be read from {@link #mInfo}.
      */
-    private String userAgent() {
-        String userAgent = mInfo.mUserAgent;
-        if (userAgent == null) {
-            userAgent = Constants.DEFAULT_USER_AGENT;
-        }
-        return userAgent;
-    }
+    private final DownloadInfo mInfo;
+    private final DownloadInfoDelta mInfoDelta;
+
+    private volatile boolean mPolicyDirty;
 
     /**
-     * State for the entire run() method.
+     * Local changes to {@link DownloadInfo}. These are kept local to avoid
+     * racing with the thread that updates based on change notifications.
      */
-    static class State {
-        public String mFilename;
+    private class DownloadInfoDelta {
+        public String mUri;
+        public String mFileName;
         public String mMimeType;
-        public boolean mCountRetry = false;
-        public int mRetryAfter = 0;
-        public boolean mGotData = false;
-        public String mRequestUri;
-        public long mTotalBytes = -1;
-        public long mCurrentBytes = 0;
-        public String mHeaderETag;
-        public boolean mContinuingDownload = false;
-        public long mBytesNotified = 0;
-        public long mTimeLastNotification = 0;
-
-        /** Historical bytes/second speed of this download. */
-        public long mSpeed;
-        /** Time when current sample started. */
-        public long mSpeedSampleStart;
-        /** Bytes transferred since current sample started. */
-        public long mSpeedSampleBytes;
-
-        public State(DownloadInfo info) {
-            mMimeType = Intent.normalizeMimeType(info.mMimeType);
-            mRequestUri = info.mUri;
-            mFilename = info.mFileName;
+        public int mStatus;
+        public int mNumFailed;
+        public int mRetryAfter;
+        public long mTotalBytes;
+        public long mCurrentBytes;
+        public String mETag;
+
+        public String mErrorMsg;
+
+        public DownloadInfoDelta(DownloadInfo info) {
+            mUri = info.mUri;
+            mFileName = info.mFileName;
+            mMimeType = info.mMimeType;
+            mStatus = info.mStatus;
+            mNumFailed = info.mNumFailed;
+            mRetryAfter = info.mRetryAfter;
             mTotalBytes = info.mTotalBytes;
             mCurrentBytes = info.mCurrentBytes;
+            mETag = info.mETag;
+        }
+
+        private ContentValues buildContentValues() {
+            final ContentValues values = new ContentValues();
+
+            values.put(Downloads.Impl.COLUMN_URI, mUri);
+            values.put(Downloads.Impl._DATA, mFileName);
+            values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
+            values.put(Downloads.Impl.COLUMN_STATUS, mStatus);
+            values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed);
+            values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
+            values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes);
+            values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes);
+            values.put(Constants.ETAG, mETag);
+
+            values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
+            values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg);
+
+            return values;
+        }
+
+        /**
+         * Blindly push update of current delta values to provider.
+         */
+        public void writeToDatabase() {
+            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(),
+                    null, null);
+        }
+
+        /**
+         * Push update of current delta values to provider, asserting strongly
+         * that we haven't been paused or deleted.
+         */
+        public void writeToDatabaseOrThrow() throws StopRequestException {
+            if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
+                    buildContentValues(), Downloads.Impl.COLUMN_DELETED + " == '0'", null) == 0) {
+                throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!");
+            }
         }
     }
 
     /**
-     * State within executeDownload()
+     * Flag indicating if we've made forward progress transferring file data
+     * from a remote server.
      */
-    private static class InnerState {
-        public long mContentLength;
-        public String mContentDisposition;
-        public String mContentLocation;
-    }
+    private boolean mMadeProgress = false;
 
     /**
-     * Executes the download in a separate thread
+     * Details from the last time we pushed a database update.
      */
+    private long mLastUpdateBytes = 0;
+    private long mLastUpdateTime = 0;
+
+    private int mNetworkType = ConnectivityManager.TYPE_NONE;
+
+    /** Historical bytes/second speed of this download. */
+    private long mSpeed;
+    /** Time when current sample started. */
+    private long mSpeedSampleStart;
+    /** Bytes transferred since current sample started. */
+    private long mSpeedSampleBytes;
+
+    public DownloadThread(Context context, SystemFacade systemFacade, DownloadNotifier notifier,
+            DownloadInfo info) {
+        mContext = context;
+        mSystemFacade = systemFacade;
+        mNotifier = notifier;
+
+        mId = info.mId;
+        mInfo = info;
+        mInfoDelta = new DownloadInfoDelta(info);
+    }
+
     @Override
     public void run() {
         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-        try {
-            runInternal();
-        } finally {
-            DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
-        }
-    }
 
-    private void runInternal() {
         // Skip when download already marked as finished; this download was
         // probably started again while racing with UpdateThread.
-        if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
+        if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mId)
                 == Downloads.Impl.STATUS_SUCCESS) {
-            Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
+            logDebug("Already finished; skipping");
             return;
         }
 
-        State state = new State(mInfo);
-        PowerManager.WakeLock wakeLock = null;
-        int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
-        String errorMsg = null;
-
         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
+        PowerManager.WakeLock wakeLock = null;
         final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
 
         try {
             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
+            wakeLock.setWorkSource(new WorkSource(mInfo.mUid));
             wakeLock.acquire();
 
             // while performing download, register for rules updates
             netPolicy.registerListener(mPolicyListener);
 
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
+            logDebug("Starting");
+
+            // Remember which network this download started on; used to
+            // determine if errors were due to network changes.
+            final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+            if (info != null) {
+                mNetworkType = info.getType();
             }
 
-            // network traffic on this thread should be counted against the
-            // requesting uid, and is tagged with well-known value.
+            // Network traffic on this thread should be counted against the
+            // requesting UID, and is tagged with well-known value.
             TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
             TrafficStats.setThreadStatsUid(mInfo.mUid);
 
-            boolean finished = false;
-            while (!finished) {
-                Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
+            executeDownload();
 
-                final URL url = new URL(state.mRequestUri);
-                final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
-                conn.setConnectTimeout(DEFAULT_TIMEOUT);
-                conn.setReadTimeout(DEFAULT_TIMEOUT);
-                try {
-                    executeDownload(state, conn);
-                    finished = true;
-                } finally {
-                    conn.disconnect();
-                }
+            mInfoDelta.mStatus = STATUS_SUCCESS;
+            TrafficStats.incrementOperationCount(1);
+
+            // If we just finished a chunked file, record total size
+            if (mInfoDelta.mTotalBytes == -1) {
+                mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes;
             }
 
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
+        } catch (StopRequestException e) {
+            mInfoDelta.mStatus = e.getFinalStatus();
+            mInfoDelta.mErrorMsg = e.getMessage();
+
+            logWarning("Stop requested with status "
+                    + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": "
+                    + mInfoDelta.mErrorMsg);
+
+            // Nobody below our level should request retries, since we handle
+            // failure counts at this level.
+            if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) {
+                throw new IllegalStateException("Execution should always throw final error codes");
             }
-            finalizeDestinationFile(state);
-            finalStatus = Downloads.Impl.STATUS_SUCCESS;
-        } catch (StopRequestException error) {
-            // remove the cause before printing, in case it contains PII
-            errorMsg = error.getMessage();
-            String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
-            Log.w(Constants.TAG, msg);
-            if (Constants.LOGV) {
-                Log.w(Constants.TAG, msg, error);
+
+            // Some errors should be retryable, unless we fail too many times.
+            if (isStatusRetryable(mInfoDelta.mStatus)) {
+                if (mMadeProgress) {
+                    mInfoDelta.mNumFailed = 1;
+                } else {
+                    mInfoDelta.mNumFailed += 1;
+                }
+
+                if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
+                    final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+                    if (info != null && info.getType() == mNetworkType && info.isConnected()) {
+                        // Underlying network is still intact, use normal backoff
+                        mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
+                    } else {
+                        // Network changed, retry on any next available
+                        mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
+                    }
+
+                    if ((mInfoDelta.mETag == null && mMadeProgress)
+                            || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
+                        // However, if we wrote data and have no ETag to verify
+                        // contents against later, we can't actually resume.
+                        mInfoDelta.mStatus = STATUS_CANNOT_RESUME;
+                    }
+                }
             }
-            finalStatus = error.mFinalStatus;
-            // fall through to finally block
-        } catch (Throwable ex) {
-            errorMsg = ex.getMessage();
-            String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
-            Log.w(Constants.TAG, msg, ex);
-            finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
-            // falls through to the code that reports an error
+
+        } catch (Throwable t) {
+            mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR;
+            mInfoDelta.mErrorMsg = t.toString();
+
+            logError("Failed: " + mInfoDelta.mErrorMsg, t);
+
         } finally {
+            logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus));
+
+            mNotifier.notifyDownloadSpeed(mId, 0);
+
+            finalizeDestination();
+
+            mInfoDelta.writeToDatabase();
+
+            if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) {
+                mInfo.sendIntentIfRequested();
+            }
+
             TrafficStats.clearThreadStatsTag();
             TrafficStats.clearThreadStatsUid();
 
-            cleanupDestination(state, finalStatus);
-            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
-                    state.mGotData, state.mFilename, state.mMimeType, errorMsg);
-
             netPolicy.unregisterListener(mPolicyListener);
 
             if (wakeLock != null) {
@@ -228,71 +335,205 @@ public class DownloadThread extends Thread {
                 wakeLock = null;
             }
         }
-        mStorageManager.incrementNumDownloadsSoFar();
     }
 
     /**
-     * Fully execute a single download request - setup and send the request, handle the response,
-     * and transfer the data to the destination file.
+     * Fully execute a single download request. Setup and send the request,
+     * handle the response, and transfer the data to the destination file.
      */
-    private void executeDownload(State state, HttpURLConnection conn) throws StopRequestException {
-        final InnerState innerState = new InnerState();
+    private void executeDownload() throws StopRequestException {
+        final boolean resuming = mInfoDelta.mCurrentBytes != 0;
 
-        setupDestinationFile(state, innerState);
-        addRequestHeaders(state, conn);
+        URL url;
+        try {
+            // TODO: migrate URL sanity checking into client side of API
+            url = new URL(mInfoDelta.mUri);
+        } catch (MalformedURLException e) {
+            throw new StopRequestException(STATUS_BAD_REQUEST, e);
+        }
+
+        final Network network = mSystemFacade.getActiveNetwork(mInfo.mUid);
+        if (network == null) {
+            throw new StopRequestException(Downloads.Impl.STATUS_WAITING_FOR_NETWORK,
+                    "no network associated with requesting UID");
+        }
+        logDebug("Using network: " + network);
+
+        boolean cleartextTrafficPermitted = mSystemFacade.isCleartextTrafficPermitted(mInfo.mUid);
+        int redirectionCount = 0;
+        while (redirectionCount++ < Constants.MAX_REDIRECTS) {
+            // Enforce the cleartext traffic opt-out for the UID. This cannot be enforced earlier
+            // because of HTTP redirects which can change the protocol between HTTP and HTTPS.
+            if ((!cleartextTrafficPermitted) && ("http".equalsIgnoreCase(url.getProtocol()))) {
+                throw new StopRequestException(STATUS_BAD_REQUEST,
+                        "Cleartext traffic not permitted for UID " + mInfo.mUid + ": "
+                        + Uri.parse(url.toString()).toSafeString());
+            }
 
-        // skip when already finished; remove after fixing race in 5217390
-        if (state.mCurrentBytes == state.mTotalBytes) {
-            Log.i(Constants.TAG, "Skipping initiating request for download " +
-                  mInfo.mId + "; already completed");
-            return;
+            // Open connection and follow any redirects until we have a useful
+            // response with body.
+            HttpURLConnection conn = null;
+            try {
+                checkConnectivity();
+                conn = (HttpURLConnection) network.openConnection(url);
+                conn.setInstanceFollowRedirects(false);
+                conn.setConnectTimeout(DEFAULT_TIMEOUT);
+                conn.setReadTimeout(DEFAULT_TIMEOUT);
+
+                addRequestHeaders(conn, resuming);
+
+                final int responseCode = conn.getResponseCode();
+                switch (responseCode) {
+                    case HTTP_OK:
+                        if (resuming) {
+                            throw new StopRequestException(
+                                    STATUS_CANNOT_RESUME, "Expected partial, but received OK");
+                        }
+                        parseOkHeaders(conn);
+                        transferData(conn);
+                        return;
+
+                    case HTTP_PARTIAL:
+                        if (!resuming) {
+                            throw new StopRequestException(
+                                    STATUS_CANNOT_RESUME, "Expected OK, but received partial");
+                        }
+                        transferData(conn);
+                        return;
+
+                    case HTTP_MOVED_PERM:
+                    case HTTP_MOVED_TEMP:
+                    case HTTP_SEE_OTHER:
+                    case HTTP_TEMP_REDIRECT:
+                        final String location = conn.getHeaderField("Location");
+                        url = new URL(url, location);
+                        if (responseCode == HTTP_MOVED_PERM) {
+                            // Push updated URL back to database
+                            mInfoDelta.mUri = url.toString();
+                        }
+                        continue;
+
+                    case HTTP_PRECON_FAILED:
+                        throw new StopRequestException(
+                                STATUS_CANNOT_RESUME, "Precondition failed");
+
+                    case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
+                        throw new StopRequestException(
+                                STATUS_CANNOT_RESUME, "Requested range not satisfiable");
+
+                    case HTTP_UNAVAILABLE:
+                        parseUnavailableHeaders(conn);
+                        throw new StopRequestException(
+                                HTTP_UNAVAILABLE, conn.getResponseMessage());
+
+                    case HTTP_INTERNAL_ERROR:
+                        throw new StopRequestException(
+                                HTTP_INTERNAL_ERROR, conn.getResponseMessage());
+
+                    default:
+                        StopRequestException.throwUnhandledHttpError(
+                                responseCode, conn.getResponseMessage());
+                }
+
+            } catch (IOException e) {
+                if (e instanceof ProtocolException
+                        && e.getMessage().startsWith("Unexpected status line")) {
+                    throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e);
+                } else {
+                    // Trouble with low-level sockets
+                    throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
+                }
+
+            } finally {
+                if (conn != null) conn.disconnect();
+            }
         }
 
-        // check just before sending the request to avoid using an invalid connection at all
-        checkConnectivity();
+        throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
+    }
+
+    /**
+     * Transfer data from the given connection to the destination file.
+     */
+    private void transferData(HttpURLConnection conn) throws StopRequestException {
+
+        // To detect when we're really finished, we either need a length, closed
+        // connection, or chunked encoding.
+        final boolean hasLength = mInfoDelta.mTotalBytes != -1;
+        final boolean isConnectionClose = "close".equalsIgnoreCase(
+                conn.getHeaderField("Connection"));
+        final boolean isEncodingChunked = "chunked".equalsIgnoreCase(
+                conn.getHeaderField("Transfer-Encoding"));
+
+        final boolean finishKnown = hasLength || isConnectionClose || isEncodingChunked;
+        if (!finishKnown) {
+            throw new StopRequestException(
+                    STATUS_CANNOT_RESUME, "can't know size of download, giving up");
+        }
 
         DrmManagerClient drmClient = null;
+        ParcelFileDescriptor outPfd = null;
+        FileDescriptor outFd = null;
         InputStream in = null;
         OutputStream out = null;
-        FileDescriptor outFd = null;
         try {
             try {
-                // Asking for response code will execute the request
-                final int statusCode = conn.getResponseCode();
                 in = conn.getInputStream();
-
-                handleExceptionalStatus(state, innerState, conn, statusCode);
-                processResponseHeaders(state, innerState, conn);
             } catch (IOException e) {
-                throw new StopRequestException(
-                        getFinalStatusForHttpError(state), "Request failed: " + e, e);
+                throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
             }
 
             try {
-                if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
+                outPfd = mContext.getContentResolver()
+                        .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
+                outFd = outPfd.getFileDescriptor();
+
+                if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
                     drmClient = new DrmManagerClient(mContext);
-                    final RandomAccessFile file = new RandomAccessFile(
-                            new File(state.mFilename), "rw");
-                    out = new DrmOutputStream(drmClient, file, state.mMimeType);
-                    outFd = file.getFD();
+                    out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType);
                 } else {
-                    out = new FileOutputStream(state.mFilename, true);
-                    outFd = ((FileOutputStream) out).getFD();
+                    out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd);
+                }
+
+                // Pre-flight disk space requirements, when known
+                if (mInfoDelta.mTotalBytes > 0) {
+                    final long curSize = Os.fstat(outFd).st_size;
+                    final long newBytes = mInfoDelta.mTotalBytes - curSize;
+
+                    StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
+
+                    try {
+                        // We found enough space, so claim it for ourselves
+                        Os.posix_fallocate(outFd, 0, mInfoDelta.mTotalBytes);
+                    } catch (ErrnoException e) {
+                        if (e.errno == OsConstants.ENOSYS || e.errno == OsConstants.ENOTSUP) {
+                            Log.w(TAG, "fallocate() not supported; falling back to ftruncate()");
+                            Os.ftruncate(outFd, mInfoDelta.mTotalBytes);
+                        } else {
+                            throw e;
+                        }
+                    }
                 }
+
+                // Move into place to begin writing
+                Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET);
+
+            } catch (ErrnoException e) {
+                throw new StopRequestException(STATUS_FILE_ERROR, e);
             } catch (IOException e) {
-                throw new StopRequestException(
-                        Downloads.Impl.STATUS_FILE_ERROR, "Failed to open destination: " + e, e);
+                throw new StopRequestException(STATUS_FILE_ERROR, e);
             }
 
-            transferData(state, innerState, in, out);
+            // Start streaming data, periodically watch for pause/cancel
+            // commands and checking disk space as needed.
+            transferData(in, out, outFd);
 
             try {
                 if (out instanceof DrmOutputStream) {
                     ((DrmOutputStream) out).finish();
                 }
             } catch (IOException e) {
-                throw new StopRequestException(
-                        Downloads.Impl.STATUS_FILE_ERROR, "Failed to finish: " + e, e);
+                throw new StopRequestException(STATUS_FILE_ERROR, e);
             }
 
         } finally {
@@ -312,91 +553,139 @@ public class DownloadThread extends Thread {
         }
     }
 
-    /**
-     * Check if current connectivity is valid for this request.
-     */
-    private void checkConnectivity() throws StopRequestException {
-        // checking connectivity will apply current policy
-        mPolicyDirty = false;
-
-        int networkUsable = mInfo.checkCanUseNetwork();
-        if (networkUsable != DownloadInfo.NETWORK_OK) {
-            int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
-            if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
-                status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
-                mInfo.notifyPauseDueToSize(true);
-            } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
-                status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
-                mInfo.notifyPauseDueToSize(false);
-            }
-            throw new StopRequestException(status,
-                    mInfo.getLogMessageForNetworkError(networkUsable));
-        }
-    }
-
     /**
      * Transfer as much data as possible from the HTTP response to the
      * destination file.
      */
-    private void transferData(State state, InnerState innerState, InputStream in, OutputStream out)
+    private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
             throws StopRequestException {
-        final byte data[] = new byte[Constants.BUFFER_SIZE];
-        for (;;) {
-            int bytesRead = readFromResponse(state, innerState, data, in);
-            if (bytesRead == -1) { // success, end of stream already reached
-                handleEndOfStream(state, innerState);
-                return;
+        final byte buffer[] = new byte[Constants.BUFFER_SIZE];
+        while (true) {
+            checkPausedOrCanceled();
+
+            int len = -1;
+            try {
+                len = in.read(buffer);
+            } catch (IOException e) {
+                throw new StopRequestException(
+                        STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e);
+            }
+
+            if (len == -1) {
+                break;
             }
 
-            state.mGotData = true;
-            writeDataToDestination(state, data, bytesRead, out);
-            state.mCurrentBytes += bytesRead;
-            reportProgress(state, innerState);
+            try {
+                // When streaming, ensure space before each write
+                if (mInfoDelta.mTotalBytes == -1) {
+                    final long curSize = Os.fstat(outFd).st_size;
+                    final long newBytes = (mInfoDelta.mCurrentBytes + len) - curSize;
+
+                    StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
+                }
+
+                out.write(buffer, 0, len);
+
+                mMadeProgress = true;
+                mInfoDelta.mCurrentBytes += len;
 
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
-                      + mInfo.mUri);
+                updateProgress(outFd);
+
+            } catch (ErrnoException e) {
+                throw new StopRequestException(STATUS_FILE_ERROR, e);
+            } catch (IOException e) {
+                throw new StopRequestException(STATUS_FILE_ERROR, e);
             }
+        }
 
-            checkPausedOrCanceled(state);
+        // Finished without error; verify length if known
+        if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) {
+            throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch");
         }
     }
 
     /**
-     * Called after a successful completion to take any necessary action on the downloaded file.
+     * Called just before the thread finishes, regardless of status, to take any
+     * necessary action on the downloaded file.
      */
-    private void finalizeDestinationFile(State state) {
-        if (state.mFilename != null) {
-            // make sure the file is readable
-            FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
+    private void finalizeDestination() {
+        if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) {
+            // When error, free up any disk space
+            try {
+                final ParcelFileDescriptor target = mContext.getContentResolver()
+                        .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
+                try {
+                    Os.ftruncate(target.getFileDescriptor(), 0);
+                } catch (ErrnoException ignored) {
+                } finally {
+                    IoUtils.closeQuietly(target);
+                }
+            } catch (FileNotFoundException ignored) {
+            }
+
+            // Delete if local file
+            if (mInfoDelta.mFileName != null) {
+                new File(mInfoDelta.mFileName).delete();
+                mInfoDelta.mFileName = null;
+            }
+
+        } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
+            // When success, open access if local file
+            if (mInfoDelta.mFileName != null) {
+                if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
+                    try {
+                        // Move into final resting place, if needed
+                        final File before = new File(mInfoDelta.mFileName);
+                        final File beforeDir = Helpers.getRunningDestinationDirectory(
+                                mContext, mInfo.mDestination);
+                        final File afterDir = Helpers.getSuccessDestinationDirectory(
+                                mContext, mInfo.mDestination);
+                        if (!beforeDir.equals(afterDir)
+                                && before.getParentFile().equals(beforeDir)) {
+                            final File after = new File(afterDir, before.getName());
+                            if (before.renameTo(after)) {
+                                mInfoDelta.mFileName = after.getAbsolutePath();
+                            }
+                        }
+                    } catch (IOException ignored) {
+                    }
+                }
+            }
         }
     }
 
     /**
-     * Called just before the thread finishes, regardless of status, to take any necessary action on
-     * the downloaded file.
+     * Check if current connectivity is valid for this request.
      */
-    private void cleanupDestination(State state, int finalStatus) {
-        if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
-            if (Constants.LOGVV) {
-                Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
+    private void checkConnectivity() throws StopRequestException {
+        // checking connectivity will apply current policy
+        mPolicyDirty = false;
+
+        final NetworkState networkUsable = mInfo.checkCanUseNetwork(mInfoDelta.mTotalBytes);
+        if (networkUsable != NetworkState.OK) {
+            int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
+            if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
+                status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
+                mInfo.notifyPauseDueToSize(true);
+            } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
+                status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
+                mInfo.notifyPauseDueToSize(false);
             }
-            new File(state.mFilename).delete();
-            state.mFilename = null;
+            throw new StopRequestException(status, networkUsable.name());
         }
     }
 
     /**
-     * Check if the download has been paused or canceled, stopping the request appropriately if it
-     * has been.
+     * Check if the download has been paused or canceled, stopping the request
+     * appropriately if it has been.
      */
-    private void checkPausedOrCanceled(State state) throws StopRequestException {
+    private void checkPausedOrCanceled() throws StopRequestException {
         synchronized (mInfo) {
             if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
                 throw new StopRequestException(
                         Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
             }
-            if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
+            if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) {
                 throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
             }
         }
@@ -410,412 +699,137 @@ public class DownloadThread extends Thread {
     /**
      * Report download progress through the database if necessary.
      */
-    private void reportProgress(State state, InnerState innerState) {
+    private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException {
         final long now = SystemClock.elapsedRealtime();
+        final long currentBytes = mInfoDelta.mCurrentBytes;
 
-        final long sampleDelta = now - state.mSpeedSampleStart;
+        final long sampleDelta = now - mSpeedSampleStart;
         if (sampleDelta > 500) {
-            final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
+            final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000)
                     / sampleDelta;
 
-            if (state.mSpeed == 0) {
-                state.mSpeed = sampleSpeed;
+            if (mSpeed == 0) {
+                mSpeed = sampleSpeed;
             } else {
-                state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
+                mSpeed = ((mSpeed * 3) + sampleSpeed) / 4;
             }
 
-            state.mSpeedSampleStart = now;
-            state.mSpeedSampleBytes = state.mCurrentBytes;
-
-            DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
-        }
+            // Only notify once we have a full sample window
+            if (mSpeedSampleStart != 0) {
+                mNotifier.notifyDownloadSpeed(mId, mSpeed);
+            }
 
-        if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
-            now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
-            ContentValues values = new ContentValues();
-            values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
-            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
-            state.mBytesNotified = state.mCurrentBytes;
-            state.mTimeLastNotification = now;
+            mSpeedSampleStart = now;
+            mSpeedSampleBytes = currentBytes;
         }
-    }
 
-    /**
-     * 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, OutputStream out)
-            throws StopRequestException {
-        mStorageManager.verifySpaceBeforeWritingToFile(
-                mInfo.mDestination, state.mFilename, bytesRead);
+        final long bytesDelta = currentBytes - mLastUpdateBytes;
+        final long timeDelta = now - mLastUpdateTime;
+        if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) {
+            // fsync() to ensure that current progress has been flushed to disk,
+            // so we can always resume based on latest database information.
+            outFd.sync();
 
-        boolean forceVerified = false;
-        while (true) {
-            try {
-                out.write(data, 0, bytesRead);
-                return;
-            } catch (IOException ex) {
-                // TODO: better differentiate between DRM and disk failures
-                if (!forceVerified) {
-                    // couldn't write to file. are we out of space? check.
-                    mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
-                    forceVerified = true;
-                } else {
-                    throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
-                            "Failed to write data: " + ex);
-                }
-            }
-        }
-    }
+            mInfoDelta.writeToDatabaseOrThrow();
 
-    /**
-     * 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 StopRequestException {
-        ContentValues values = new ContentValues();
-        values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
-        if (innerState.mContentLength == -1) {
-            values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
-        }
-        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
-
-        boolean lengthMismatched = (innerState.mContentLength != -1)
-                && (state.mCurrentBytes != innerState.mContentLength);
-        if (lengthMismatched) {
-            if (cannotResume(state)) {
-                throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
-                        "mismatched content length; unable to resume");
-            } else {
-                throw new StopRequestException(getFinalStatusForHttpError(state),
-                        "closed socket before end of file");
-            }
+            mLastUpdateBytes = currentBytes;
+            mLastUpdateTime = now;
         }
     }
 
-    private boolean cannotResume(State state) {
-        return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
-                || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
-    }
-
     /**
-     * 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
+     * Process response headers from first server response. This derives its
+     * filename, size, and ETag.
      */
-    private int readFromResponse(State state, InnerState innerState, byte[] data,
-                                 InputStream entityStream) throws StopRequestException {
-        try {
-            return entityStream.read(data);
-        } catch (IOException ex) {
-            // TODO: handle stream errors the same as other retries
-            if ("unexpected end of stream".equals(ex.getMessage())) {
-                return -1;
-            }
+    private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException {
+        if (mInfoDelta.mFileName == null) {
+            final String contentDisposition = conn.getHeaderField("Content-Disposition");
+            final String contentLocation = conn.getHeaderField("Content-Location");
 
-            ContentValues values = new ContentValues();
-            values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
-            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
-            if (cannotResume(state)) {
-                throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
-                        "Failed reading response: " + ex + "; unable to resume", ex);
-            } else {
-                throw new StopRequestException(getFinalStatusForHttpError(state),
-                        "Failed reading response: " + ex, ex);
+            try {
+                mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri,
+                        mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType,
+                        mInfo.mDestination);
+            } catch (IOException e) {
+                throw new StopRequestException(
+                        Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e);
             }
         }
-    }
 
-    /**
-     * 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, HttpURLConnection conn)
-            throws StopRequestException {
-        if (state.mContinuingDownload) {
-            // ignore response headers on resume requests
-            return;
+        if (mInfoDelta.mMimeType == null) {
+            mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType());
         }
 
-        readResponseHeaders(state, innerState, conn);
-
-        state.mFilename = Helpers.generateSaveFile(
-                mContext,
-                mInfo.mUri,
-                mInfo.mHint,
-                innerState.mContentDisposition,
-                innerState.mContentLocation,
-                state.mMimeType,
-                mInfo.mDestination,
-                innerState.mContentLength,
-                mInfo.mIsPublicApi, mStorageManager);
-
-        updateDatabaseFromHeaders(state, innerState);
-        // check connectivity again now that we know the total size
-        checkConnectivity();
-    }
-
-    /**
-     * 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 (state.mHeaderETag != null) {
-            values.put(Constants.ETAG, state.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(mInfo.getAllDownloadsUri(), values, null, null);
-    }
-
-    /**
-     * Read headers from the HTTP response and store them into local state.
-     */
-    private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection conn)
-            throws StopRequestException {
-        innerState.mContentDisposition = conn.getHeaderField("Content-Disposition");
-        innerState.mContentLocation = conn.getHeaderField("Content-Location");
-
-        if (state.mMimeType == null) {
-            state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
-        }
-
-        state.mHeaderETag = conn.getHeaderField("ETag");
-
         final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
         if (transferEncoding == null) {
-            innerState.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
+            mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1);
         } else {
-            Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
-            innerState.mContentLength = -1;
+            mInfoDelta.mTotalBytes = -1;
         }
 
-        state.mTotalBytes = innerState.mContentLength;
-        mInfo.mTotalBytes = innerState.mContentLength;
+        mInfoDelta.mETag = conn.getHeaderField("ETag");
 
-        final boolean noSizeInfo = innerState.mContentLength == -1
-                && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
-        if (!mInfo.mNoIntegrity && noSizeInfo) {
-            throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
-                    "can't know size of download, giving up");
-        }
-    }
+        mInfoDelta.writeToDatabaseOrThrow();
 
-    /**
-     * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
-     */
-    private void handleExceptionalStatus(
-            State state, InnerState innerState, HttpURLConnection conn, int statusCode)
-            throws StopRequestException {
-        if (statusCode == HTTP_UNAVAILABLE && mInfo.mNumFailed < Constants.MAX_RETRIES) {
-            handleServiceUnavailable(state, conn);
-        }
-
-        if (Constants.LOGV) {
-            Log.i(Constants.TAG, "recevd_status = " + statusCode +
-                    ", mContinuingDownload = " + state.mContinuingDownload);
-        }
-        int expectedStatus = state.mContinuingDownload ? HTTP_PARTIAL : HTTP_OK;
-        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 StopRequestException {
-        if (statusCode == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
-            // range request failed. it should never fail.
-            throw new IllegalStateException("Http Range request failure: totalBytes = " +
-                    state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
-        }
-        int finalStatus;
-        if (statusCode >= 400 && statusCode < 600) {
-            finalStatus = statusCode;
-        } else if (statusCode >= 300 && statusCode < 400) {
-            finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
-        } else if (state.mContinuingDownload && statusCode == HTTP_OK) {
-            finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
-        } else {
-            finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
-        }
-        throw new StopRequestException(finalStatus, "http error " +
-                statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
+        // Check connectivity again now that we know the total size
+        checkConnectivity();
     }
 
-    /**
-     * Handle a 503 Service Unavailable status by processing the Retry-After header.
-     */
-    private void handleServiceUnavailable(State state, HttpURLConnection conn)
-            throws StopRequestException {
-        state.mCountRetry = true;
-        state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
-        if (state.mRetryAfter < 0) {
-            state.mRetryAfter = 0;
+    private void parseUnavailableHeaders(HttpURLConnection conn) {
+        long retryAfter = conn.getHeaderFieldInt("Retry-After", -1);
+        if (retryAfter < 0) {
+            retryAfter = 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;
+            if (retryAfter < Constants.MIN_RETRY_AFTER) {
+                retryAfter = Constants.MIN_RETRY_AFTER;
+            } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
+                retryAfter = Constants.MAX_RETRY_AFTER;
             }
-            state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
-            state.mRetryAfter *= 1000;
+            retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
         }
 
-        throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
-                "got 503 Service Unavailable, will retry later");
-    }
-
-    private int getFinalStatusForHttpError(State state) {
-        int networkUsable = mInfo.checkCanUseNetwork();
-        if (networkUsable != DownloadInfo.NETWORK_OK) {
-            switch (networkUsable) {
-                case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
-                case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
-                    return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
-                default:
-                    return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
-            }
-        } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
-            state.mCountRetry = true;
-            return Downloads.Impl.STATUS_WAITING_TO_RETRY;
-        } else {
-            Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
-            return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
-        }
-    }
-
-    /**
-     * 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 StopRequestException {
-        if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
-            if (Constants.LOGV) {
-                Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
-                        ", and state.mFilename: " + state.mFilename);
-            }
-            if (!Helpers.isFilenameValid(state.mFilename,
-                    mStorageManager.getDownloadDataDirectory())) {
-                // this should never happen
-                throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
-                        "found invalid internal destination filename");
-            }
-            // We're resuming a download that got interrupted
-            File f = new File(state.mFilename);
-            if (f.exists()) {
-                if (Constants.LOGV) {
-                    Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
-                            ", and state.mFilename: " + state.mFilename);
-                }
-                long fileLength = f.length();
-                if (fileLength == 0) {
-                    // The download hadn't actually started, we can restart from scratch
-                    if (Constants.LOGVV) {
-                        Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
-                                + state.mFilename);
-                    }
-                    f.delete();
-                    state.mFilename = null;
-                    if (Constants.LOGV) {
-                        Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
-                                ", BUT starting from scratch again: ");
-                    }
-                } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
-                    // This should've been caught upon failure
-                    if (Constants.LOGVV) {
-                        Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
-                                + state.mFilename);
-                    }
-                    f.delete();
-                    throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
-                            "Trying to resume a download that can't be resumed");
-                } else {
-                    // All right, we'll be able to resume this download
-                    if (Constants.LOGV) {
-                        Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
-                                ", and starting with file of length: " + fileLength);
-                    }
-                    state.mCurrentBytes = (int) fileLength;
-                    if (mInfo.mTotalBytes != -1) {
-                        innerState.mContentLength = mInfo.mTotalBytes;
-                    }
-                    state.mHeaderETag = mInfo.mETag;
-                    state.mContinuingDownload = true;
-                    if (Constants.LOGV) {
-                        Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
-                                ", state.mCurrentBytes: " + state.mCurrentBytes +
-                                ", and setting mContinuingDownload to true: ");
-                    }
-                }
-            }
-        }
+        mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS);
     }
 
     /**
      * Add custom headers for this download to the HTTP request.
      */
-    private void addRequestHeaders(State state, HttpURLConnection conn) {
-        conn.addRequestProperty("User-Agent", userAgent());
-
+    private void addRequestHeaders(HttpURLConnection conn, boolean resuming) {
         for (Pair<String, String> header : mInfo.getHeaders()) {
             conn.addRequestProperty(header.first, header.second);
         }
 
-        if (state.mContinuingDownload) {
-            if (state.mHeaderETag != null) {
-                conn.addRequestProperty("If-Match", state.mHeaderETag);
-            }
-            conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
-            if (Constants.LOGV) {
-                Log.i(Constants.TAG, "Adding Range header: " +
-                        "bytes=" + state.mCurrentBytes + "-");
-                Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
+        // Only splice in user agent when not already defined
+        if (conn.getRequestProperty("User-Agent") == null) {
+            conn.addRequestProperty("User-Agent", mInfo.getUserAgent());
+        }
+
+        // Defeat transparent gzip compression, since it doesn't allow us to
+        // easily resume partial downloads.
+        conn.setRequestProperty("Accept-Encoding", "identity");
+
+        // Defeat connection reuse, since otherwise servers may continue
+        // streaming large downloads after cancelled.
+        conn.setRequestProperty("Connection", "close");
+
+        if (resuming) {
+            if (mInfoDelta.mETag != null) {
+                conn.addRequestProperty("If-Match", mInfoDelta.mETag);
             }
+            conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-");
         }
     }
 
-    /**
-     * Stores information about the completed download, and notifies the initiating application.
-     */
-    private void notifyDownloadCompleted(int status, boolean countRetry, int retryAfter,
-            boolean gotData, String filename, String mimeType, String errorMsg) {
-        notifyThroughDatabase(
-                status, countRetry, retryAfter, gotData, filename, mimeType, errorMsg);
-        if (Downloads.Impl.isStatusCompleted(status)) {
-            mInfo.sendIntentIfRequested();
-        }
+    private void logDebug(String msg) {
+        Log.d(TAG, "[" + mId + "] " + msg);
     }
 
-    private void notifyThroughDatabase(int status, boolean countRetry, int retryAfter,
-            boolean gotData, String filename, String mimeType, String errorMsg) {
-        ContentValues values = new ContentValues();
-        values.put(Downloads.Impl.COLUMN_STATUS, status);
-        values.put(Downloads.Impl._DATA, filename);
-        values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
-        values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
-        values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
-        if (!countRetry) {
-            values.put(Constants.FAILED_CONNECTIONS, 0);
-        } else if (gotData) {
-            values.put(Constants.FAILED_CONNECTIONS, 1);
-        } else {
-            values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
-        }
-        // save the error message. could be useful to developers.
-        if (!TextUtils.isEmpty(errorMsg)) {
-            values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
-        }
-        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
+    private void logWarning(String msg) {
+        Log.w(TAG, "[" + mId + "] " + msg);
+    }
+
+    private void logError(String msg, Throwable t) {
+        Log.e(TAG, "[" + mId + "] " + msg, t);
     }
 
     private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
@@ -840,11 +854,27 @@ public class DownloadThread extends Thread {
         }
     };
 
-    public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
+    private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
         try {
             return Long.parseLong(conn.getHeaderField(field));
         } catch (NumberFormatException e) {
             return defaultValue;
         }
     }
+
+    /**
+     * Return if given status is eligible to be treated as
+     * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
+     */
+    public static boolean isStatusRetryable(int status) {
+        switch (status) {
+            case STATUS_HTTP_DATA_ERROR:
+            case HTTP_UNAVAILABLE:
+            case HTTP_INTERNAL_ERROR:
+            case STATUS_FILE_ERROR:
+                return true;
+            default:
+                return false;
+        }
+    }
 }