am 64f6b529: Merge "Avoid NullPointerException"

* commit '64f6b529963d1ec997cff60a79c9d249e5e09040':
  Avoid NullPointerException
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7a1ce39..3024a17 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -57,6 +57,7 @@
 
     <application android:process="android.process.media"
                  android:label="@string/app_label">
+
         <provider android:name=".DownloadProvider"
                   android:authorities="downloads" android:exported="true">
           <!-- Anyone can access /my_downloads, the provider internally restricts access by UID for
@@ -72,6 +73,9 @@
           <!-- Apps with access to /all_downloads/... can grant permissions, allowing them to share
                downloaded files with other viewers -->
           <grant-uri-permission android:pathPrefix="/all_downloads/"/>
+          <!-- Apps with access to /my_downloads/... can grant permissions, allowing them to share
+               downloaded files with other viewers -->
+          <grant-uri-permission android:pathPrefix="/my_downloads/"/>
         </provider>
         <service android:name=".DownloadService"
                 android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 7a1dd6d..7fdb7b7 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -28,7 +28,7 @@
     <string name="permdesc_seeAllExternal" msgid="1672759909065511233">"Umožňuje aplikaci zobrazit všechny soubory stažené na kartu SD bez ohledu na aplikaci, pomocí které byly staženy."</string>
     <string name="permlab_downloadCacheNonPurgeable" msgid="3069534308882047412">"Rezervovat místo v mezipaměti stahování"</string>
     <string name="permdesc_downloadCacheNonPurgeable" msgid="2408760720334570420">"Umožňuje aplikaci stahovat soubory do mezipaměti stahování, kterou nelze automaticky vymazat, pokud správce stahování potřebuje více místa."</string>
-    <string name="permlab_downloadWithoutNotification" msgid="8837971946078327262">"stahovat soubory bez upozornění"</string>
+    <string name="permlab_downloadWithoutNotification" msgid="8837971946078327262">"stahování souborů bez upozornění"</string>
     <string name="permdesc_downloadWithoutNotification" msgid="8483135034298639727">"Umožňuje aplikaci stahovat soubory prostřednictvím správce stahování bez oznámení uživateli."</string>
     <string name="permlab_accessAllDownloads" msgid="2436240495424393717">"Přístup ke všem systémovým stahováním"</string>
     <string name="permdesc_accessAllDownloads" msgid="1871832254578267128">"Umožňuje aplikaci zobrazit a upravovat všechna stahování zahájená libovolnou aplikací v systému."</string>
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 8d80618..08ef466 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -48,9 +48,6 @@
     /** The column that is used to remember whether the media scanner was invoked */
     public static final String MEDIA_SCANNED = "scanned";
 
-    /** The column that is used to count retries */
-    public static final String FAILED_CONNECTIONS = "numfailed";
-
     /** The intent that gets sent when the service must wake up for a retry */
     public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
 
diff --git a/src/com/android/providers/downloads/DownloadDrmHelper.java b/src/com/android/providers/downloads/DownloadDrmHelper.java
index 10cb792..d135824 100644
--- a/src/com/android/providers/downloads/DownloadDrmHelper.java
+++ b/src/com/android/providers/downloads/DownloadDrmHelper.java
@@ -19,7 +19,8 @@
 
 import android.content.Context;
 import android.drm.DrmManagerClient;
-import android.util.Log;
+
+import java.io.File;
 
 public class DownloadDrmHelper {
 
@@ -32,31 +33,6 @@
     public static final String EXTENSION_INTERNAL_FWDL = ".fl";
 
     /**
-     * Checks if the Media Type is a DRM Media Type
-     *
-     * @param drmManagerClient A DrmManagerClient
-     * @param mimetype Media Type to check
-     * @return True if the Media Type is DRM else false
-     */
-    public static boolean isDrmMimeType(Context context, String mimetype) {
-        boolean result = false;
-        if (context != null) {
-            try {
-                DrmManagerClient drmClient = new DrmManagerClient(context);
-                if (drmClient != null && mimetype != null && mimetype.length() > 0) {
-                    result = drmClient.canHandle("", mimetype);
-                }
-            } catch (IllegalArgumentException e) {
-                Log.w(Constants.TAG,
-                        "DrmManagerClient instance could not be created, context is Illegal.");
-            } catch (IllegalStateException e) {
-                Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly.");
-            }
-        }
-        return result;
-    }
-
-    /**
      * Checks if the Media Type needs to be DRM converted
      *
      * @param mimetype Media type of the content
@@ -83,28 +59,20 @@
     }
 
     /**
-     * Gets the original mime type of DRM protected content.
-     *
-     * @param context The context
-     * @param path Path to the file
-     * @param containingMime The current mime type of of the file i.e. the
-     *            containing mime type
-     * @return The original mime type of the file if DRM protected else the
-     *         currentMime
+     * Return the original MIME type of the given file, using the DRM framework
+     * if the file is protected content.
      */
-    public static String getOriginalMimeType(Context context, String path, String containingMime) {
-        String result = containingMime;
-        DrmManagerClient drmClient = new DrmManagerClient(context);
+    public static String getOriginalMimeType(Context context, File file, String currentMime) {
+        final DrmManagerClient client = new DrmManagerClient(context);
         try {
-            if (drmClient.canHandle(path, null)) {
-                result = drmClient.getOriginalMimeType(path);
+            final String rawFile = file.toString();
+            if (client.canHandle(rawFile, null)) {
+                return client.getOriginalMimeType(rawFile);
+            } else {
+                return currentMime;
             }
-        } catch (IllegalArgumentException ex) {
-            Log.w(Constants.TAG,
-                    "Can't get original mime type since path is null or empty string.");
-        } catch (IllegalStateException ex) {
-            Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly.");
+        } finally {
+            client.release();
         }
-        return result;
     }
 }
diff --git a/src/com/android/providers/downloads/DownloadHandler.java b/src/com/android/providers/downloads/DownloadHandler.java
deleted file mode 100644
index 2f02864..0000000
--- a/src/com/android/providers/downloads/DownloadHandler.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.downloads;
-
-import android.content.res.Resources;
-import android.util.Log;
-import android.util.LongSparseArray;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-
-public class DownloadHandler {
-    private static final String TAG = "DownloadHandler";
-
-    @GuardedBy("this")
-    private final LinkedHashMap<Long, DownloadInfo> mDownloadsQueue =
-            new LinkedHashMap<Long, DownloadInfo>();
-    @GuardedBy("this")
-    private final HashMap<Long, DownloadInfo> mDownloadsInProgress =
-            new HashMap<Long, DownloadInfo>();
-    @GuardedBy("this")
-    private final LongSparseArray<Long> mCurrentSpeed = new LongSparseArray<Long>();
-
-    private final int mMaxConcurrentDownloadsAllowed = Resources.getSystem().getInteger(
-            com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
-
-    private static final DownloadHandler sDownloadHandler = new DownloadHandler();
-
-    public static DownloadHandler getInstance() {
-        return sDownloadHandler;
-    }
-
-    public synchronized void enqueueDownload(DownloadInfo info) {
-        if (!mDownloadsQueue.containsKey(info.mId)) {
-            if (Constants.LOGV) {
-                Log.i(TAG, "enqueued download. id: " + info.mId + ", uri: " + info.mUri);
-            }
-            mDownloadsQueue.put(info.mId, info);
-            startDownloadThreadLocked();
-        }
-    }
-
-    private void startDownloadThreadLocked() {
-        Iterator<Long> keys = mDownloadsQueue.keySet().iterator();
-        ArrayList<Long> ids = new ArrayList<Long>();
-        while (mDownloadsInProgress.size() < mMaxConcurrentDownloadsAllowed && keys.hasNext()) {
-            Long id = keys.next();
-            DownloadInfo info = mDownloadsQueue.get(id);
-            info.startDownloadThread();
-            ids.add(id);
-            mDownloadsInProgress.put(id, mDownloadsQueue.get(id));
-            if (Constants.LOGV) {
-                Log.i(TAG, "started download for : " + id);
-            }
-        }
-        for (Long id : ids) {
-            mDownloadsQueue.remove(id);
-        }
-    }
-
-    public synchronized boolean hasDownloadInQueue(long id) {
-        return mDownloadsQueue.containsKey(id) || mDownloadsInProgress.containsKey(id);
-    }
-
-    public synchronized void dequeueDownload(long id) {
-        mDownloadsInProgress.remove(id);
-        mCurrentSpeed.remove(id);
-        startDownloadThreadLocked();
-        if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) {
-            notifyAll();
-        }
-    }
-
-    public synchronized void setCurrentSpeed(long id, long speed) {
-        mCurrentSpeed.put(id, speed);
-    }
-
-    public synchronized long getCurrentSpeed(long id) {
-        return mCurrentSpeed.get(id, -1L);
-    }
-
-    // right now this is only used by tests. but there is no reason why it can't be used
-    // by any module using DownloadManager (TODO add API to DownloadManager.java)
-    public synchronized void waitUntilDownloadsTerminate() throws InterruptedException {
-        if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) {
-            if (Constants.LOGVV) {
-                Log.i(TAG, "nothing to wait on");
-            }
-            return;
-        }
-        if (Constants.LOGVV) {
-            for (DownloadInfo info : mDownloadsInProgress.values()) {
-                Log.i(TAG, "** progress: " + info.mId + ", " + info.mUri);
-            }
-            for (DownloadInfo info : mDownloadsQueue.values()) {
-                Log.i(TAG, "** in Q: " + info.mId + ", " + info.mUri);
-            }
-        }
-        if (Constants.LOGVV) {
-            Log.i(TAG, "waiting for 5 sec");
-        }
-        // wait upto 5 sec
-        wait(5 * 1000);
-    }
-}
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 5172b69..7a912d5 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -31,20 +31,26 @@
 import android.provider.Downloads;
 import android.provider.Downloads.Impl;
 import android.text.TextUtils;
-import android.util.Log;
 import android.util.Pair;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
 
 /**
  * Stores information about an individual download.
  */
 public class DownloadInfo {
+    // TODO: move towards these in-memory objects being sources of truth, and
+    // periodically pushing to provider.
+
     public static class Reader {
         private ContentResolver mResolver;
         private Cursor mCursor;
@@ -54,8 +60,10 @@
             mCursor = cursor;
         }
 
-        public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade) {
-            DownloadInfo info = new DownloadInfo(context, systemFacade);
+        public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade,
+                StorageManager storageManager, DownloadNotifier notifier) {
+            final DownloadInfo info = new DownloadInfo(
+                    context, systemFacade, storageManager, notifier);
             updateFromDatabase(info);
             readRequestHeaders(info);
             return info;
@@ -71,7 +79,7 @@
             info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION);
             info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY);
             info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS);
-            info.mNumFailed = getInt(Constants.FAILED_CONNECTIONS);
+            info.mNumFailed = getInt(Downloads.Impl.COLUMN_FAILED_CONNECTIONS);
             int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT);
             info.mRetryAfter = retryRedirect & 0xfffffff;
             info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION);
@@ -146,44 +154,49 @@
         }
     }
 
-    // the following NETWORK_* constants are used to indicates specfic reasons for disallowing a
-    // download from using a network, since specific causes can require special handling
-
     /**
-     * The network is usable for the given download.
+     * Constants used to indicate network state for a specific download, after
+     * applying any requested constraints.
      */
-    public static final int NETWORK_OK = 1;
+    public enum NetworkState {
+        /**
+         * The network is usable for the given download.
+         */
+        OK,
 
-    /**
-     * There is no network connectivity.
-     */
-    public static final int NETWORK_NO_CONNECTION = 2;
+        /**
+         * There is no network connectivity.
+         */
+        NO_CONNECTION,
 
-    /**
-     * The download exceeds the maximum size for this network.
-     */
-    public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
+        /**
+         * The download exceeds the maximum size for this network.
+         */
+        UNUSABLE_DUE_TO_SIZE,
 
-    /**
-     * The download exceeds the recommended maximum size for this network, the user must confirm for
-     * this download to proceed without WiFi.
-     */
-    public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
+        /**
+         * The download exceeds the recommended maximum size for this network,
+         * the user must confirm for this download to proceed without WiFi.
+         */
+        RECOMMENDED_UNUSABLE_DUE_TO_SIZE,
 
-    /**
-     * The current connection is roaming, and the download can't proceed over a roaming connection.
-     */
-    public static final int NETWORK_CANNOT_USE_ROAMING = 5;
+        /**
+         * The current connection is roaming, and the download can't proceed
+         * over a roaming connection.
+         */
+        CANNOT_USE_ROAMING,
 
-    /**
-     * The app requesting the download specific that it can't use the current network connection.
-     */
-    public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6;
+        /**
+         * The app requesting the download specific that it can't use the
+         * current network connection.
+         */
+        TYPE_DISALLOWED_BY_REQUESTOR,
 
-    /**
-     * Current network is blocked for requesting application.
-     */
-    public static final int NETWORK_BLOCKED = 7;
+        /**
+         * Current network is blocked for requesting application.
+         */
+        BLOCKED;
+    }
 
     /**
      * For intents used to notify the user that a download exceeds a size threshold, if this extra
@@ -191,7 +204,6 @@
      */
     public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
 
-
     public long mId;
     public String mUri;
     public boolean mNoIntegrity;
@@ -229,12 +241,28 @@
     public int mFuzz;
 
     private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
-    private SystemFacade mSystemFacade;
-    private Context mContext;
 
-    private DownloadInfo(Context context, SystemFacade systemFacade) {
+    /**
+     * Result of last {@link DownloadThread} started by
+     * {@link #startDownloadIfReady(ExecutorService)}.
+     */
+    @GuardedBy("this")
+    private Future<?> mSubmittedTask;
+
+    @GuardedBy("this")
+    private DownloadThread mTask;
+
+    private final Context mContext;
+    private final SystemFacade mSystemFacade;
+    private final StorageManager mStorageManager;
+    private final DownloadNotifier mNotifier;
+
+    private DownloadInfo(Context context, SystemFacade systemFacade, StorageManager storageManager,
+            DownloadNotifier notifier) {
         mContext = context;
         mSystemFacade = systemFacade;
+        mStorageManager = storageManager;
+        mNotifier = notifier;
         mFuzz = Helpers.sRandom.nextInt(1001);
     }
 
@@ -285,14 +313,9 @@
     }
 
     /**
-     * Returns whether this download (which the download manager hasn't seen yet)
-     * should be started.
+     * Returns whether this download should be enqueued.
      */
-    private boolean isReadyToStart(long now) {
-        if (DownloadHandler.getInstance().hasDownloadInQueue(mId)) {
-            // already running
-            return false;
-        }
+    private boolean isReadyToDownload() {
         if (mControl == Downloads.Impl.CONTROL_PAUSED) {
             // the download is paused, so it's not going to start
             return false;
@@ -306,10 +329,11 @@
 
             case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
             case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
-                return checkCanUseNetwork() == NETWORK_OK;
+                return checkCanUseNetwork() == NetworkState.OK;
 
             case Downloads.Impl.STATUS_WAITING_TO_RETRY:
                 // download was waiting for a delayed restart
+                final long now = mSystemFacade.currentTimeMillis();
                 return restartTime(now) <= now;
             case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
                 // is the media mounted?
@@ -337,21 +361,20 @@
 
     /**
      * Returns whether this download is allowed to use the network.
-     * @return one of the NETWORK_* constants
      */
-    public int checkCanUseNetwork() {
+    public NetworkState checkCanUseNetwork() {
         final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid);
         if (info == null || !info.isConnected()) {
-            return NETWORK_NO_CONNECTION;
+            return NetworkState.NO_CONNECTION;
         }
         if (DetailedState.BLOCKED.equals(info.getDetailedState())) {
-            return NETWORK_BLOCKED;
+            return NetworkState.BLOCKED;
         }
-        if (!isRoamingAllowed() && mSystemFacade.isNetworkRoaming()) {
-            return NETWORK_CANNOT_USE_ROAMING;
+        if (mSystemFacade.isNetworkRoaming() && !isRoamingAllowed()) {
+            return NetworkState.CANNOT_USE_ROAMING;
         }
-        if (!mAllowMetered && mSystemFacade.isActiveNetworkMetered()) {
-            return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
+        if (mSystemFacade.isActiveNetworkMetered() && !mAllowMetered) {
+            return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR;
         }
         return checkIsNetworkTypeAllowed(info.getType());
     }
@@ -365,45 +388,16 @@
     }
 
     /**
-     * @return a non-localized string appropriate for logging corresponding to one of the
-     * NETWORK_* constants.
-     */
-    public String getLogMessageForNetworkError(int networkError) {
-        switch (networkError) {
-            case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
-                return "download size exceeds recommended limit for mobile network";
-
-            case NETWORK_UNUSABLE_DUE_TO_SIZE:
-                return "download size exceeds limit for mobile network";
-
-            case NETWORK_NO_CONNECTION:
-                return "no network connection available";
-
-            case NETWORK_CANNOT_USE_ROAMING:
-                return "download cannot use the current network connection because it is roaming";
-
-            case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
-                return "download was requested to not use the current network type";
-
-            case NETWORK_BLOCKED:
-                return "network is blocked for requesting application";
-
-            default:
-                return "unknown error with network connectivity";
-        }
-    }
-
-    /**
      * Check if this download can proceed over the given network type.
      * @param networkType a constant from ConnectivityManager.TYPE_*.
      * @return one of the NETWORK_* constants
      */
-    private int checkIsNetworkTypeAllowed(int networkType) {
+    private NetworkState checkIsNetworkTypeAllowed(int networkType) {
         if (mIsPublicApi) {
             final int flag = translateNetworkTypeToApiFlag(networkType);
             final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0;
             if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) {
-                return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
+                return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR;
             }
         }
         return checkSizeAllowedForNetwork(networkType);
@@ -433,42 +427,68 @@
      * Check if the download's size prohibits it from running over the current network.
      * @return one of the NETWORK_* constants
      */
-    private int checkSizeAllowedForNetwork(int networkType) {
+    private NetworkState checkSizeAllowedForNetwork(int networkType) {
         if (mTotalBytes <= 0) {
-            return NETWORK_OK; // we don't know the size yet
+            return NetworkState.OK; // we don't know the size yet
         }
         if (networkType == ConnectivityManager.TYPE_WIFI) {
-            return NETWORK_OK; // anything goes over wifi
+            return NetworkState.OK; // anything goes over wifi
         }
         Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile();
         if (maxBytesOverMobile != null && mTotalBytes > maxBytesOverMobile) {
-            return NETWORK_UNUSABLE_DUE_TO_SIZE;
+            return NetworkState.UNUSABLE_DUE_TO_SIZE;
         }
         if (mBypassRecommendedSizeLimit == 0) {
             Long recommendedMaxBytesOverMobile = mSystemFacade.getRecommendedMaxBytesOverMobile();
             if (recommendedMaxBytesOverMobile != null
                     && mTotalBytes > recommendedMaxBytesOverMobile) {
-                return NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
+                return NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
             }
         }
-        return NETWORK_OK;
+        return NetworkState.OK;
     }
 
-    void startIfReady(long now, StorageManager storageManager) {
-        if (!isReadyToStart(now)) {
-            return;
-        }
+    /**
+     * If download is ready to start, and isn't already pending or executing,
+     * create a {@link DownloadThread} and enqueue it into given
+     * {@link Executor}.
+     *
+     * @return If actively downloading.
+     */
+    public boolean startDownloadIfReady(ExecutorService executor) {
+        synchronized (this) {
+            final boolean isReady = isReadyToDownload();
+            final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone();
+            if (isReady && !isActive) {
+                if (mStatus != Impl.STATUS_RUNNING) {
+                    mStatus = Impl.STATUS_RUNNING;
+                    ContentValues values = new ContentValues();
+                    values.put(Impl.COLUMN_STATUS, mStatus);
+                    mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
+                }
 
-        if (Constants.LOGV) {
-            Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
+                mTask = new DownloadThread(
+                        mContext, mSystemFacade, this, mStorageManager, mNotifier);
+                mSubmittedTask = executor.submit(mTask);
+            }
+            return isReady;
         }
-        if (mStatus != Impl.STATUS_RUNNING) {
-            mStatus = Impl.STATUS_RUNNING;
-            ContentValues values = new ContentValues();
-            values.put(Impl.COLUMN_STATUS, mStatus);
-            mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
+    }
+
+    /**
+     * If download is ready to be scanned, enqueue it into the given
+     * {@link DownloadScanner}.
+     *
+     * @return If actively scanning.
+     */
+    public boolean startScanIfReady(DownloadScanner scanner) {
+        synchronized (this) {
+            final boolean isReady = shouldScanFile();
+            if (isReady) {
+                scanner.requestScan(this);
+            }
+            return isReady;
         }
-        DownloadHandler.getInstance().enqueueDownload(this);
     }
 
     public boolean isOnCache() {
@@ -529,15 +549,15 @@
     }
 
     /**
-     * Returns the amount of time (as measured from the "now" parameter)
-     * at which a download will be active.
-     * 0 = immediately - service should stick around to handle this download.
-     * -1 = never - service can go away without ever waking up.
-     * positive value - service must wake up in the future, as specified in ms from "now"
+     * Return time when this download will be ready for its next action, in
+     * milliseconds after given time.
+     *
+     * @return If {@code 0}, download is ready to proceed immediately. If
+     *         {@link Long#MAX_VALUE}, then download has no future actions.
      */
-    long nextAction(long now) {
+    public long nextActionMillis(long now) {
         if (Downloads.Impl.isStatusCompleted(mStatus)) {
-            return -1;
+            return Long.MAX_VALUE;
         }
         if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) {
             return 0;
@@ -552,7 +572,7 @@
     /**
      * Returns whether a file should be scanned
      */
-    boolean shouldScanFile() {
+    public boolean shouldScanFile() {
         return (mMediaScanned == 0)
                 && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
                         mDestination == Downloads.Impl.DESTINATION_FILE_URI ||
@@ -570,12 +590,6 @@
         mContext.startActivity(intent);
     }
 
-    void startDownloadThread() {
-        DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this,
-                StorageManager.getInstance(mContext));
-        mSystemFacade.startThread(downloader);
-    }
-
     /**
      * Query and return status of requested download.
      */
diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java
index f387865..ac52eba 100644
--- a/src/com/android/providers/downloads/DownloadNotifier.java
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -19,6 +19,8 @@
 import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE;
 import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
 import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
+import static android.provider.Downloads.Impl.STATUS_RUNNING;
+import static com.android.providers.downloads.Constants.TAG;
 
 import android.app.DownloadManager;
 import android.app.Notification;
@@ -29,9 +31,12 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.net.Uri;
+import android.os.SystemClock;
 import android.provider.Downloads;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.LongSparseLongArray;
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Maps;
@@ -66,6 +71,20 @@
     @GuardedBy("mActiveNotifs")
     private final HashMap<String, Long> mActiveNotifs = Maps.newHashMap();
 
+    /**
+     * Current speed of active downloads, mapped from {@link DownloadInfo#mId}
+     * to speed in bytes per second.
+     */
+    @GuardedBy("mDownloadSpeed")
+    private final LongSparseLongArray mDownloadSpeed = new LongSparseLongArray();
+
+    /**
+     * Last time speed was reproted, mapped from {@link DownloadInfo#mId} to
+     * {@link SystemClock#elapsedRealtime()}.
+     */
+    @GuardedBy("mDownloadSpeed")
+    private final LongSparseLongArray mDownloadTouch = new LongSparseLongArray();
+
     public DownloadNotifier(Context context) {
         mContext = context;
         mNotifManager = (NotificationManager) context.getSystemService(
@@ -77,6 +96,22 @@
     }
 
     /**
+     * Notify the current speed of an active download, used for calculating
+     * estimated remaining time.
+     */
+    public void notifyDownloadSpeed(long id, long bytesPerSecond) {
+        synchronized (mDownloadSpeed) {
+            if (bytesPerSecond != 0) {
+                mDownloadSpeed.put(id, bytesPerSecond);
+                mDownloadTouch.put(id, SystemClock.elapsedRealtime());
+            } else {
+                mDownloadSpeed.delete(id);
+                mDownloadTouch.delete(id);
+            }
+        }
+    }
+
+    /**
      * Update {@link NotificationManager} to reflect the given set of
      * {@link DownloadInfo}, adding, collapsing, and removing as needed.
      */
@@ -140,6 +175,7 @@
                 final DownloadInfo info = cluster.iterator().next();
                 final Uri uri = ContentUris.withAppendedId(
                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, info.mId);
+                builder.setAutoCancel(true);
 
                 final String action;
                 if (Downloads.Impl.isStatusError(info.mStatus)) {
@@ -167,16 +203,16 @@
             String remainingText = null;
             String percentText = null;
             if (type == TYPE_ACTIVE) {
-                final DownloadHandler handler = DownloadHandler.getInstance();
-
                 long current = 0;
                 long total = 0;
                 long speed = 0;
-                for (DownloadInfo info : cluster) {
-                    if (info.mTotalBytes != -1) {
-                        current += info.mCurrentBytes;
-                        total += info.mTotalBytes;
-                        speed += handler.getCurrentSpeed(info.mId);
+                synchronized (mDownloadSpeed) {
+                    for (DownloadInfo info : cluster) {
+                        if (info.mTotalBytes != -1) {
+                            current += info.mCurrentBytes;
+                            total += info.mTotalBytes;
+                            speed += mDownloadSpeed.get(info.mId);
+                        }
                     }
                 }
 
@@ -283,6 +319,17 @@
         return ids;
     }
 
+    public void dumpSpeeds() {
+        synchronized (mDownloadSpeed) {
+            for (int i = 0; i < mDownloadSpeed.size(); i++) {
+                final long id = mDownloadSpeed.keyAt(i);
+                final long delta = SystemClock.elapsedRealtime() - mDownloadTouch.get(id);
+                Log.d(TAG, "Download " + id + " speed " + mDownloadSpeed.valueAt(i) + "bps, "
+                        + delta + "ms ago");
+            }
+        }
+    }
+
     /**
      * Build tag used for collapsing several {@link DownloadInfo} into a single
      * {@link Notification}.
@@ -309,7 +356,7 @@
     }
 
     private static boolean isActiveAndVisible(DownloadInfo download) {
-        return Downloads.Impl.isStatusInformational(download.mStatus) &&
+        return download.mStatus == STATUS_RUNNING &&
                 (download.mVisibility == VISIBILITY_VISIBLE
                 || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
     }
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index c554e41..e0b5842 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -37,17 +37,23 @@
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.SELinux;
+import android.provider.BaseColumns;
 import android.provider.Downloads;
 import android.provider.OpenableColumns;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.Log;
 
+import com.android.internal.util.IndentingPrintWriter;
 import com.google.android.collect.Maps;
 import com.google.common.annotations.VisibleForTesting;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -384,7 +390,7 @@
                         Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
                         Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
                         Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
-                        Constants.FAILED_CONNECTIONS + " INTEGER, " +
+                        Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " +
                         Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
                         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
                         Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
@@ -436,8 +442,7 @@
             appInfo = getContext().getPackageManager().
                     getApplicationInfo("com.android.defcontainer", 0);
         } catch (NameNotFoundException e) {
-            // TODO Auto-generated catch block
-            e.printStackTrace();
+            Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
         }
         if (appInfo != null) {
             mDefContainerUid = appInfo.uid;
@@ -446,7 +451,12 @@
         // saves us by getting some initialization code in DownloadService out of the way.
         Context context = getContext();
         context.startService(new Intent(context, DownloadService.class));
-        mDownloadsDataDir = StorageManager.getInstance(getContext()).getDownloadDataDirectory();
+        mDownloadsDataDir = StorageManager.getDownloadDataDirectory(getContext());
+        try {
+            SELinux.restorecon(mDownloadsDataDir.getCanonicalPath());
+        } catch (IOException e) {
+            Log.wtf(Constants.TAG, "Could not get canonical path for download directory", e);
+        }
         return true;
     }
 
@@ -1199,6 +1209,41 @@
         return ret;
     }
 
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 120);
+
+        pw.println("Downloads updated in last hour:");
+        pw.increaseIndent();
+
+        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
+        final Cursor cursor = db.query(DB_TABLE, null,
+                Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
+                Downloads.Impl._ID + " ASC");
+        try {
+            final String[] cols = cursor.getColumnNames();
+            final int idCol = cursor.getColumnIndex(BaseColumns._ID);
+            while (cursor.moveToNext()) {
+                pw.println("Download #" + cursor.getInt(idCol) + ":");
+                pw.increaseIndent();
+                for (int i = 0; i < cols.length; i++) {
+                    // Omit sensitive data when dumping
+                    if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
+                        continue;
+                    }
+                    pw.printPair(cols[i], cursor.getString(i));
+                }
+                pw.println();
+                pw.decreaseIndent();
+            }
+        } finally {
+            cursor.close();
+        }
+
+        pw.decreaseIndent();
+    }
+
     private void logVerboseOpenFileInfo(Uri uri, String mode) {
         Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
                 + ", uid: " + Binder.getCallingUid());
@@ -1229,7 +1274,7 @@
                     Log.v(Constants.TAG, "file exists in openFile");
                 }
             }
-           cursor.close();
+            cursor.close();
         }
     }
 
diff --git a/src/com/android/providers/downloads/DownloadScanner.java b/src/com/android/providers/downloads/DownloadScanner.java
new file mode 100644
index 0000000..ca79506
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadScanner.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static com.android.providers.downloads.Constants.LOGV;
+import static com.android.providers.downloads.Constants.TAG;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.provider.Downloads;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+
+/**
+ * Manages asynchronous scanning of completed downloads.
+ */
+public class DownloadScanner implements MediaScannerConnectionClient {
+    private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS;
+
+    private final Context mContext;
+    private final MediaScannerConnection mConnection;
+
+    private static class ScanRequest {
+        public final long id;
+        public final String path;
+        public final String mimeType;
+        public final long requestRealtime;
+
+        public ScanRequest(long id, String path, String mimeType) {
+            this.id = id;
+            this.path = path;
+            this.mimeType = mimeType;
+            this.requestRealtime = SystemClock.elapsedRealtime();
+        }
+
+        public void exec(MediaScannerConnection conn) {
+            conn.scanFile(path, mimeType);
+        }
+    }
+
+    @GuardedBy("mConnection")
+    private HashMap<String, ScanRequest> mPending = Maps.newHashMap();
+
+    public DownloadScanner(Context context) {
+        mContext = context;
+        mConnection = new MediaScannerConnection(context, this);
+    }
+
+    /**
+     * Check if requested scans are still pending. Scans may timeout after an
+     * internal duration.
+     */
+    public boolean hasPendingScans() {
+        synchronized (mConnection) {
+            if (mPending.isEmpty()) {
+                return false;
+            } else {
+                // Check if pending scans have timed out
+                final long nowRealtime = SystemClock.elapsedRealtime();
+                for (ScanRequest req : mPending.values()) {
+                    if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Request that given {@link DownloadInfo} be scanned at some point in
+     * future. Enqueues the request to be scanned asynchronously.
+     *
+     * @see #hasPendingScans()
+     */
+    public void requestScan(DownloadInfo info) {
+        if (LOGV) Log.v(TAG, "requestScan() for " + info.mFileName);
+        synchronized (mConnection) {
+            final ScanRequest req = new ScanRequest(info.mId, info.mFileName, info.mMimeType);
+            mPending.put(req.path, req);
+
+            if (mConnection.isConnected()) {
+                req.exec(mConnection);
+            } else {
+                mConnection.connect();
+            }
+        }
+    }
+
+    public void shutdown() {
+        mConnection.disconnect();
+    }
+
+    @Override
+    public void onMediaScannerConnected() {
+        synchronized (mConnection) {
+            for (ScanRequest req : mPending.values()) {
+                req.exec(mConnection);
+            }
+        }
+    }
+
+    @Override
+    public void onScanCompleted(String path, Uri uri) {
+        final ScanRequest req;
+        synchronized (mConnection) {
+            req = mPending.remove(path);
+        }
+        if (req == null) {
+            Log.w(TAG, "Missing request for path " + path);
+            return;
+        }
+
+        // Update scanned column, which will kick off a database update pass,
+        // eventually deciding if overall service is ready for teardown.
+        final ContentValues values = new ContentValues();
+        values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
+        if (uri != null) {
+            values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
+        }
+
+        final ContentResolver resolver = mContext.getContentResolver();
+        final Uri downloadUri = ContentUris.withAppendedId(
+                Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
+        final int rows = resolver.update(downloadUri, values, null, null);
+        if (rows == 0) {
+            // Local row disappeared during scan; download was probably deleted
+            // so clean up now-orphaned media entry.
+            resolver.delete(uri, null, null);
+        }
+    }
+}
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index b97346b..7d746cc 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -16,25 +16,25 @@
 
 package com.android.providers.downloads;
 
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static com.android.providers.downloads.Constants.TAG;
 
 import android.app.AlarmManager;
+import android.app.DownloadManager;
 import android.app.PendingIntent;
 import android.app.Service;
-import android.content.ComponentName;
-import android.content.ContentValues;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.ServiceConnection;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.database.Cursor;
-import android.media.IMediaScannerListener;
-import android.media.IMediaScannerService;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Message;
 import android.os.Process;
-import android.os.RemoteException;
 import android.provider.Downloads;
 import android.text.TextUtils;
 import android.util.Log;
@@ -44,22 +44,41 @@
 import com.google.android.collect.Maps;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 
 /**
- * Performs the background downloads requested by applications that use the Downloads provider.
+ * Performs background downloads as requested by applications that use
+ * {@link DownloadManager}. Multiple start commands can be issued at this
+ * service, and it will continue running until no downloads are being actively
+ * processed. It may schedule alarms to resume downloads in future.
+ * <p>
+ * Any database updates important enough to initiate tasks should always be
+ * delivered through {@link Context#startService(Intent)}.
  */
 public class DownloadService extends Service {
-    /** amount of time to wait to connect to MediaScannerService before timing out */
-    private static final long WAIT_TIMEOUT = 10 * 1000;
+    // TODO: migrate WakeLock from individual DownloadThreads out into
+    // DownloadReceiver to protect our entire workflow.
+
+    private static final boolean DEBUG_LIFECYCLE = false;
+
+    @VisibleForTesting
+    SystemFacade mSystemFacade;
+
+    private AlarmManager mAlarmManager;
+    private StorageManager mStorageManager;
 
     /** Observer to get notified when the content observer's data changes */
     private DownloadManagerContentObserver mObserver;
@@ -74,118 +93,41 @@
      * content provider changes or disappears.
      */
     @GuardedBy("mDownloads")
-    private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
+    private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
 
-    /**
-     * The thread that updates the internal download list from the content
-     * provider.
-     */
-    @VisibleForTesting
-    UpdateThread mUpdateThread;
+    private final ExecutorService mExecutor = buildDownloadExecutor();
 
-    /**
-     * Whether the internal download list should be updated from the content
-     * provider.
-     */
-    private boolean mPendingUpdate;
+    private static ExecutorService buildDownloadExecutor() {
+        final int maxConcurrent = Resources.getSystem().getInteger(
+                com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
 
-    /**
-     * The ServiceConnection object that tells us when we're connected to and disconnected from
-     * the Media Scanner
-     */
-    private MediaScannerConnection mMediaScannerConnection;
+        // Create a bounded thread pool for executing downloads; it creates
+        // threads as needed (up to maximum) and reclaims them when finished.
+        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
+                maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>());
+        executor.allowCoreThreadTimeOut(true);
+        return executor;
+    }
 
-    private boolean mMediaScannerConnecting;
+    private DownloadScanner mScanner;
 
-    /**
-     * The IPC interface to the Media Scanner
-     */
-    private IMediaScannerService mMediaScannerService;
+    private HandlerThread mUpdateThread;
+    private Handler mUpdateHandler;
 
-    @VisibleForTesting
-    SystemFacade mSystemFacade;
-
-    private StorageManager mStorageManager;
+    private volatile int mLastStartId;
 
     /**
      * Receives notifications when the data in the content provider changes
      */
     private class DownloadManagerContentObserver extends ContentObserver {
-
         public DownloadManagerContentObserver() {
             super(new Handler());
         }
 
-        /**
-         * Receives notification when the data in the observed content
-         * provider changes.
-         */
         @Override
         public void onChange(final boolean selfChange) {
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "Service ContentObserver received notification");
-            }
-            updateFromProvider();
-        }
-
-    }
-
-    /**
-     * Gets called back when the connection to the media
-     * scanner is established or lost.
-     */
-    public class MediaScannerConnection implements ServiceConnection {
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "Connected to Media Scanner");
-            }
-            synchronized (DownloadService.this) {
-                try {
-                    mMediaScannerConnecting = false;
-                    mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
-                    if (mMediaScannerService != null) {
-                        updateFromProvider();
-                    }
-                } finally {
-                    // notify anyone waiting on successful connection to MediaService
-                    DownloadService.this.notifyAll();
-                }
-            }
-        }
-
-        public void disconnectMediaScanner() {
-            synchronized (DownloadService.this) {
-                mMediaScannerConnecting = false;
-                if (mMediaScannerService != null) {
-                    mMediaScannerService = null;
-                    if (Constants.LOGVV) {
-                        Log.v(Constants.TAG, "Disconnecting from Media Scanner");
-                    }
-                    try {
-                        unbindService(this);
-                    } catch (IllegalArgumentException ex) {
-                        Log.w(Constants.TAG, "unbindService failed: " + ex);
-                    } finally {
-                        // notify anyone waiting on unsuccessful connection to MediaService
-                        DownloadService.this.notifyAll();
-                    }
-                }
-            }
-        }
-
-        public void onServiceDisconnected(ComponentName className) {
-            try {
-                if (Constants.LOGVV) {
-                    Log.v(Constants.TAG, "Disconnected from Media Scanner");
-                }
-            } finally {
-                synchronized (DownloadService.this) {
-                    mMediaScannerService = null;
-                    mMediaScannerConnecting = false;
-                    // notify anyone waiting on disconnect from MediaService
-                    DownloadService.this.notifyAll();
-                }
-            }
+            enqueueUpdate();
         }
     }
 
@@ -214,19 +156,21 @@
             mSystemFacade = new RealSystemFacade(this);
         }
 
-        mObserver = new DownloadManagerContentObserver();
-        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                true, mObserver);
+        mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+        mStorageManager = new StorageManager(this);
 
-        mMediaScannerService = null;
-        mMediaScannerConnecting = false;
-        mMediaScannerConnection = new MediaScannerConnection();
+        mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
+        mUpdateThread.start();
+        mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
+
+        mScanner = new DownloadScanner(this);
 
         mNotifier = new DownloadNotifier(this);
         mNotifier.cancelAll();
 
-        mStorageManager = StorageManager.getInstance(getApplicationContext());
-        updateFromProvider();
+        mObserver = new DownloadManagerContentObserver();
+        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+                true, mObserver);
     }
 
     @Override
@@ -235,16 +179,16 @@
         if (Constants.LOGVV) {
             Log.v(Constants.TAG, "Service onStart");
         }
-        updateFromProvider();
+        mLastStartId = startId;
+        enqueueUpdate();
         return returnValue;
     }
 
-    /**
-     * Cleans up when the service is destroyed
-     */
     @Override
     public void onDestroy() {
         getContentResolver().unregisterContentObserver(mObserver);
+        mScanner.shutdown();
+        mUpdateThread.quit();
         if (Constants.LOGVV) {
             Log.v(Constants.TAG, "Service onDestroy");
         }
@@ -252,182 +196,179 @@
     }
 
     /**
-     * Parses data from the content provider into private array
+     * Enqueue an {@link #updateLocked()} pass to occur in future.
      */
-    private void updateFromProvider() {
-        synchronized (this) {
-            mPendingUpdate = true;
-            if (mUpdateThread == null) {
-                mUpdateThread = new UpdateThread();
-                mSystemFacade.startThread(mUpdateThread);
-            }
-        }
+    private void enqueueUpdate() {
+        mUpdateHandler.removeMessages(MSG_UPDATE);
+        mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
     }
 
-    private class UpdateThread extends Thread {
-        public UpdateThread() {
-            super("Download Service");
-        }
+    /**
+     * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to
+     * catch any finished operations that didn't trigger an update pass.
+     */
+    private void enqueueFinalUpdate() {
+        mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
+        mUpdateHandler.sendMessageDelayed(
+                mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1),
+                5 * MINUTE_IN_MILLIS);
+    }
 
+    private static final int MSG_UPDATE = 1;
+    private static final int MSG_FINAL_UPDATE = 2;
+
+    private Handler.Callback mUpdateCallback = new Handler.Callback() {
         @Override
-        public void run() {
+        public boolean handleMessage(Message msg) {
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-            boolean keepService = false;
-            // for each update from the database, remember which download is
-            // supposed to get restarted soonest in the future
-            long wakeUp = Long.MAX_VALUE;
-            for (;;) {
-                synchronized (DownloadService.this) {
-                    if (mUpdateThread != this) {
-                        throw new IllegalStateException(
-                                "multiple UpdateThreads in DownloadService");
+
+            final int startId = msg.arg1;
+            if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId);
+
+            // Since database is current source of truth, our "active" status
+            // depends on database state. We always get one final update pass
+            // once the real actions have finished and persisted their state.
+
+            // TODO: switch to asking real tasks to derive active state
+            // TODO: handle media scanner timeouts
+
+            final boolean isActive;
+            synchronized (mDownloads) {
+                isActive = updateLocked();
+            }
+
+            if (msg.what == MSG_FINAL_UPDATE) {
+                // Dump thread stacks belonging to pool
+                for (Map.Entry<Thread, StackTraceElement[]> entry :
+                        Thread.getAllStackTraces().entrySet()) {
+                    if (entry.getKey().getName().startsWith("pool")) {
+                        Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue()));
                     }
-                    if (!mPendingUpdate) {
-                        mUpdateThread = null;
-                        if (!keepService) {
-                            stopSelf();
-                        }
-                        if (wakeUp != Long.MAX_VALUE) {
-                            scheduleAlarm(wakeUp);
-                        }
-                        return;
-                    }
-                    mPendingUpdate = false;
                 }
 
-                synchronized (mDownloads) {
-                    long now = mSystemFacade.currentTimeMillis();
-                    boolean mustScan = false;
-                    keepService = false;
-                    wakeUp = Long.MAX_VALUE;
-                    Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
+                // Dump speed and update details
+                mNotifier.dumpSpeeds();
 
-                    Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                            null, null, null, null);
-                    if (cursor == null) {
-                        continue;
-                    }
-                    try {
-                        DownloadInfo.Reader reader =
-                                new DownloadInfo.Reader(getContentResolver(), cursor);
-                        int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
-                        if (Constants.LOGVV) {
-                            Log.i(Constants.TAG, "number of rows from downloads-db: " +
-                                    cursor.getCount());
-                        }
-                        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
-                            long id = cursor.getLong(idColumn);
-                            idsNoLongerInDatabase.remove(id);
-                            DownloadInfo info = mDownloads.get(id);
-                            if (info != null) {
-                                updateDownload(reader, info, now);
-                            } else {
-                                info = insertDownloadLocked(reader, now);
-                            }
+                Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
+                        + "; someone didn't update correctly.");
+            }
 
-                            if (info.shouldScanFile() && !scanFile(info, true, false)) {
-                                mustScan = true;
-                                keepService = true;
-                            }
-                            if (info.hasCompletionNotification()) {
-                                keepService = true;
-                            }
-                            long next = info.nextAction(now);
-                            if (next == 0) {
-                                keepService = true;
-                            } else if (next > 0 && next < wakeUp) {
-                                wakeUp = next;
-                            }
-                        }
-                    } finally {
-                        cursor.close();
-                    }
+            if (isActive) {
+                // Still doing useful work, keep service alive. These active
+                // tasks will trigger another update pass when they're finished.
 
-                    for (Long id : idsNoLongerInDatabase) {
-                        deleteDownloadLocked(id);
-                    }
+                // Enqueue delayed update pass to catch finished operations that
+                // didn't trigger an update pass; these are bugs.
+                enqueueFinalUpdate();
 
-                    // is there a need to start the DownloadService? yes, if there are rows to be
-                    // deleted.
-                    if (!mustScan) {
-                        for (DownloadInfo info : mDownloads.values()) {
-                            if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
-                                mustScan = true;
-                                keepService = true;
-                                break;
-                            }
-                        }
-                    }
-                    mNotifier.updateWith(mDownloads.values());
-                    if (mustScan) {
-                        bindMediaScanner();
-                    } else {
-                        mMediaScannerConnection.disconnectMediaScanner();
-                    }
+            } else {
+                // No active tasks, and any pending update messages can be
+                // ignored, since any updates important enough to initiate tasks
+                // will always be delivered with a new startId.
 
-                    // look for all rows with deleted flag set and delete the rows from the database
-                    // permanently
-                    for (DownloadInfo info : mDownloads.values()) {
-                        if (info.mDeleted) {
-                            // this row is to be deleted from the database. but does it have
-                            // mediaProviderUri?
-                            if (TextUtils.isEmpty(info.mMediaProviderUri)) {
-                                if (info.shouldScanFile()) {
-                                    // initiate rescan of the file to - which will populate
-                                    // mediaProviderUri column in this row
-                                    if (!scanFile(info, false, true)) {
-                                        throw new IllegalStateException("scanFile failed!");
-                                    }
-                                    continue;
-                                }
-                            } else {
-                                // yes it has mediaProviderUri column already filled in.
-                                // delete it from MediaProvider database.
-                                getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
-                                        null);
-                            }
-                            // delete the file
-                            deleteFileIfExists(info.mFileName);
-                            // delete from the downloads db
-                            getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                                    Downloads.Impl._ID + " = ? ",
-                                    new String[]{String.valueOf(info.mId)});
-                        }
-                    }
+                if (stopSelfResult(startId)) {
+                    if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
+                    getContentResolver().unregisterContentObserver(mObserver);
+                    mScanner.shutdown();
+                    mUpdateThread.quit();
                 }
             }
+
+            return true;
+        }
+    };
+
+    /**
+     * Update {@link #mDownloads} to match {@link DownloadProvider} state.
+     * Depending on current download state it may enqueue {@link DownloadThread}
+     * instances, request {@link DownloadScanner} scans, update user-visible
+     * notifications, and/or schedule future actions with {@link AlarmManager}.
+     * <p>
+     * Should only be called from {@link #mUpdateThread} as after being
+     * requested through {@link #enqueueUpdate()}.
+     *
+     * @return If there are active tasks being processed, as of the database
+     *         snapshot taken in this update.
+     */
+    private boolean updateLocked() {
+        final long now = mSystemFacade.currentTimeMillis();
+
+        boolean isActive = false;
+        long nextActionMillis = Long.MAX_VALUE;
+
+        final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
+
+        final ContentResolver resolver = getContentResolver();
+        final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+                null, null, null, null);
+        try {
+            final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
+            final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
+            while (cursor.moveToNext()) {
+                final long id = cursor.getLong(idColumn);
+                staleIds.remove(id);
+
+                DownloadInfo info = mDownloads.get(id);
+                if (info != null) {
+                    updateDownload(reader, info, now);
+                } else {
+                    info = insertDownloadLocked(reader, now);
+                }
+
+                if (info.mDeleted) {
+                    // Delete download if requested, but only after cleaning up
+                    if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
+                        resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
+                    }
+
+                    deleteFileIfExists(info.mFileName);
+                    resolver.delete(info.getAllDownloadsUri(), null, null);
+
+                } else {
+                    // Kick off download task if ready
+                    final boolean activeDownload = info.startDownloadIfReady(mExecutor);
+
+                    // Kick off media scan if completed
+                    final boolean activeScan = info.startScanIfReady(mScanner);
+
+                    if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) {
+                        Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload
+                                + ", activeScan=" + activeScan);
+                    }
+
+                    isActive |= activeDownload;
+                    isActive |= activeScan;
+                }
+
+                // Keep track of nearest next action
+                nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
+            }
+        } finally {
+            cursor.close();
         }
 
-        private void bindMediaScanner() {
-            if (!mMediaScannerConnecting) {
-                Intent intent = new Intent();
-                intent.setClassName("com.android.providers.media",
-                        "com.android.providers.media.MediaScannerService");
-                mMediaScannerConnecting = true;
-                bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
-            }
+        // Clean up stale downloads that disappeared
+        for (Long id : staleIds) {
+            deleteDownloadLocked(id);
         }
 
-        private void scheduleAlarm(long wakeUp) {
-            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-            if (alarms == null) {
-                Log.e(Constants.TAG, "couldn't get alarm manager");
-                return;
-            }
+        // Update notifications visible to user
+        mNotifier.updateWith(mDownloads.values());
 
+        // Set alarm when next action is in future. It's okay if the service
+        // continues to run in meantime, since it will kick off an update pass.
+        if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
             if (Constants.LOGV) {
-                Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
+                Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
             }
 
-            Intent intent = new Intent(Constants.ACTION_RETRY);
-            intent.setClassName("com.android.providers.downloads",
-                    DownloadReceiver.class.getName());
-            alarms.set(
-                    AlarmManager.RTC_WAKEUP,
-                    mSystemFacade.currentTimeMillis() + wakeUp,
-                    PendingIntent.getBroadcast(DownloadService.this, 0, intent,
-                            PendingIntent.FLAG_ONE_SHOT));
+            final Intent intent = new Intent(Constants.ACTION_RETRY);
+            intent.setClass(this, DownloadReceiver.class);
+            mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
+                    PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
         }
+
+        return isActive;
     }
 
     /**
@@ -435,14 +376,14 @@
      * download if appropriate.
      */
     private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
-        DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
+        final DownloadInfo info = reader.newDownloadInfo(
+                this, mSystemFacade, mStorageManager, mNotifier);
         mDownloads.put(info.mId, info);
 
         if (Constants.LOGVV) {
             Log.v(Constants.TAG, "processing inserted download " + info.mId);
         }
 
-        info.startIfReady(now, mStorageManager);
         return info;
     }
 
@@ -450,15 +391,11 @@
      * Updates the local copy of the info about a download.
      */
     private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
-        int oldVisibility = info.mVisibility;
-        int oldStatus = info.mStatus;
-
         reader.updateFromDatabase(info);
         if (Constants.LOGVV) {
             Log.v(Constants.TAG, "processing updated download " + info.mId +
                     ", status: " + info.mStatus);
         }
-        info.startIfReady(now, mStorageManager);
     }
 
     /**
@@ -473,88 +410,20 @@
             if (Constants.LOGVV) {
                 Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
             }
-            new File(info.mFileName).delete();
+            deleteFileIfExists(info.mFileName);
         }
         mDownloads.remove(info.mId);
     }
 
-    /**
-     * Attempts to scan the file if necessary.
-     * @return true if the file has been properly scanned.
-     */
-    private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
-            final boolean deleteFile) {
-        synchronized (this) {
-            if (mMediaScannerService == null) {
-                // not bound to mediaservice. but if in the process of connecting to it, wait until
-                // connection is resolved
-                while (mMediaScannerConnecting) {
-                    Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
-                    try {
-                        this.wait(WAIT_TIMEOUT);
-                    } catch (InterruptedException e1) {
-                        throw new IllegalStateException("wait interrupted");
-                    }
-                }
-            }
-            // do we have mediaservice?
-            if (mMediaScannerService == null) {
-                // no available MediaService And not even in the process of connecting to it
-                return false;
-            }
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "Scanning file " + info.mFileName);
-            }
-            try {
-                final Uri key = info.getAllDownloadsUri();
-                final long id = info.mId;
-                mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
-                        new IMediaScannerListener.Stub() {
-                            public void scanCompleted(String path, Uri uri) {
-                                if (updateDatabase) {
-                                    // Mark this as 'scanned' in the database
-                                    // so that it is NOT subject to re-scanning by MediaScanner
-                                    // next time this database row row is encountered
-                                    ContentValues values = new ContentValues();
-                                    values.put(Constants.MEDIA_SCANNED, 1);
-                                    if (uri != null) {
-                                        values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
-                                                uri.toString());
-                                    }
-                                    getContentResolver().update(key, values, null, null);
-                                } else if (deleteFile) {
-                                    if (uri != null) {
-                                        // use the Uri returned to delete it from the MediaProvider
-                                        getContentResolver().delete(uri, null, null);
-                                    }
-                                    // delete the file and delete its row from the downloads db
-                                    deleteFileIfExists(path);
-                                    getContentResolver().delete(
-                                            Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                                            Downloads.Impl._ID + " = ? ",
-                                            new String[]{String.valueOf(id)});
-                                }
-                            }
-                        });
-                return true;
-            } catch (RemoteException e) {
-                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
-                return false;
-            }
-        }
-    }
-
     private void deleteFileIfExists(String path) {
-        try {
-            if (!TextUtils.isEmpty(path)) {
-                if (Constants.LOGVV) {
-                    Log.d(TAG, "deleteFileIfExists() deleting " + path);
-                }
-                File file = new File(path);
-                file.delete();
+        if (!TextUtils.isEmpty(path)) {
+            if (Constants.LOGVV) {
+                Log.d(TAG, "deleteFileIfExists() deleting " + path);
             }
-        } catch (Exception e) {
-            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
+            final File file = new File(path);
+            if (file.exists() && !file.delete()) {
+                Log.w(TAG, "file: '" + path + "' couldn't be deleted");
+            }
         }
     }
 
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 34bc8e3..6a0eb47 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -16,16 +16,33 @@
 
 package com.android.providers.downloads;
 
+import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
+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_TOO_MANY_REDIRECTS;
+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_SEE_OTHER;
+import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
 
 import android.content.ContentValues;
 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.NetworkInfo;
 import android.net.NetworkPolicyManager;
-import android.net.Proxy;
 import android.net.TrafficStats;
-import android.net.http.AndroidHttpClient;
 import android.os.FileUtils;
 import android.os.PowerManager;
 import android.os.Process;
@@ -35,39 +52,51 @@
 import android.util.Log;
 import android.util.Pair;
 
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.conn.params.ConnRouteParams;
+import com.android.providers.downloads.DownloadInfo.NetworkState;
 
 import java.io.File;
-import java.io.FileNotFoundException;
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.SyncFailedException;
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import libcore.io.IoUtils;
 
 /**
- * Runs an actual download
+ * Task which executes a given {@link DownloadInfo}: making network requests,
+ * persisting data to disk, and updating {@link DownloadProvider}.
  */
-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
+
+    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) (20 * SECOND_IN_MILLIS);
 
     private final Context mContext;
     private final DownloadInfo mInfo;
     private final SystemFacade mSystemFacade;
     private final StorageManager mStorageManager;
-    private DrmConvertSession mDrmConvertSession;
+    private final DownloadNotifier mNotifier;
 
     private volatile boolean mPolicyDirty;
 
     public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
-            StorageManager storageManager) {
+            StorageManager storageManager, DownloadNotifier notifier) {
         mContext = context;
         mSystemFacade = systemFacade;
         mInfo = info;
         mStorageManager = storageManager;
+        mNotifier = notifier;
     }
 
     /**
@@ -86,12 +115,8 @@
      */
     static class State {
         public String mFilename;
-        public FileOutputStream mStream;
         public String mMimeType;
-        public boolean mCountRetry = false;
         public int mRetryAfter = 0;
-        public int mRedirectCount = 0;
-        public String mNewUri;
         public boolean mGotData = false;
         public String mRequestUri;
         public long mTotalBytes = -1;
@@ -100,6 +125,7 @@
         public boolean mContinuingDownload = false;
         public long mBytesNotified = 0;
         public long mTimeLastNotification = 0;
+        public int mNetworkType = ConnectivityManager.TYPE_NONE;
 
         /** Historical bytes/second speed of this download. */
         public long mSpeed;
@@ -108,6 +134,13 @@
         /** Bytes transferred since current sample started. */
         public long mSpeedSampleBytes;
 
+        public long mContentLength = -1;
+        public String mContentDisposition;
+        public String mContentLocation;
+
+        public int mRedirectionCount;
+        public URL mUrl;
+
         public State(DownloadInfo info) {
             mMimeType = Intent.normalizeMimeType(info.mMimeType);
             mRequestUri = info.mUri;
@@ -115,33 +148,23 @@
             mTotalBytes = info.mTotalBytes;
             mCurrentBytes = info.mCurrentBytes;
         }
+
+        public void resetBeforeExecute() {
+            // Reset any state from previous execution
+            mContentLength = -1;
+            mContentDisposition = null;
+            mContentLocation = null;
+            mRedirectionCount = 0;
+        }
     }
 
-    /**
-     * State within executeDownload()
-     */
-    private static class InnerState {
-        public String mHeaderContentLength;
-        public String mHeaderContentDisposition;
-        public String mHeaderContentLocation;
-    }
-
-    /**
-     * Raised from methods called by executeDownload() to indicate that the download should be
-     * retried immediately.
-     */
-    private class RetryDownload extends Throwable {}
-
-    /**
-     * Executes the download in a separate thread
-     */
     @Override
     public void run() {
         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
         try {
             runInternal();
         } finally {
-            DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
+            mNotifier.notifyDownloadSpeed(mInfo.mId, 0);
         }
     }
 
@@ -155,9 +178,9 @@
         }
 
         State state = new State(mInfo);
-        AndroidHttpClient client = null;
         PowerManager.WakeLock wakeLock = null;
         int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+        int numFailed = mInfo.mNumFailed;
         String errorMsg = null;
 
         final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
@@ -170,39 +193,29 @@
             // while performing download, register for rules updates
             netPolicy.registerListener(mPolicyListener);
 
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
+            Log.i(Constants.TAG, "Download " + mInfo.mId + " 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) {
+                state.mNetworkType = info.getType();
             }
 
-            client = AndroidHttpClient.newInstance(userAgent(), mContext);
-
-            // 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);
-                // Set or unset proxy, which may have changed since last GET request.
-                // setDefaultProxy() supports null as proxy parameter.
-                ConnRouteParams.setDefaultProxy(client.getParams(),
-                        Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
-                HttpGet request = new HttpGet(state.mRequestUri);
-                try {
-                    executeDownload(state, client, request);
-                    finished = true;
-                } catch (RetryDownload exc) {
-                    // fall through
-                } finally {
-                    request.abort();
-                    request = null;
-                }
+            try {
+                // TODO: migrate URL sanity checking into client side of API
+                state.mUrl = new URL(state.mRequestUri);
+            } catch (MalformedURLException e) {
+                throw new StopRequestException(STATUS_BAD_REQUEST, e);
             }
 
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
-            }
+            executeDownload(state);
+
             finalizeDestinationFile(state);
             finalStatus = Downloads.Impl.STATUS_SUCCESS;
         } catch (StopRequestException error) {
@@ -213,9 +226,37 @@
             if (Constants.LOGV) {
                 Log.w(Constants.TAG, msg, error);
             }
-            finalStatus = error.mFinalStatus;
+            finalStatus = error.getFinalStatus();
+
+            // Nobody below our level should request retries, since we handle
+            // failure counts at this level.
+            if (finalStatus == STATUS_WAITING_TO_RETRY) {
+                throw new IllegalStateException("Execution should always throw final error codes");
+            }
+
+            // Some errors should be retryable, unless we fail too many times.
+            if (isStatusRetryable(finalStatus)) {
+                if (state.mGotData) {
+                    numFailed = 1;
+                } else {
+                    numFailed += 1;
+                }
+
+                if (numFailed < Constants.MAX_RETRIES) {
+                    final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+                    if (info != null && info.getType() == state.mNetworkType
+                            && info.isConnected()) {
+                        // Underlying network is still intact, use normal backoff
+                        finalStatus = STATUS_WAITING_TO_RETRY;
+                    } else {
+                        // Network changed, retry on any next available
+                        finalStatus = STATUS_WAITING_FOR_NETWORK;
+                    }
+                }
+            }
+
             // fall through to finally block
-        } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
+        } catch (Throwable ex) {
             errorMsg = ex.getMessage();
             String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
             Log.w(Constants.TAG, msg, ex);
@@ -225,14 +266,11 @@
             TrafficStats.clearThreadStatsTag();
             TrafficStats.clearThreadStatsUid();
 
-            if (client != null) {
-                client.close();
-                client = null;
-            }
             cleanupDestination(state, finalStatus);
-            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
-                                    state.mGotData, state.mFilename,
-                                    state.mNewUri, state.mMimeType, errorMsg);
+            notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
+
+            Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status "
+                    + Downloads.Impl.statusToString(finalStatus));
 
             netPolicy.unregisterListener(mPolicyListener);
 
@@ -245,16 +283,12 @@
     }
 
     /**
-     * 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, AndroidHttpClient client, HttpGet request)
-            throws StopRequestException, RetryDownload {
-        InnerState innerState = new InnerState();
-        byte data[] = new byte[Constants.BUFFER_SIZE];
-
-        setupDestinationFile(state, innerState);
-        addRequestHeaders(state, request);
+    private void executeDownload(State state) throws StopRequestException {
+        state.resetBeforeExecute();
+        setupDestinationFile(state);
 
         // skip when already finished; remove after fixing race in 5217390
         if (state.mCurrentBytes == state.mTotalBytes) {
@@ -263,19 +297,136 @@
             return;
         }
 
-        // check just before sending the request to avoid using an invalid connection at all
-        checkConnectivity();
+        while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) {
+            // Open connection and follow any redirects until we have a useful
+            // response with body.
+            HttpURLConnection conn = null;
+            try {
+                checkConnectivity();
+                conn = (HttpURLConnection) state.mUrl.openConnection();
+                conn.setInstanceFollowRedirects(false);
+                conn.setConnectTimeout(DEFAULT_TIMEOUT);
+                conn.setReadTimeout(DEFAULT_TIMEOUT);
 
-        HttpResponse response = sendRequest(state, client, request);
-        handleExceptionalStatus(state, innerState, response);
+                addRequestHeaders(state, conn);
 
-        if (Constants.LOGV) {
-            Log.v(Constants.TAG, "received response for " + mInfo.mUri);
+                final int responseCode = conn.getResponseCode();
+                switch (responseCode) {
+                    case HTTP_OK:
+                        if (state.mContinuingDownload) {
+                            throw new StopRequestException(
+                                    STATUS_CANNOT_RESUME, "Expected partial, but received OK");
+                        }
+                        processResponseHeaders(state, conn);
+                        transferData(state, conn);
+                        return;
+
+                    case HTTP_PARTIAL:
+                        if (!state.mContinuingDownload) {
+                            throw new StopRequestException(
+                                    STATUS_CANNOT_RESUME, "Expected OK, but received partial");
+                        }
+                        transferData(state, conn);
+                        return;
+
+                    case HTTP_MOVED_PERM:
+                    case HTTP_MOVED_TEMP:
+                    case HTTP_SEE_OTHER:
+                    case HTTP_TEMP_REDIRECT:
+                        final String location = conn.getHeaderField("Location");
+                        state.mUrl = new URL(state.mUrl, location);
+                        if (responseCode == HTTP_MOVED_PERM) {
+                            // Push updated URL back to database
+                            state.mRequestUri = state.mUrl.toString();
+                        }
+                        continue;
+
+                    case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
+                        throw new StopRequestException(
+                                STATUS_CANNOT_RESUME, "Requested range not satisfiable");
+
+                    case HTTP_UNAVAILABLE:
+                        parseRetryAfterHeaders(state, 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) {
+                // Trouble with low-level sockets
+                throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
+
+            } finally {
+                if (conn != null) conn.disconnect();
+            }
         }
 
-        processResponseHeaders(state, innerState, response);
-        InputStream entityStream = openResponseEntity(state, response);
-        transferData(state, innerState, data, entityStream);
+        throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
+    }
+
+    /**
+     * Transfer data from the given connection to the destination file.
+     */
+    private void transferData(State state, HttpURLConnection conn) throws StopRequestException {
+        DrmManagerClient drmClient = null;
+        InputStream in = null;
+        OutputStream out = null;
+        FileDescriptor outFd = null;
+        try {
+            try {
+                in = conn.getInputStream();
+            } catch (IOException e) {
+                throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
+            }
+
+            try {
+                if (DownloadDrmHelper.isDrmConvertNeeded(state.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();
+                } else {
+                    out = new FileOutputStream(state.mFilename, true);
+                    outFd = ((FileOutputStream) out).getFD();
+                }
+            } catch (IOException e) {
+                throw new StopRequestException(STATUS_FILE_ERROR, e);
+            }
+
+            // Start streaming data, periodically watch for pause/cancel
+            // commands and checking disk space as needed.
+            transferData(state, in, out);
+
+            try {
+                if (out instanceof DrmOutputStream) {
+                    ((DrmOutputStream) out).finish();
+                }
+            } catch (IOException e) {
+                throw new StopRequestException(STATUS_FILE_ERROR, e);
+            }
+
+        } finally {
+            if (drmClient != null) {
+                drmClient.release();
+            }
+
+            IoUtils.closeQuietly(in);
+
+            try {
+                if (out != null) out.flush();
+                if (outFd != null) outFd.sync();
+            } catch (IOException e) {
+            } finally {
+                IoUtils.closeQuietly(out);
+            }
+        }
     }
 
     /**
@@ -285,40 +436,38 @@
         // checking connectivity will apply current policy
         mPolicyDirty = false;
 
-        int networkUsable = mInfo.checkCanUseNetwork();
-        if (networkUsable != DownloadInfo.NETWORK_OK) {
+        final NetworkState networkUsable = mInfo.checkCanUseNetwork();
+        if (networkUsable != NetworkState.OK) {
             int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
-            if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
+            if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
                 mInfo.notifyPauseDueToSize(true);
-            } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
+            } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
                 status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
                 mInfo.notifyPauseDueToSize(false);
             }
-            throw new StopRequestException(status,
-                    mInfo.getLogMessageForNetworkError(networkUsable));
+            throw new StopRequestException(status, networkUsable.name());
         }
     }
 
     /**
-     * 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
+     * Transfer as much data as possible from the HTTP response to the
+     * destination file.
      */
-    private void transferData(
-            State state, InnerState innerState, byte[] data, InputStream entityStream)
+    private void transferData(State state, InputStream in, OutputStream out)
             throws StopRequestException {
+        final byte data[] = new byte[Constants.BUFFER_SIZE];
         for (;;) {
-            int bytesRead = readFromResponse(state, innerState, data, entityStream);
+            int bytesRead = readFromResponse(state, data, in);
             if (bytesRead == -1) { // success, end of stream already reached
-                handleEndOfStream(state, innerState);
+                handleEndOfStream(state);
                 return;
             }
 
             state.mGotData = true;
-            writeDataToDestination(state, data, bytesRead);
+            writeDataToDestination(state, data, bytesRead, out);
             state.mCurrentBytes += bytesRead;
-            reportProgress(state, innerState);
+            reportProgress(state);
 
             if (Constants.LOGVV) {
                 Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
@@ -332,11 +481,10 @@
     /**
      * Called after a successful completion to take any necessary action on the downloaded file.
      */
-    private void finalizeDestinationFile(State state) throws StopRequestException {
+    private void finalizeDestinationFile(State state) {
         if (state.mFilename != null) {
             // make sure the file is readable
             FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
-            syncDestination(state);
         }
     }
 
@@ -345,11 +493,6 @@
      * the downloaded file.
      */
     private void cleanupDestination(State state, int finalStatus) {
-        if (mDrmConvertSession != null) {
-            finalStatus = mDrmConvertSession.close(state.mFilename);
-        }
-
-        closeDestination(state);
         if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
             if (Constants.LOGVV) {
                 Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
@@ -360,53 +503,6 @@
     }
 
     /**
-     * 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 {
-                    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);
-                }
-            }
-        }
-    }
-
-    /**
-     * Close the destination output stream.
-     */
-    private void closeDestination(State state) {
-        try {
-            // close the file
-            if (state.mStream != null) {
-                state.mStream.close();
-                state.mStream = null;
-            }
-        } 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.
      */
@@ -430,7 +526,7 @@
     /**
      * Report download progress through the database if necessary.
      */
-    private void reportProgress(State state, InnerState innerState) {
+    private void reportProgress(State state) {
         final long now = SystemClock.elapsedRealtime();
 
         final long sampleDelta = now - state.mSpeedSampleStart;
@@ -444,10 +540,13 @@
                 state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
             }
 
+            // Only notify once we have a full sample window
+            if (state.mSpeedSampleStart != 0) {
+                mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed);
+            }
+
             state.mSpeedSampleStart = now;
             state.mSpeedSampleBytes = state.mCurrentBytes;
-
-            DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
         }
 
         if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
@@ -465,37 +564,25 @@
      * @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)
+    private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
             throws StopRequestException {
-        for (;;) {
+        mStorageManager.verifySpaceBeforeWritingToFile(
+                mInfo.mDestination, state.mFilename, bytesRead);
+
+        boolean forceVerified = false;
+        while (true) {
             try {
-                if (state.mStream == null) {
-                    state.mStream = new FileOutputStream(state.mFilename, true);
-                }
-                mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename,
-                        bytesRead);
-                if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) {
-                    state.mStream.write(data, 0, bytesRead);
-                } else {
-                    byte[] convertedData = mDrmConvertSession.convert(data, bytesRead);
-                    if (convertedData != null) {
-                        state.mStream.write(convertedData, 0, convertedData.length);
-                    } else {
-                        throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
-                                "Error converting drm data.");
-                    }
-                }
+                out.write(data, 0, bytesRead);
                 return;
             } catch (IOException ex) {
-                // couldn't write to file. are we out of space? check.
-                // TODO this check should only be done once. why is this being done
-                // in a while(true) loop (see the enclosing statement: for(;;)
-                if (state.mStream != null) {
+                // 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);
-                }
-            } finally {
-                if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
-                    closeDestination(state);
+                    forceVerified = true;
+                } else {
+                    throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
+                            "Failed to write data: " + ex);
                 }
             }
         }
@@ -505,29 +592,30 @@
      * 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 {
+    private void handleEndOfStream(State state) throws StopRequestException {
         ContentValues values = new ContentValues();
         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
-        if (innerState.mHeaderContentLength == null) {
+        if (state.mContentLength == -1) {
             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
         }
         mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
 
-        boolean lengthMismatched = (innerState.mHeaderContentLength != null)
-                && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength));
+        final boolean lengthMismatched = (state.mContentLength != -1)
+                && (state.mCurrentBytes != state.mContentLength);
         if (lengthMismatched) {
             if (cannotResume(state)) {
-                throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
-                        "mismatched content length");
+                throw new StopRequestException(STATUS_CANNOT_RESUME,
+                        "mismatched content length; unable to resume");
             } else {
-                throw new StopRequestException(getFinalStatusForHttpError(state),
+                throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
                         "closed socket before end of file");
             }
         }
     }
 
     private boolean cannotResume(State state) {
-        return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null;
+        return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
+                || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
     }
 
     /**
@@ -536,91 +624,51 @@
      * @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 StopRequestException {
+    private int readFromResponse(State state, byte[] data, InputStream entityStream)
+            throws StopRequestException {
         try {
             return entityStream.read(data);
         } catch (IOException ex) {
-            logNetworkState(mInfo.mUid);
+            // TODO: handle stream errors the same as other retries
+            if ("unexpected end of stream".equals(ex.getMessage())) {
+                return -1;
+            }
+
             ContentValues values = new ContentValues();
             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
             mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
             if (cannotResume(state)) {
-                String message = "while reading response: " + ex.toString()
-                + ", can't resume interrupted download with no ETag";
-                throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
-                        message, ex);
+                throw new StopRequestException(STATUS_CANNOT_RESUME,
+                        "Failed reading response: " + ex + "; unable to resume", ex);
             } else {
-                throw new StopRequestException(getFinalStatusForHttpError(state),
-                        "while reading response: " + ex.toString(), ex);
+                throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
+                        "Failed reading response: " + ex, ex);
             }
         }
     }
 
     /**
-     * Open a stream for the HTTP response entity, handling I/O errors.
-     * @return an InputStream to read the response entity
+     * Prepare target file based on given network response. Derives filename and
+     * target size as needed.
      */
-    private InputStream openResponseEntity(State state, HttpResponse response)
+    private void processResponseHeaders(State state, HttpURLConnection conn)
             throws StopRequestException {
-        try {
-            return response.getEntity().getContent();
-        } catch (IOException ex) {
-            logNetworkState(mInfo.mUid);
-            throw new StopRequestException(getFinalStatusForHttpError(state),
-                    "while getting entity: " + ex.toString(), ex);
-        }
-    }
+        // TODO: fallocate the entire file if header gave us specific length
 
-    private void logNetworkState(int uid) {
-        if (Constants.LOGX) {
-            Log.i(Constants.TAG,
-                    "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down"));
-        }
-    }
-
-    /**
-     * 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 StopRequestException {
-        if (state.mContinuingDownload) {
-            // ignore response headers on resume requests
-            return;
-        }
-
-        readResponseHeaders(state, innerState, response);
-        if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
-            mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType);
-            if (mDrmConvertSession == null) {
-                throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype "
-                        + state.mMimeType + " can not be converted.");
-            }
-        }
+        readResponseHeaders(state, conn);
 
         state.mFilename = Helpers.generateSaveFile(
                 mContext,
                 mInfo.mUri,
                 mInfo.mHint,
-                innerState.mHeaderContentDisposition,
-                innerState.mHeaderContentLocation,
+                state.mContentDisposition,
+                state.mContentLocation,
                 state.mMimeType,
                 mInfo.mDestination,
-                (innerState.mHeaderContentLength != null) ?
-                        Long.parseLong(innerState.mHeaderContentLength) : 0,
-                mInfo.mIsPublicApi, mStorageManager);
-        try {
-            state.mStream = new FileOutputStream(state.mFilename);
-        } catch (FileNotFoundException exc) {
-            throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
-                    "while opening destination file: " + exc.toString(), exc);
-        }
-        if (Constants.LOGV) {
-            Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
-        }
+                state.mContentLength,
+                mStorageManager);
 
-        updateDatabaseFromHeaders(state, innerState);
+        updateDatabaseFromHeaders(state);
         // check connectivity again now that we know the total size
         checkConnectivity();
     }
@@ -629,7 +677,7 @@
      * Update necessary database fields based on values of HTTP response headers that have been
      * read.
      */
-    private void updateDatabaseFromHeaders(State state, InnerState innerState) {
+    private void updateDatabaseFromHeaders(State state) {
         ContentValues values = new ContentValues();
         values.put(Downloads.Impl._DATA, state.mFilename);
         if (state.mHeaderETag != null) {
@@ -645,219 +693,48 @@
     /**
      * Read headers from the HTTP response and store them into local state.
      */
-    private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
+    private void readResponseHeaders(State state, HttpURLConnection conn)
             throws StopRequestException {
-        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();
-        }
+        state.mContentDisposition = conn.getHeaderField("Content-Disposition");
+        state.mContentLocation = conn.getHeaderField("Content-Location");
+
         if (state.mMimeType == null) {
-            header = response.getFirstHeader("Content-Type");
-            if (header != null) {
-                state.mMimeType = Intent.normalizeMimeType(header.getValue());
-            }
-        }
-        header = response.getFirstHeader("ETag");
-        if (header != null) {
-            state.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();
-                state.mTotalBytes = mInfo.mTotalBytes =
-                        Long.parseLong(innerState.mHeaderContentLength);
-            }
-        } 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: " + state.mHeaderETag);
-            Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
+            state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
         }
 
-        boolean noSizeInfo = innerState.mHeaderContentLength == null
-                && (headerTransferEncoding == null
-                    || !headerTransferEncoding.equalsIgnoreCase("chunked"));
+        state.mHeaderETag = conn.getHeaderField("ETag");
+
+        final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
+        if (transferEncoding == null) {
+            state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
+        } else {
+            Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
+            state.mContentLength = -1;
+        }
+
+        state.mTotalBytes = state.mContentLength;
+        mInfo.mTotalBytes = state.mContentLength;
+
+        final boolean noSizeInfo = state.mContentLength == -1
+                && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
         if (!mInfo.mNoIntegrity && noSizeInfo) {
-            throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
+            throw new StopRequestException(STATUS_CANNOT_RESUME,
                     "can't know size of download, giving up");
         }
     }
 
-    /**
-     * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
-     */
-    private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
-            throws StopRequestException, RetryDownload {
-        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);
-        }
-
-        if (Constants.LOGV) {
-            Log.i(Constants.TAG, "recevd_status = " + statusCode +
-                    ", mContinuingDownload = " + state.mContinuingDownload);
-        }
-        int expectedStatus = state.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 StopRequestException {
-        if (statusCode == 416) {
-            // 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 (Downloads.Impl.isStatusError(statusCode)) {
-            finalStatus = statusCode;
-        } else if (statusCode >= 300 && statusCode < 400) {
-            finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
-        } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
-            finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
+    private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
+        state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
+        if (state.mRetryAfter < 0) {
+            state.mRetryAfter = 0;
         } else {
-            finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
-        }
-        throw new StopRequestException(finalStatus, "http error " +
-                statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
-    }
-
-    /**
-     * Handle a 3xx redirect status.
-     */
-    private void handleRedirect(State state, HttpResponse response, int statusCode)
-            throws StopRequestException, RetryDownload {
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
-        }
-        if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
-            throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
-                    "too many redirects");
-        }
-        Header header = response.getFirstHeader("Location");
-        if (header == null) {
-            return;
-        }
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "Location :" + header.getValue());
-        }
-
-        String newUri;
-        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);
+            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;
             }
-            throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
-                    "Couldn't resolve redirect URI");
-        }
-        ++state.mRedirectCount;
-        state.mRequestUri = newUri;
-        if (statusCode == 301 || statusCode == 303) {
-            // use the new URI for all future requests (should a retry/resume be necessary)
-            state.mNewUri = newUri;
-        }
-        throw new RetryDownload();
-    }
-
-    /**
-     * Handle a 503 Service Unavailable status by processing the Retry-After header.
-     */
-    private void handleServiceUnavailable(State state, HttpResponse response)
-            throws StopRequestException {
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "got HTTP response code 503");
-        }
-        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 StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
-                "got 503 Service Unavailable, will retry later");
-    }
-
-    /**
-     * Send the request to the server, handling any I/O exceptions.
-     */
-    private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
-            throws StopRequestException {
-        try {
-            return client.execute(request);
-        } catch (IllegalArgumentException ex) {
-            throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
-                    "while trying to execute request: " + ex.toString(), ex);
-        } catch (IOException ex) {
-            logNetworkState(mInfo.mUid);
-            throw new StopRequestException(getFinalStatusForHttpError(state),
-                    "while trying to execute request: " + ex.toString(), ex);
-        }
-    }
-
-    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;
+            state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
+            state.mRetryAfter *= 1000;
         }
     }
 
@@ -865,8 +742,7 @@
      * 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 {
+    private void setupDestinationFile(State state) 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 +
@@ -913,15 +789,9 @@
                         Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
                                 ", and starting with file of length: " + fileLength);
                     }
-                    try {
-                        state.mStream = new FileOutputStream(state.mFilename, true);
-                    } catch (FileNotFoundException exc) {
-                        throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
-                                "while opening destination for resuming: " + exc.toString(), exc);
-                    }
                     state.mCurrentBytes = (int) fileLength;
                     if (mInfo.mTotalBytes != -1) {
-                        innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
+                        state.mContentLength = mInfo.mTotalBytes;
                     }
                     state.mHeaderETag = mInfo.mETag;
                     state.mContinuingDownload = true;
@@ -933,30 +803,30 @@
                 }
             }
         }
-
-        if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
-            closeDestination(state);
-        }
     }
 
     /**
      * Add custom headers for this download to the HTTP request.
      */
-    private void addRequestHeaders(State state, HttpGet request) {
+    private void addRequestHeaders(State state, HttpURLConnection conn) {
         for (Pair<String, String> header : mInfo.getHeaders()) {
-            request.addHeader(header.first, header.second);
+            conn.addRequestProperty(header.first, header.second);
         }
 
+        // Only splice in user agent when not already defined
+        if (conn.getRequestProperty("User-Agent") == null) {
+            conn.addRequestProperty("User-Agent", userAgent());
+        }
+
+        // Defeat transparent gzip compression, since it doesn't allow us to
+        // easily resume partial downloads.
+        conn.setRequestProperty("Accept-Encoding", "identity");
+
         if (state.mContinuingDownload) {
             if (state.mHeaderETag != null) {
-                request.addHeader("If-Match", state.mHeaderETag);
+                conn.addRequestProperty("If-Match", state.mHeaderETag);
             }
-            request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-");
-            if (Constants.LOGV) {
-                Log.i(Constants.TAG, "Adding Range header: " +
-                        "bytes=" + state.mCurrentBytes + "-");
-                Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
-            }
+            conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
         }
     }
 
@@ -964,35 +834,27 @@
      * 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 uri, String mimeType, String errorMsg) {
-        notifyThroughDatabase(
-                status, countRetry, retryAfter, gotData, filename, uri, mimeType,
-                errorMsg);
-        if (Downloads.Impl.isStatusCompleted(status)) {
+            State state, int finalStatus, String errorMsg, int numFailed) {
+        notifyThroughDatabase(state, finalStatus, errorMsg, numFailed);
+        if (Downloads.Impl.isStatusCompleted(finalStatus)) {
             mInfo.sendIntentIfRequested();
         }
     }
 
     private void notifyThroughDatabase(
-            int status, boolean countRetry, int retryAfter, boolean gotData,
-            String filename, String uri, String mimeType, String errorMsg) {
+            State state, int finalStatus, String errorMsg, int numFailed) {
         ContentValues values = new ContentValues();
-        values.put(Downloads.Impl.COLUMN_STATUS, status);
-        values.put(Downloads.Impl._DATA, filename);
-        if (uri != null) {
-            values.put(Downloads.Impl.COLUMN_URI, uri);
-        }
-        values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
+        values.put(Downloads.Impl.COLUMN_STATUS, finalStatus);
+        values.put(Downloads.Impl._DATA, state.mFilename);
+        values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
         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);
+        values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed);
+        values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter);
+
+        if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) {
+            values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri);
         }
+
         // save the error message. could be useful to developers.
         if (!TextUtils.isEmpty(errorMsg)) {
             values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
@@ -1021,4 +883,27 @@
             mPolicyDirty = true;
         }
     };
+
+    public 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:
+                return true;
+            default:
+                return false;
+        }
+    }
 }
diff --git a/src/com/android/providers/downloads/DrmConvertSession.java b/src/com/android/providers/downloads/DrmConvertSession.java
deleted file mode 100644
index d10edf1..0000000
--- a/src/com/android/providers/downloads/DrmConvertSession.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-package com.android.providers.downloads;
-
-import android.content.Context;
-import android.drm.DrmConvertedStatus;
-import android.drm.DrmManagerClient;
-import android.util.Log;
-import android.provider.Downloads;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-
-
-public class DrmConvertSession {
-    private DrmManagerClient mDrmClient;
-    private int mConvertSessionId;
-
-    private DrmConvertSession(DrmManagerClient drmClient, int convertSessionId) {
-        mDrmClient = drmClient;
-        mConvertSessionId = convertSessionId;
-    }
-
-    /**
-     * Start of converting a file.
-     *
-     * @param context The context of the application running the convert session.
-     * @param mimeType Mimetype of content that shall be converted.
-     * @return A convert session or null in case an error occurs.
-     */
-    public static DrmConvertSession open(Context context, String mimeType) {
-        DrmManagerClient drmClient = null;
-        int convertSessionId = -1;
-        if (context != null && mimeType != null && !mimeType.equals("")) {
-            try {
-                drmClient = new DrmManagerClient(context);
-                try {
-                    convertSessionId = drmClient.openConvertSession(mimeType);
-                } catch (IllegalArgumentException e) {
-                    Log.w(Constants.TAG, "Conversion of Mimetype: " + mimeType
-                            + " is not supported.", e);
-                } catch (IllegalStateException e) {
-                    Log.w(Constants.TAG, "Could not access Open DrmFramework.", e);
-                }
-            } catch (IllegalArgumentException e) {
-                Log.w(Constants.TAG,
-                        "DrmManagerClient instance could not be created, context is Illegal.");
-            } catch (IllegalStateException e) {
-                Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly.");
-            }
-        }
-
-        if (drmClient == null || convertSessionId < 0) {
-            return null;
-        } else {
-            return new DrmConvertSession(drmClient, convertSessionId);
-        }
-    }
-    /**
-     * Convert a buffer of data to protected format.
-     *
-     * @param buffer Buffer filled with data to convert.
-     * @param size The number of bytes that shall be converted.
-     * @return A Buffer filled with converted data, if execution is ok, in all
-     *         other case null.
-     */
-    public byte [] convert(byte[] inBuffer, int size) {
-        byte[] result = null;
-        if (inBuffer != null) {
-            DrmConvertedStatus convertedStatus = null;
-            try {
-                if (size != inBuffer.length) {
-                    byte[] buf = new byte[size];
-                    System.arraycopy(inBuffer, 0, buf, 0, size);
-                    convertedStatus = mDrmClient.convertData(mConvertSessionId, buf);
-                } else {
-                    convertedStatus = mDrmClient.convertData(mConvertSessionId, inBuffer);
-                }
-
-                if (convertedStatus != null &&
-                        convertedStatus.statusCode == DrmConvertedStatus.STATUS_OK &&
-                        convertedStatus.convertedData != null) {
-                    result = convertedStatus.convertedData;
-                }
-            } catch (IllegalArgumentException e) {
-                Log.w(Constants.TAG, "Buffer with data to convert is illegal. Convertsession: "
-                        + mConvertSessionId, e);
-            } catch (IllegalStateException e) {
-                Log.w(Constants.TAG, "Could not convert data. Convertsession: " +
-                        mConvertSessionId, e);
-            }
-        } else {
-            throw new IllegalArgumentException("Parameter inBuffer is null");
-        }
-        return result;
-    }
-
-    /**
-     * Ends a conversion session of a file.
-     *
-     * @param fileName The filename of the converted file.
-     * @return Downloads.Impl.STATUS_SUCCESS if execution is ok.
-     *         Downloads.Impl.STATUS_FILE_ERROR in case converted file can not
-     *         be accessed. Downloads.Impl.STATUS_NOT_ACCEPTABLE if a problem
-     *         occurs when accessing drm framework.
-     *         Downloads.Impl.STATUS_UNKNOWN_ERROR if a general error occurred.
-     */
-    public int close(String filename) {
-        DrmConvertedStatus convertedStatus = null;
-        int result = Downloads.Impl.STATUS_UNKNOWN_ERROR;
-        if (mDrmClient != null && mConvertSessionId >= 0) {
-            try {
-                convertedStatus = mDrmClient.closeConvertSession(mConvertSessionId);
-                if (convertedStatus == null ||
-                        convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
-                        convertedStatus.convertedData == null) {
-                    result = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
-                } else {
-                    RandomAccessFile rndAccessFile = null;
-                    try {
-                        rndAccessFile = new RandomAccessFile(filename, "rw");
-                        rndAccessFile.seek(convertedStatus.offset);
-                        rndAccessFile.write(convertedStatus.convertedData);
-                        result = Downloads.Impl.STATUS_SUCCESS;
-                    } catch (FileNotFoundException e) {
-                        result = Downloads.Impl.STATUS_FILE_ERROR;
-                        Log.w(Constants.TAG, "File: " + filename + " could not be found.", e);
-                    } catch (IOException e) {
-                        result = Downloads.Impl.STATUS_FILE_ERROR;
-                        Log.w(Constants.TAG, "Could not access File: " + filename + " .", e);
-                    } catch (IllegalArgumentException e) {
-                        result = Downloads.Impl.STATUS_FILE_ERROR;
-                        Log.w(Constants.TAG, "Could not open file in mode: rw", e);
-                    } catch (SecurityException e) {
-                        Log.w(Constants.TAG, "Access to File: " + filename +
-                                " was denied denied by SecurityManager.", e);
-                    } finally {
-                        if (rndAccessFile != null) {
-                            try {
-                                rndAccessFile.close();
-                            } catch (IOException e) {
-                                result = Downloads.Impl.STATUS_FILE_ERROR;
-                                Log.w(Constants.TAG, "Failed to close File:" + filename
-                                        + ".", e);
-                            }
-                        }
-                    }
-                }
-            } catch (IllegalStateException e) {
-                Log.w(Constants.TAG, "Could not close convertsession. Convertsession: " +
-                        mConvertSessionId, e);
-            }
-        }
-        return result;
-    }
-}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 359f6fa..3320555 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -17,10 +17,6 @@
 package com.android.providers.downloads;
 
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.NetworkInfo;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.SystemClock;
@@ -29,6 +25,7 @@
 import android.webkit.MimeTypeMap;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.Random;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -44,6 +41,8 @@
     private static final Pattern CONTENT_DISPOSITION_PATTERN =
             Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
 
+    private static final Object sUniqueLock = new Object();
+
     private Helpers() {
     }
 
@@ -77,8 +76,10 @@
             String mimeType,
             int destination,
             long contentLength,
-            boolean isPublicApi, StorageManager storageManager) throws StopRequestException {
-        checkCanHandleDownload(context, mimeType, destination, isPublicApi);
+            StorageManager storageManager) throws StopRequestException {
+        if (contentLength < 0) {
+            contentLength = 0;
+        }
         String path;
         File base = null;
         if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
@@ -90,10 +91,10 @@
                                              destination);
         }
         storageManager.verifySpace(destination, path, contentLength);
-        path = getFullPath(path, mimeType, destination, base);
         if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
             path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
         }
+        path = getFullPath(path, mimeType, destination, base);
         return path;
     }
 
@@ -130,47 +131,20 @@
         if (Constants.LOGVV) {
             Log.v(Constants.TAG, "target file: " + filename + extension);
         }
-        return chooseUniqueFilename(destination, filename, extension, recoveryDir);
-    }
 
-    private static void checkCanHandleDownload(Context context, String mimeType, int destination,
-            boolean isPublicApi) throws StopRequestException {
-        if (isPublicApi) {
-            return;
-        }
+        synchronized (sUniqueLock) {
+            final String path = chooseUniqueFilenameLocked(
+                    destination, filename, extension, recoveryDir);
 
-        if (destination == Downloads.Impl.DESTINATION_EXTERNAL
-                || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
-            if (mimeType == null) {
-                throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
-                        "external download with no mime type not allowed");
+            // Claim this filename inside lock to prevent other threads from
+            // clobbering us. We're not paranoid enough to use O_EXCL.
+            try {
+                new File(path).createNewFile();
+            } catch (IOException e) {
+                throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
+                        "Failed to create target file " + path, e);
             }
-            if (!DownloadDrmHelper.isDrmMimeType(context, mimeType)) {
-                // Check to see if we are allowed to download this file. Only files
-                // that can be handled by the platform can be downloaded.
-                // special case DRM files, which we should always allow downloading.
-                Intent intent = new Intent(Intent.ACTION_VIEW);
-
-                // We can provide data as either content: or file: URIs,
-                // so allow both.  (I think it would be nice if we just did
-                // everything as content: URIs)
-                // Actually, right now the download manager's UId restrictions
-                // prevent use from using content: so it's got to be file: or
-                // nothing
-
-                PackageManager pm = context.getPackageManager();
-                intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
-                ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
-                //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
-
-                if (ri == null) {
-                    if (Constants.LOGV) {
-                        Log.v(Constants.TAG, "no handler found for type " + mimeType);
-                    }
-                    throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
-                            "no handler found for this download type");
-                }
-            }
+            return path;
         }
     }
 
@@ -321,7 +295,7 @@
         return extension;
     }
 
-    private static String chooseUniqueFilename(int destination, String filename,
+    private static String chooseUniqueFilenameLocked(int destination, String filename,
             String extension, boolean recoveryDir) throws StopRequestException {
         String fullFilename = filename + extension;
         if (!new File(fullFilename).exists()
@@ -365,14 +339,6 @@
     }
 
     /**
-     * Returns whether the network is available
-     */
-    public static boolean isNetworkAvailable(SystemFacade system, int uid) {
-        final NetworkInfo info = system.getActiveNetworkInfo(uid);
-        return info != null && info.isConnected();
-    }
-
-    /**
      * Checks whether the filename looks legitimate
      */
     static boolean isFilenameValid(String filename, File downloadsDataDir) {
diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java
index 7eca95c..0d5f5e9 100644
--- a/src/com/android/providers/downloads/OpenHelper.java
+++ b/src/com/android/providers/downloads/OpenHelper.java
@@ -30,6 +30,8 @@
 import android.net.Uri;
 import android.provider.Downloads.Impl.RequestHeaders;
 
+import java.io.File;
+
 public class OpenHelper {
     /**
      * Build an {@link Intent} to view the download at current {@link Cursor}
@@ -47,9 +49,9 @@
             }
 
             final Uri localUri = getCursorUri(cursor, COLUMN_LOCAL_URI);
-            final String filename = getCursorString(cursor, COLUMN_LOCAL_FILENAME);
+            final File file = getCursorFile(cursor, COLUMN_LOCAL_FILENAME);
             String mimeType = getCursorString(cursor, COLUMN_MEDIA_TYPE);
-            mimeType = DownloadDrmHelper.getOriginalMimeType(context, filename, mimeType);
+            mimeType = DownloadDrmHelper.getOriginalMimeType(context, file, mimeType);
 
             final Intent intent = new Intent(Intent.ACTION_VIEW);
             intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -122,4 +124,8 @@
     private static long getCursorLong(Cursor cursor, String column) {
         return cursor.getLong(cursor.getColumnIndexOrThrow(column));
     }
+
+    private static File getCursorFile(Cursor cursor, String column) {
+        return new File(cursor.getString(cursor.getColumnIndexOrThrow(column)));
+    }
 }
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index 228c716..fa4f348 100644
--- a/src/com/android/providers/downloads/RealSystemFacade.java
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -32,10 +32,12 @@
         mContext = context;
     }
 
+    @Override
     public long currentTimeMillis() {
         return System.currentTimeMillis();
     }
 
+    @Override
     public NetworkInfo getActiveNetworkInfo(int uid) {
         ConnectivityManager connectivity =
                 (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -57,6 +59,7 @@
         return conn.isActiveNetworkMetered();
     }
 
+    @Override
     public boolean isNetworkRoaming() {
         ConnectivityManager connectivity =
             (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -74,6 +77,7 @@
         return isRoaming;
     }
 
+    @Override
     public Long getMaxBytesOverMobile() {
         return DownloadManager.getMaxBytesOverMobile(mContext);
     }
@@ -92,9 +96,4 @@
     public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
         return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
     }
-
-    @Override
-    public void startThread(Thread thread) {
-        thread.start();
-    }
 }
diff --git a/src/com/android/providers/downloads/StopRequestException.java b/src/com/android/providers/downloads/StopRequestException.java
index 0ccf53c..a2b642d 100644
--- a/src/com/android/providers/downloads/StopRequestException.java
+++ b/src/com/android/providers/downloads/StopRequestException.java
@@ -15,6 +15,9 @@
  */
 package com.android.providers.downloads;
 
+import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
+import static android.provider.Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
+
 /**
  * Raised to indicate that the current request should be stopped immediately.
  *
@@ -23,15 +26,36 @@
  * URI, headers, or destination filename.
  */
 class StopRequestException extends Exception {
-    public int mFinalStatus;
+    private final int mFinalStatus;
 
     public StopRequestException(int finalStatus, String message) {
         super(message);
         mFinalStatus = finalStatus;
     }
 
-    public StopRequestException(int finalStatus, String message, Throwable throwable) {
-        super(message, throwable);
+    public StopRequestException(int finalStatus, Throwable t) {
+        super(t);
         mFinalStatus = finalStatus;
     }
+
+    public StopRequestException(int finalStatus, String message, Throwable t) {
+        super(message, t);
+        mFinalStatus = finalStatus;
+    }
+
+    public int getFinalStatus() {
+        return mFinalStatus;
+    }
+
+    public static StopRequestException throwUnhandledHttpError(int code, String message)
+            throws StopRequestException {
+        final String error = "Unhandled HTTP response: " + code + " " + message;
+        if (code >= 400 && code < 600) {
+            throw new StopRequestException(code, error);
+        } else if (code >= 300 && code < 400) {
+            throw new StopRequestException(STATUS_UNHANDLED_REDIRECT, error);
+        } else {
+            throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, error);
+        }
+    }
 }
diff --git a/src/com/android/providers/downloads/StorageManager.java b/src/com/android/providers/downloads/StorageManager.java
index 915d141..deb412e 100644
--- a/src/com/android/providers/downloads/StorageManager.java
+++ b/src/com/android/providers/downloads/StorageManager.java
@@ -71,12 +71,6 @@
      */
     private final File mDownloadDataDir;
 
-    /** the Singleton instance of this class.
-     * TODO: once DownloadService is refactored into a long-living object, there is no need
-     * for this Singleton'ing.
-     */
-    private static StorageManager sSingleton = null;
-
     /** how often do we need to perform checks on space to make sure space is available */
     private static final int FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY = 1024 * 1024; // 1MB
     private int mBytesDownloadedSinceLastCheckOnSpace = 0;
@@ -84,19 +78,9 @@
     /** misc members */
     private final Context mContext;
 
-    /**
-     * maintains Singleton instance of this class
-     */
-    synchronized static StorageManager getInstance(Context context) {
-        if (sSingleton == null) {
-            sSingleton = new StorageManager(context);
-        }
-        return sSingleton;
-    }
-
-    private StorageManager(Context context) { // constructor is private
+    public StorageManager(Context context) {
         mContext = context;
-        mDownloadDataDir = context.getCacheDir();
+        mDownloadDataDir = getDownloadDataDirectory(context);
         mExternalStorageDir = Environment.getExternalStorageDirectory();
         mSystemCacheDir = Environment.getDownloadCacheDirectory();
         startThreadToCleanupDatabaseAndPurgeFileSystem();
@@ -308,6 +292,10 @@
         return mDownloadDataDir;
     }
 
+    public static File getDownloadDataDirectory(Context context) {
+        return context.getCacheDir();
+    }
+
     /**
      * Deletes purgeable files from the cache partition. This also deletes
      * the matching database entries. Files are deleted in LRU order until
@@ -370,7 +358,7 @@
      * This is not a very common occurrence. So, do this only once in a while.
      */
     private void removeSpuriousFiles() {
-        if (true || Constants.LOGV) {
+        if (Constants.LOGV) {
             Log.i(Constants.TAG, "in removeSpuriousFiles");
         }
         // get a list of all files in system cache dir and downloads data dir
diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java
index fda97e0..15fc31f 100644
--- a/src/com/android/providers/downloads/SystemFacade.java
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -61,9 +61,4 @@
      * Returns true if the specified UID owns the specified package name.
      */
     public boolean userOwnsPackage(int uid, String pckg) throws NameNotFoundException;
-
-    /**
-     * Start a thread.
-     */
-    public void startThread(Thread thread);
 }
diff --git a/tests/Android.mk b/tests/Android.mk
index ff3e1d4..655ec16 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -8,7 +8,7 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_INSTRUMENTATION_FOR := DownloadProvider
 LOCAL_JAVA_LIBRARIES := android.test.runner
-LOCAL_STATIC_JAVA_LIBRARIES := mockwebserver littlemock dexmaker
+LOCAL_STATIC_JAVA_LIBRARIES := mockwebserver dexmaker mockito-target
 LOCAL_PACKAGE_NAME := DownloadProviderTests
 LOCAL_CERTIFICATE := media
 
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
index a65693f..e59aff0 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
@@ -16,7 +16,7 @@
 
 package com.android.providers.downloads;
 
-import static com.google.testing.littlemock.LittleMock.mock;
+import static org.mockito.Mockito.mock;
 
 import android.app.NotificationManager;
 import android.content.ComponentName;
@@ -34,6 +34,7 @@
 import android.util.Log;
 
 import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockStreamResponse;
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.RecordedRequest;
 import com.google.mockwebserver.SocketPolicy;
@@ -52,11 +53,11 @@
     protected static final String LOG_TAG = "DownloadProviderFunctionalTest";
     private static final String PROVIDER_AUTHORITY = "downloads";
     protected static final long RETRY_DELAY_MILLIS = 61 * 1000;
-    protected static final String FILE_CONTENT = "hello world hello world hello world hello world";
-    protected static final int HTTP_OK = 200;
-    protected static final int HTTP_PARTIAL_CONTENT = 206;
-    protected static final int HTTP_NOT_FOUND = 404;
-    protected static final int HTTP_SERVICE_UNAVAILABLE = 503;
+
+    protected static final String
+            FILE_CONTENT = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+    private final MockitoHelper mMockitoHelper = new MockitoHelper();
 
     protected MockWebServer mServer;
     protected MockContentResolverWithNotify mResolver;
@@ -149,6 +150,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        mMockitoHelper.setUp(getClass());
 
         // Since we're testing a system app, AppDataDirGuesser doesn't find our
         // cache dir, so set it explicitly.
@@ -161,6 +163,7 @@
         setContext(mTestContext);
         setupService();
         getService().mSystemFacade = mSystemFacade;
+        mSystemFacade.setUp();
         assertTrue(isDatabaseEmpty()); // ensure we're not messing with real data
         mServer = new MockWebServer();
         mServer.play();
@@ -170,6 +173,7 @@
     protected void tearDown() throws Exception {
         cleanUpDownloads();
         mServer.shutdown();
+        mMockitoHelper.tearDown();
         super.tearDown();
     }
 
@@ -217,6 +221,10 @@
         mServer.enqueue(resp);
     }
 
+    void enqueueResponse(MockStreamResponse resp) {
+        mServer.enqueue(resp);
+    }
+
     MockResponse buildResponse(int status, String body) {
         return new MockResponse().setResponseCode(status).setBody(body)
                 .setHeader("Content-type", "text/plain")
@@ -246,11 +254,6 @@
         return mServer.getUrl(path).toString();
     }
 
-    public void runService() throws Exception {
-        startService(null);
-        mSystemFacade.runAllThreads();
-    }
-
     protected String readStream(InputStream inputStream) throws IOException {
         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
         try {
diff --git a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
index cda607a..348dbd1 100644
--- a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
@@ -16,17 +16,22 @@
 
 package com.android.providers.downloads;
 
+import static android.app.DownloadManager.STATUS_FAILED;
+import static android.app.DownloadManager.STATUS_SUCCESSFUL;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
 import android.app.DownloadManager;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
-import android.provider.Downloads;
+import android.os.SystemClock;
 import android.util.Log;
 
-import java.io.FileInputStream;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.UnknownHostException;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Code common to tests that use the download manager public API.
@@ -44,6 +49,10 @@
             return (int) getLongField(DownloadManager.COLUMN_STATUS);
         }
 
+        public int getReason() {
+            return (int) getLongField(DownloadManager.COLUMN_REASON);
+        }
+
         public int getStatusIfExists() {
             Cursor cursor = mManager.query(new DownloadManager.Query().setFilterById(mId));
             try {
@@ -86,7 +95,8 @@
             ParcelFileDescriptor downloadedFile = mManager.openDownloadedFile(mId);
             assertTrue("Invalid file descriptor: " + downloadedFile,
                        downloadedFile.getFileDescriptor().valid());
-            InputStream stream = new FileInputStream(downloadedFile.getFileDescriptor());
+            final InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(
+                    downloadedFile);
             try {
                 return readStream(stream);
             } finally {
@@ -94,9 +104,52 @@
             }
         }
 
-        void runUntilStatus(int status) throws Exception {
-            runService();
-            assertEquals(status, getStatus());
+        void runUntilStatus(int status) throws TimeoutException {
+            final long startMillis = mSystemFacade.currentTimeMillis();
+            startService(null);
+            waitForStatus(status, startMillis);
+        }
+
+        void runUntilStatus(int status, long timeout) throws TimeoutException {
+            final long startMillis = mSystemFacade.currentTimeMillis();
+            startService(null);
+            waitForStatus(status, startMillis, timeout);
+        }
+
+        void waitForStatus(int expected, long afterMillis) throws TimeoutException {
+            waitForStatus(expected, afterMillis, 15 * SECOND_IN_MILLIS);
+        }
+
+        void waitForStatus(int expected, long afterMillis, long timeout) throws TimeoutException {
+            int actual = -1;
+
+            final long elapsedTimeout = SystemClock.elapsedRealtime() + timeout;
+            while (SystemClock.elapsedRealtime() < elapsedTimeout) {
+                if (getLongField(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP) >= afterMillis) {
+                    actual = getStatus();
+                    if (actual == STATUS_SUCCESSFUL || actual == STATUS_FAILED) {
+                        assertEquals(expected, actual);
+                        return;
+                    } else if (actual == expected) {
+                        return;
+                    }
+
+                    if (timeout > MINUTE_IN_MILLIS) {
+                        final int percent = (int) (100
+                                * getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
+                                / getLongField(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+                        Log.d(LOG_TAG, percent + "% complete");
+                    }
+                }
+
+                if (timeout > MINUTE_IN_MILLIS) {
+                    SystemClock.sleep(SECOND_IN_MILLIS * 3);
+                } else {
+                    SystemClock.sleep(100);
+                }
+            }
+
+            throw new TimeoutException("Expected status " + expected + "; only reached " + actual);
         }
 
         // max time to wait before giving up on the current download operation.
@@ -105,22 +158,10 @@
         // download thread
         private static final int TIME_TO_SLEEP = 1000;
 
-        int runUntilDone() throws InterruptedException {
-            int sleepCounter = MAX_TIME_TO_WAIT_FOR_OPERATION * 1000 / TIME_TO_SLEEP;
-            for (int i = 0; i < sleepCounter; i++) {
-                int status = getStatusIfExists();
-                if (status == -1 || Downloads.Impl.isStatusCompleted(getStatus())) {
-                    // row doesn't exist or the download is done
-                    return status;
-                }
-                // download not done yet. sleep a while and try again
-                Thread.sleep(TIME_TO_SLEEP);
-            }
-            return 0; // failed
-        }
-
         // waits until progress_so_far is >= (progress)%
         boolean runUntilProgress(int progress) throws InterruptedException {
+            startService(null);
+
             int sleepCounter = MAX_TIME_TO_WAIT_FOR_OPERATION * 1000 / TIME_TO_SLEEP;
             int numBytesReceivedSoFar = 0;
             int totalBytes = 0;
diff --git a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
index 23d300f..dbab203 100644
--- a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
@@ -16,14 +16,17 @@
 
 package com.android.providers.downloads;
 
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+import static java.net.HttpURLConnection.HTTP_OK;
+
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.net.ConnectivityManager;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.SystemClock;
 import android.provider.Downloads;
 import android.test.suitebuilder.annotation.LargeTest;
-import android.util.Log;
 
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.RecordedRequest;
@@ -31,6 +34,7 @@
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.UnknownHostException;
+import java.util.concurrent.TimeoutException;
 
 /**
  * This test exercises the entire download manager working together -- it requests downloads through
@@ -109,20 +113,22 @@
         }
     }
 
-    private void runUntilStatus(Uri downloadUri, int status) throws Exception {
-        runService();
-        boolean done = false;
-        while (!done) {
-            int rslt = getDownloadStatus(downloadUri);
-            if (rslt == Downloads.Impl.STATUS_RUNNING || rslt == Downloads.Impl.STATUS_PENDING) {
-                Log.i(TAG, "status is: " + rslt + ", for: " + downloadUri);
-                DownloadHandler.getInstance().waitUntilDownloadsTerminate();
-                Thread.sleep(100);
-            } else {
-                done = true;
+    private void runUntilStatus(Uri downloadUri, int expected) throws Exception {
+        startService(null);
+        
+        int actual = -1;
+
+        final long timeout = SystemClock.elapsedRealtime() + (15 * SECOND_IN_MILLIS);
+        while (SystemClock.elapsedRealtime() < timeout) {
+            actual = getDownloadStatus(downloadUri);
+            if (expected == actual) {
+                return;
             }
+
+            SystemClock.sleep(100);
         }
-        assertEquals(status, getDownloadStatus(downloadUri));
+
+        throw new TimeoutException("Expected status " + expected + "; only reached " + actual);
     }
 
     protected int getDownloadStatus(Uri downloadUri) {
diff --git a/tests/src/com/android/providers/downloads/FakeInputStream.java b/tests/src/com/android/providers/downloads/FakeInputStream.java
new file mode 100644
index 0000000..179ae6e
--- /dev/null
+++ b/tests/src/com/android/providers/downloads/FakeInputStream.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import java.io.InputStream;
+import java.util.Arrays;
+
+/**
+ * Provides fake data for large transfers.
+ */
+public class FakeInputStream extends InputStream {
+    private long mRemaining;
+
+    public FakeInputStream(long length) {
+        mRemaining = length;
+    }
+
+    @Override
+    public int read() {
+        final int value;
+        if (mRemaining > 0) {
+            mRemaining--;
+            return 0;
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public int read(byte[] buffer, int offset, int length) {
+        Arrays.checkOffsetAndCount(buffer.length, offset, length);
+
+        if (length > mRemaining) {
+            length = (int) mRemaining;
+        }
+        mRemaining -= length;
+
+        if (length == 0) {
+            return -1;
+        } else {
+            return length;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/downloads/FakeSystemFacade.java b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
index 481b5cb..d54c122 100644
--- a/tests/src/com/android/providers/downloads/FakeSystemFacade.java
+++ b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
@@ -7,10 +7,7 @@
 import android.net.NetworkInfo.DetailedState;
 
 import java.util.ArrayList;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Queue;
-
 public class FakeSystemFacade implements SystemFacade {
     long mTimeMillis = 0;
     Integer mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
@@ -19,20 +16,32 @@
     Long mMaxBytesOverMobile = null;
     Long mRecommendedMaxBytesOverMobile = null;
     List<Intent> mBroadcastsSent = new ArrayList<Intent>();
-    Queue<Thread> mStartedThreads = new LinkedList<Thread>();
-    private boolean returnActualTime = false;
+    private boolean mReturnActualTime = false;
+
+    public void setUp() {
+        mTimeMillis = 0;
+        mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
+        mIsRoaming = false;
+        mIsMetered = false;
+        mMaxBytesOverMobile = null;
+        mRecommendedMaxBytesOverMobile = null;
+        mBroadcastsSent.clear();
+        mReturnActualTime = false;
+    }
 
     void incrementTimeMillis(long delta) {
         mTimeMillis += delta;
     }
 
+    @Override
     public long currentTimeMillis() {
-        if (returnActualTime) {
+        if (mReturnActualTime) {
             return System.currentTimeMillis();
         }
         return mTimeMillis;
     }
 
+    @Override
     public NetworkInfo getActiveNetworkInfo(int uid) {
         if (mActiveNetworkType == null) {
             return null;
@@ -48,14 +57,17 @@
         return mIsMetered;
     }
 
+    @Override
     public boolean isNetworkRoaming() {
         return mIsRoaming;
     }
 
+    @Override
     public Long getMaxBytesOverMobile() {
         return mMaxBytesOverMobile ;
     }
 
+    @Override
     public Long getRecommendedMaxBytesOverMobile() {
         return mRecommendedMaxBytesOverMobile ;
     }
@@ -70,27 +82,7 @@
         return true;
     }
 
-    public boolean startThreadsWithoutWaiting = false;
-    public void setStartThreadsWithoutWaiting(boolean flag) {
-        this.startThreadsWithoutWaiting = flag;
-    }
-
-    @Override
-    public void startThread(Thread thread) {
-        if (startThreadsWithoutWaiting) {
-            thread.start();
-        } else {
-            mStartedThreads.add(thread);
-        }
-    }
-
-    public void runAllThreads() {
-        while (!mStartedThreads.isEmpty()) {
-            mStartedThreads.poll().run();
-        }
-    }
-
     public void setReturnActualTime(boolean flag) {
-        returnActualTime = flag;
+        mReturnActualTime = flag;
     }
 }
diff --git a/tests/src/com/android/providers/downloads/MockitoHelper.java b/tests/src/com/android/providers/downloads/MockitoHelper.java
new file mode 100644
index 0000000..485128d
--- /dev/null
+++ b/tests/src/com/android/providers/downloads/MockitoHelper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.util.Log;
+
+/**
+ * Helper for Mockito-based test cases.
+ */
+public final class MockitoHelper {
+    private static final String TAG = "MockitoHelper";
+
+    private ClassLoader mOriginalClassLoader;
+    private Thread mContextThread;
+
+    /**
+     * Creates a new helper, which in turn will set the context classloader so
+     * it can load Mockito resources.
+     *
+     * @param packageClass test case class
+     */
+    public void setUp(Class<?> packageClass) throws Exception {
+        // makes a copy of the context classloader
+        mContextThread = Thread.currentThread();
+        mOriginalClassLoader = mContextThread.getContextClassLoader();
+        ClassLoader newClassLoader = packageClass.getClassLoader();
+        Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader
+                + " to " + newClassLoader);
+        mContextThread.setContextClassLoader(newClassLoader);
+    }
+
+    /**
+     * Restores the context classloader to the previous value.
+     */
+    public void tearDown() throws Exception {
+        Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader);
+        mContextThread.setContextClassLoader(mOriginalClassLoader);
+    }
+}
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index 2661a1f..b6fd611 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -16,13 +16,23 @@
 
 package com.android.providers.downloads;
 
-import static com.google.testing.littlemock.LittleMock.anyInt;
-import static com.google.testing.littlemock.LittleMock.anyString;
-import static com.google.testing.littlemock.LittleMock.atLeastOnce;
-import static com.google.testing.littlemock.LittleMock.isA;
-import static com.google.testing.littlemock.LittleMock.never;
-import static com.google.testing.littlemock.LittleMock.times;
-import static com.google.testing.littlemock.LittleMock.verify;
+import static android.app.DownloadManager.STATUS_FAILED;
+import static android.app.DownloadManager.STATUS_PAUSED;
+import static android.net.TrafficStats.GB_IN_BYTES;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+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_UNAVAILABLE;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.app.DownloadManager;
 import android.app.Notification;
@@ -33,20 +43,24 @@
 import android.net.ConnectivityManager;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.SystemClock;
 import android.provider.Downloads;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.text.format.DateUtils;
 
 import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockStreamResponse;
 import com.google.mockwebserver.RecordedRequest;
+import com.google.mockwebserver.SocketPolicy;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.MalformedURLException;
 import java.util.List;
-
+import java.util.concurrent.TimeoutException;
 
 @LargeTest
 public class PublicApiFunctionalTest extends AbstractPublicApiTest {
@@ -76,7 +90,6 @@
         } else {
             mTestDirectory.mkdir();
         }
-        mSystemFacade.setStartThreadsWithoutWaiting(false);
     }
 
     @Override
@@ -122,6 +135,24 @@
         checkCompleteDownload(download);
     }
 
+    @Suppress
+    public void testExtremelyLarge() throws Exception {
+        // NOTE: suppressed since this takes several minutes to run
+        final long length = 3 * GB_IN_BYTES;
+        final InputStream body = new FakeInputStream(length);
+
+        enqueueResponse(new MockStreamResponse().setResponseCode(HTTP_OK).setBody(body, length)
+                .setHeader("Content-type", "text/plain")
+                .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+
+        final Download download = enqueueRequest(getRequest()
+                .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "extreme.bin"));
+        download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL, 10 * DateUtils.MINUTE_IN_MILLIS);
+
+        assertEquals(length, download.getLongField(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+        assertEquals(length, download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+    }
+
     private void checkUriContent(Uri uri) throws FileNotFoundException, IOException {
         InputStream inputStream = mResolver.openInputStream(uri);
         try {
@@ -191,7 +222,7 @@
     private MockResponse buildPartialResponse(int start, int end) {
         int totalLength = FILE_CONTENT.length();
         boolean isFirstResponse = (start == 0);
-        int status = isFirstResponse ? HTTP_OK : HTTP_PARTIAL_CONTENT;
+        int status = isFirstResponse ? HTTP_OK : HTTP_PARTIAL;
         MockResponse response = buildResponse(status, FILE_CONTENT.substring(start, end))
                 .setHeader("Content-length", totalLength)
                 .setHeader("Etag", ETAG);
@@ -385,11 +416,72 @@
         assertEquals(REQUEST_PATH, lastRequest.getPath());
     }
 
+    public void testRunawayRedirect() throws Exception {
+        for (int i = 0; i < 16; i++) {
+            enqueueResponse(buildEmptyResponse(HTTP_MOVED_TEMP)
+                    .setHeader("Location", mServer.getUrl("/" + i).toString()));
+        }
+
+        final Download download = enqueueRequest(getRequest());
+
+        // Ensure that we arrive at failed download, instead of spinning forever
+        download.runUntilStatus(DownloadManager.STATUS_FAILED);
+        assertEquals(DownloadManager.ERROR_TOO_MANY_REDIRECTS, download.getReason());
+    }
+
+    public void testRunawayUnavailable() throws Exception {
+        final int RETRY_DELAY = 120;
+        for (int i = 0; i < 16; i++) {
+            enqueueResponse(
+                    buildEmptyResponse(HTTP_UNAVAILABLE).setHeader("Retry-after", RETRY_DELAY));
+        }
+
+        final Download download = enqueueRequest(getRequest());
+        for (int i = 0; i < Constants.MAX_RETRIES - 1; i++) {
+            download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+            mSystemFacade.incrementTimeMillis((RETRY_DELAY + 60) * SECOND_IN_MILLIS);
+        }
+
+        // Ensure that we arrive at failed download, instead of spinning forever
+        download.runUntilStatus(DownloadManager.STATUS_FAILED);
+    }
+
     public void testNoEtag() throws Exception {
         enqueueResponse(buildPartialResponse(0, 5).removeHeader("Etag"));
         runSimpleFailureTest(DownloadManager.ERROR_CANNOT_RESUME);
     }
 
+    public void testEtagChanged() throws Exception {
+        final String A = "kittenz";
+        final String B = "puppiez";
+
+        // 1. Try downloading A, but partial result
+        enqueueResponse(buildResponse(HTTP_OK, A.substring(0, 2))
+                .setHeader("Content-length", A.length())
+                .setHeader("Etag", A));
+
+        // 2. Try resuming A, but fail ETag check
+        enqueueResponse(buildEmptyResponse(HTTP_PRECON_FAILED));
+
+        final Download download = enqueueRequest(getRequest());
+        RecordedRequest req;
+
+        // 1. Try downloading A, but partial result
+        download.runUntilStatus(STATUS_PAUSED);
+        assertEquals(DownloadManager.PAUSED_WAITING_TO_RETRY, download.getReason());
+        req = takeRequest();
+        assertNull(getHeaderValue(req, "Range"));
+        assertNull(getHeaderValue(req, "If-Match"));
+
+        // 2. Try resuming A, but fail ETag check
+        mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+        download.runUntilStatus(STATUS_FAILED);
+        assertEquals(HTTP_PRECON_FAILED, download.getReason());
+        req = takeRequest();
+        assertEquals("bytes=2-", getHeaderValue(req, "Range"));
+        assertEquals(A, getHeaderValue(req, "If-Match"));
+    }
+
     public void testSanitizeMediaType() throws Exception {
         enqueueResponse(buildEmptyResponse(HTTP_OK)
                 .setHeader("Content-Type", "text/html; charset=ISO-8859-4"));
@@ -400,7 +492,7 @@
 
     public void testNoContentLength() throws Exception {
         enqueueResponse(buildEmptyResponse(HTTP_OK).removeHeader("Content-length"));
-        runSimpleFailureTest(DownloadManager.ERROR_HTTP_DATA_ERROR);
+        runSimpleFailureTest(DownloadManager.ERROR_CANNOT_RESUME);
     }
 
     public void testInsufficientSpace() throws Exception {
@@ -412,22 +504,24 @@
     }
 
     public void testCancel() throws Exception {
-        mSystemFacade.setStartThreadsWithoutWaiting(true);
         // return 'real time' from FakeSystemFacade so that DownloadThread will report progress
         mSystemFacade.setReturnActualTime(true);
         enqueueResponse(buildContinuingResponse());
         Download download = enqueueRequest(getRequest());
-        startService(null);
         // give the download time to get started and progress to 1% completion
         // before cancelling it.
         boolean rslt = download.runUntilProgress(1);
         assertTrue(rslt);
         mManager.remove(download.mId);
-        startService(null);
-        int status = download.runUntilDone();
-        // make sure the row is gone from the database
-        assertEquals(-1, status);
-        mSystemFacade.setReturnActualTime(false);
+
+        // Verify that row is removed from database
+        final long timeout = SystemClock.elapsedRealtime() + (15 * SECOND_IN_MILLIS);
+        while (download.getStatusIfExists() != -1) {
+            if (SystemClock.elapsedRealtime() > timeout) {
+                throw new TimeoutException("Row wasn't removed");
+            }
+            SystemClock.sleep(100);
+        }
     }
 
     public void testDownloadCompleteBroadcast() throws Exception {
@@ -512,9 +606,9 @@
 
     public void testContentObserver() throws Exception {
         enqueueResponse(buildEmptyResponse(HTTP_OK));
-        enqueueRequest(getRequest());
         mResolver.resetNotified();
-        runService();
+        final Download download = enqueueRequest(getRequest());
+        download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
         assertTrue(mResolver.mNotifyWasCalled);
     }
 
@@ -524,10 +618,9 @@
         final Download download = enqueueRequest(
                 getRequest().setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN));
         download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
-        runService();
 
+        verify(mNotifManager, times(1)).cancelAll();
         verify(mNotifManager, never()).notify(anyString(), anyInt(), isA(Notification.class));
-        // TODO: verify that it never cancels
     }
 
     public void testNotificationVisible() throws Exception {
@@ -536,11 +629,10 @@
         // only shows in-progress notifications
         final Download download = enqueueRequest(getRequest());
         download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
-        runService();
 
         // TODO: verify different notif types with tags
+        verify(mNotifManager, times(1)).cancelAll();
         verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
-        verify(mNotifManager, times(1)).cancel(anyString(), anyInt());
     }
 
     public void testNotificationVisibleComplete() throws Exception {
@@ -549,17 +641,16 @@
         final Download download = enqueueRequest(getRequest().setNotificationVisibility(
                 DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED));
         download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
-        runService();
 
         // TODO: verify different notif types with tags
+        verify(mNotifManager, times(1)).cancelAll();
         verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
-        verify(mNotifManager, times(1)).cancel(anyString(), anyInt());
     }
 
     public void testRetryAfter() throws Exception {
         final int delay = 120;
         enqueueResponse(
-                buildEmptyResponse(HTTP_SERVICE_UNAVAILABLE).setHeader("Retry-after", delay));
+                buildEmptyResponse(HTTP_UNAVAILABLE).setHeader("Retry-after", delay));
         enqueueResponse(buildEmptyResponse(HTTP_OK));
 
         Download download = enqueueRequest(getRequest());
@@ -643,19 +734,32 @@
      * 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 {
+    private RecordedRequest runRedirectionTest(int status) throws Exception {
         enqueueResponse(buildEmptyResponse(status)
                 .setHeader("Location", mServer.getUrl(REDIRECTED_PATH).toString()));
         enqueueInterruptedDownloadResponses(5);
 
-        Download download = enqueueRequest(getRequest());
-        runService();
+        final Download download = enqueueRequest(getRequest());
+        download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+        mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+        download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+
         assertEquals(REQUEST_PATH, takeRequest().getPath());
         assertEquals(REDIRECTED_PATH, takeRequest().getPath());
 
-        mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
-        download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
         return takeRequest();
     }
+
+    /**
+     * Return value of requested HTTP header, if it exists.
+     */
+    private static String getHeaderValue(RecordedRequest req, String header) {
+        header = header.toLowerCase() + ":";
+        for (String h : req.getHeaders()) {
+            if (h.toLowerCase().startsWith(header)) {
+                return h.substring(header.length()).trim();
+            }
+        }
+        return null;
+    }
 }
diff --git a/tests/src/com/android/providers/downloads/ThreadingTest.java b/tests/src/com/android/providers/downloads/ThreadingTest.java
index 8605c76..920f703 100644
--- a/tests/src/com/android/providers/downloads/ThreadingTest.java
+++ b/tests/src/com/android/providers/downloads/ThreadingTest.java
@@ -16,23 +16,27 @@
 
 package com.android.providers.downloads;
 
+import static java.net.HttpURLConnection.HTTP_OK;
+
 import android.app.DownloadManager;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Pair;
+
+import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.SocketPolicy;
+
+import java.util.List;
+import java.util.Set;
 
 /**
  * Download manager tests that require multithreading.
  */
 @LargeTest
 public class ThreadingTest extends AbstractPublicApiTest {
-    private static class FakeSystemFacadeWithThreading extends FakeSystemFacade {
-        @Override
-        public void startThread(Thread thread) {
-            thread.start();
-        }
-    }
-
     public ThreadingTest() {
-        super(new FakeSystemFacadeWithThreading());
+        super(new FakeSystemFacade());
     }
 
     @Override
@@ -53,4 +57,41 @@
             Thread.sleep(10);
         }
     }
+
+    public void testFilenameRace() throws Exception {
+        final List<Pair<Download, String>> downloads = Lists.newArrayList();
+
+        // Request dozen files at once with same name
+        for (int i = 0; i < 12; i++) {
+            final String body = "DOWNLOAD " + i + " CONTENTS";
+            enqueueResponse(new MockResponse().setResponseCode(HTTP_OK).setBody(body)
+                    .setHeader("Content-type", "text/plain")
+                    .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+
+            final Download d = enqueueRequest(getRequest());
+            downloads.add(Pair.create(d, body));
+        }
+
+        // Kick off downloads in parallel
+        final long startMillis = mSystemFacade.currentTimeMillis();
+        startService(null);
+
+        for (Pair<Download,String> d : downloads) {
+            d.first.waitForStatus(DownloadManager.STATUS_SUCCESSFUL, startMillis);
+        }
+
+        // Ensure that contents are clean and filenames unique
+        final Set<String> seenFiles = Sets.newHashSet();
+
+        for (Pair<Download, String> d : downloads) {
+            final String file = d.first.getStringField(DownloadManager.COLUMN_LOCAL_FILENAME);
+            if (!seenFiles.add(file)) {
+                fail("Another download already claimed " + file);
+            }
+
+            final String expected = d.second;
+            final String actual = d.first.getContents();
+            assertEquals(expected, actual);
+        }
+    }
 }
diff --git a/ui/AndroidManifest.xml b/ui/AndroidManifest.xml
index 04d1863..f707dfb 100644
--- a/ui/AndroidManifest.xml
+++ b/ui/AndroidManifest.xml
@@ -9,11 +9,13 @@
     <application android:process="android.process.media"
                  android:label="@string/app_label"
                  android:icon="@mipmap/ic_launcher_download"
-                 android:hardwareAccelerated="true">
+                 android:hardwareAccelerated="true"
+                 android:supportsRtl="true"
+                 android:requiredForAllUsers="true">
+
         <activity android:name=".DownloadList"
                   android:launchMode="singleTop"
                   android:theme="@android:style/Theme.Holo.DialogWhenLarge">
-
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
diff --git a/ui/res/layout/download_list.xml b/ui/res/layout/download_list.xml
index e4ebf7c..a0ff5ff 100644
--- a/ui/res/layout/download_list.xml
+++ b/ui/res/layout/download_list.xml
@@ -30,16 +30,16 @@
                  android:layout_weight="1">
 
         <ExpandableListView android:id="@+id/date_ordered_list"
-                            android:paddingLeft="16dip"
-                            android:paddingRight="16dip"
+                            android:paddingStart="16dip"
+                            android:paddingEnd="16dip"
                             android:paddingBottom="16dip"
                             android:clipToPadding="false"
                             android:layout_width="match_parent"
                             android:layout_height="match_parent"
                             android:scrollbarStyle="outsideOverlay" />
         <ListView android:id="@+id/size_ordered_list"
-                  android:paddingLeft="16dip"
-                  android:paddingRight="16dip"
+                  android:paddingStart="16dip"
+                  android:paddingEnd="16dip"
                   android:paddingBottom="16dip"
                   android:clipToPadding="false"
                   android:layout_width="match_parent"
diff --git a/ui/res/layout/download_list_item.xml b/ui/res/layout/download_list_item.xml
index e5759d5..2435ba7 100644
--- a/ui/res/layout/download_list_item.xml
+++ b/ui/res/layout/download_list_item.xml
@@ -21,8 +21,9 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
-    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
     android:paddingTop="8dip"
     android:paddingBottom="8dip"
     android:columnCount="4"
@@ -40,9 +41,10 @@
         android:layout_width="@android:dimen/app_icon_size"
         android:layout_height="@android:dimen/app_icon_size"
         android:layout_rowSpan="3"
-        android:layout_marginRight="8dip"
+        android:layout_marginEnd="8dip"
         android:layout_gravity="center_vertical"
-        android:scaleType="centerInside" />
+        android:scaleType="centerInside"
+        android:contentDescription="@null" />
 
     <TextView
         android:id="@+id/download_title"
@@ -52,7 +54,8 @@
         android:singleLine="true"
         android:ellipsize="marquee"
         android:textStyle="bold"
-        android:textAppearance="?android:attr/textAppearance" />
+        android:textAppearance="?android:attr/textAppearance"
+        android:textAlignment="viewStart" />
 
     <TextView
         android:id="@+id/domain"
@@ -61,7 +64,8 @@
         android:layout_gravity="fill_horizontal"
         android:singleLine="true"
         android:ellipsize="marquee"
-        android:textAppearance="?android:attr/textAppearanceSmall" />
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textAlignment="viewStart" />
 
     <TextView
         android:id="@+id/size_text"
@@ -69,11 +73,13 @@
         android:layout_gravity="fill_horizontal"
         android:singleLine="true"
         android:ellipsize="marquee"
-        android:textAppearance="?android:attr/textAppearanceSmall" />
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textAlignment="viewStart" />
 
     <TextView
         android:id="@+id/status_text"
-        android:layout_marginLeft="8dip"
-        android:textAppearance="?android:attr/textAppearanceSmall" />
+        android:layout_marginStart="8dip"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textAlignment="viewStart" />
 
 </com.android.providers.downloads.ui.DownloadItem>
diff --git a/ui/res/layout/list_group_header.xml b/ui/res/layout/list_group_header.xml
index 2600f8d..466cd6c 100644
--- a/ui/res/layout/list_group_header.xml
+++ b/ui/res/layout/list_group_header.xml
@@ -15,10 +15,9 @@
 -->
 
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:minHeight="?android:attr/listPreferredItemHeight"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:paddingLeft="43dip"
-        android:layout_gravity="center_vertical"
-        android:gravity="center_vertical"/>
+    android:id="@android:id/text1"
+    android:layout_width="match_parent"
+    android:layout_height="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/expandableListPreferredItemPaddingLeft"
+    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:gravity="center_vertical" />
diff --git a/ui/res/values-ca/strings.xml b/ui/res/values-ca/strings.xml
index 43a8e38..072ca53 100644
--- a/ui/res/values-ca/strings.xml
+++ b/ui/res/values-ca/strings.xml
@@ -31,7 +31,7 @@
     <string name="dialog_failed_body" msgid="587545111677064427">"Vols tornar a intentar baixar el fitxer més tard o vols suprimir-lo de la cua?"</string>
     <string name="dialog_title_queued_body" msgid="6760681913815015219">"Fitxer en cua"</string>
     <string name="dialog_queued_body" msgid="708552801635572720">"Aquest fitxer està en cua per baixar més endavant, per tant, encara no està disponible."</string>
-    <string name="dialog_file_missing_body" msgid="3223012612774276284">"No es troba el fitxer que s\'ha baixat."</string>
+    <string name="dialog_file_missing_body" msgid="3223012612774276284">"No es pot trobar el fitxer baixat."</string>
     <string name="dialog_insufficient_space_on_external" msgid="8692452156251449195">"No es pot finalitzar la baixada. No hi ha prou espai a l\'emmagatzematge extern."</string>
     <string name="dialog_insufficient_space_on_cache" msgid="6313630206163908994">"No es pot finalitzar la baixada. No hi ha prou espai a l\'emmagatzematge intern."</string>
     <string name="dialog_cannot_resume" msgid="8664509751358983543">"S\'ha interromput la baixada i no es pot reprendre."</string>
diff --git a/ui/res/values-es/strings.xml b/ui/res/values-es/strings.xml
index 5c72460..37903dc 100644
--- a/ui/res/values-es/strings.xml
+++ b/ui/res/values-es/strings.xml
@@ -45,6 +45,6 @@
     <string name="retry_download" msgid="7617100787922717912">"Reintentar"</string>
     <string name="deselect_all" msgid="6348198946254776764">"Desmarcar todo"</string>
     <string name="select_all" msgid="634074918366265804">"Seleccionar todo"</string>
-    <string name="selected_count" msgid="2101564570019753277">"Has seleccionado <xliff:g id="NUMBER">%1$d</xliff:g> de <xliff:g id="TOTAL">%2$d</xliff:g>."</string>
+    <string name="selected_count" msgid="2101564570019753277">"Elegido: <xliff:g id="NUMBER">%1$d</xliff:g> de <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="download_share_dialog" msgid="3355867339806448955">"Compartir a través de"</string>
 </resources>
diff --git a/ui/res/values-fa/strings.xml b/ui/res/values-fa/strings.xml
index fd81dd2..8650ce3 100644
--- a/ui/res/values-fa/strings.xml
+++ b/ui/res/values-fa/strings.xml
@@ -21,7 +21,7 @@
     <string name="download_title_sorted_by_size" msgid="1417193166677094813">"دانلودها - مرتب شده بر اساس اندازه"</string>
     <string name="no_downloads" msgid="1029667411186146836">"خیر دانلودها."</string>
     <string name="missing_title" msgid="830115697868833773">"&lt;ناشناس&gt;"</string>
-    <string name="button_sort_by_size" msgid="7331549713691146251">"ترتیب بر اساس اندازه"</string>
+    <string name="button_sort_by_size" msgid="7331549713691146251">"بر اساس اندازه مرتب شود"</string>
     <string name="button_sort_by_date" msgid="8800842892684101528">"ترتیب براساس تاریخ"</string>
     <string name="download_queued" msgid="104973307780629904">"در صف"</string>
     <string name="download_running" msgid="4656462962155580641">"در حال انجام"</string>
diff --git a/ui/res/values-hi/strings.xml b/ui/res/values-hi/strings.xml
index 8e36dcc..4761bd3 100644
--- a/ui/res/values-hi/strings.xml
+++ b/ui/res/values-hi/strings.xml
@@ -46,5 +46,5 @@
     <string name="deselect_all" msgid="6348198946254776764">"सभी का चयन रद्द करें"</string>
     <string name="select_all" msgid="634074918366265804">"सभी चुनें"</string>
     <string name="selected_count" msgid="2101564570019753277">"<xliff:g id="TOTAL">%2$d</xliff:g> में से <xliff:g id="NUMBER">%1$d</xliff:g> चयनित"</string>
-    <string name="download_share_dialog" msgid="3355867339806448955">"इसके द्वारा शेयर करें"</string>
+    <string name="download_share_dialog" msgid="3355867339806448955">"इसके द्वारा साझा करें"</string>
 </resources>
diff --git a/ui/res/values/dimen.xml b/ui/res/values/dimen.xml
index 6e48f13..7519b87 100644
--- a/ui/res/values/dimen.xml
+++ b/ui/res/values/dimen.xml
@@ -15,5 +15,5 @@
 -->
 
 <resources>
-    <dimen name="checkmark_area">40dip</dimen>
+    <dimen name="checkmark_area">48dip</dimen>
 </resources>
diff --git a/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
index 19132a1..f5d7077 100644
--- a/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
+++ b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
@@ -273,7 +273,7 @@
         TextView item;
         if (null == convertView || !(convertView instanceof TextView)) {
             LayoutInflater factory = LayoutInflater.from(mContext);
-            item = (TextView) factory.inflate(R.layout.list_group_header, null);
+            item = (TextView) factory.inflate(R.layout.list_group_header, parent, false);
         } else {
             item = (TextView) convertView;
         }
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadItem.java b/ui/src/com/android/providers/downloads/ui/DownloadItem.java
index e24ac4a..0562cd0 100644
--- a/ui/src/com/android/providers/downloads/ui/DownloadItem.java
+++ b/ui/src/com/android/providers/downloads/ui/DownloadItem.java
@@ -18,12 +18,11 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.view.accessibility.AccessibilityEvent;
 import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.CheckBox;
 import android.widget.Checkable;
 import android.widget.GridLayout;
-import android.widget.RelativeLayout;
 
 /**
  * This class customizes RelativeLayout to directly handle clicks on the left part of the view and
@@ -83,12 +82,20 @@
         mDownloadList = downloadList;
     }
 
+    private boolean inCheckArea(MotionEvent event) {
+        if (isLayoutRtl()) {
+            return event.getX() > getWidth() - CHECKMARK_AREA;
+        } else {
+            return event.getX() < CHECKMARK_AREA;
+        }
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         boolean handled = false;
         switch(event.getAction()) {
             case MotionEvent.ACTION_DOWN:
-                if (event.getX() < CHECKMARK_AREA) {
+                if (inCheckArea(event)) {
                     mIsInDownEvent = true;
                     handled = true;
                 }
@@ -99,7 +106,7 @@
                 break;
 
             case MotionEvent.ACTION_UP:
-                if (mIsInDownEvent && event.getX() < CHECKMARK_AREA) {
+                if (mIsInDownEvent && inCheckArea(event)) {
                     toggle();
                     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                     handled = true;