]> nv-tegra.nvidia Code Review - android/platform/packages/providers/DownloadProvider.git/blobdiff - src/com/android/providers/downloads/DownloadService.java
Ack, we actually need to UpdateThread.quit().
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadService.java
index 169ef97065aac07eaef5763eed0bf5efdd41a5a0..c8e55d7d305d2858a1c631c05f453e015a53c741 100644 (file)
 
 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.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
 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.Environment;
 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;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.IndentingPrintWriter;
 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.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Collections;
+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 = true;
+
+    @VisibleForTesting
+    SystemFacade mSystemFacade;
+
+    private AlarmManager mAlarmManager;
+    private StorageManager mStorageManager;
 
     /** Observer to get notified when the content observer's data changes */
     private DownloadManagerContentObserver mObserver;
 
     /** Class to handle Notification Manager updates */
-    private DownloadNotification mNotifier;
+    private DownloadNotifier mNotifier;
 
     /**
      * The Service's view of the list of downloads, mapping download IDs to the corresponding info
@@ -70,115 +91,42 @@ public class DownloadService extends Service {
      * downloads based on this data, so that it can deal with situation where the data in the
      * content provider changes or disappears.
      */
-    private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
-
-    /**
-     * The thread that updates the internal download list from the content
-     * provider.
-     */
-    @VisibleForTesting
-    UpdateThread mUpdateThread;
-
-    /**
-     * Whether the internal download list should be updated from the content
-     * provider.
-     */
-    private boolean mPendingUpdate;
-
-    /**
-     * The ServiceConnection object that tells us when we're connected to and disconnected from
-     * the Media Scanner
-     */
-    private MediaScannerConnection mMediaScannerConnection;
+    @GuardedBy("mDownloads")
+    private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
+
+    private final ExecutorService mExecutor = buildDownloadExecutor();
+
+    private static ExecutorService buildDownloadExecutor() {
+        final int maxConcurrent = Resources.getSystem().getInteger(
+                com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
+
+        // 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 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();
         }
     }
 
@@ -188,6 +136,7 @@ public class DownloadService extends Service {
      *
      * @throws UnsupportedOperationException
      */
+    @Override
     public IBinder onBind(Intent i) {
         throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
     }
@@ -195,6 +144,7 @@ public class DownloadService extends Service {
     /**
      * Initializes the service when it is first created
      */
+    @Override
     public void onCreate() {
         super.onCreate();
         if (Constants.LOGVV) {
@@ -205,18 +155,21 @@ public class DownloadService extends Service {
             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);
+
+        mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
+        mUpdateThread.start();
+        mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
 
-        mMediaScannerService = null;
-        mMediaScannerConnecting = false;
-        mMediaScannerConnection = new MediaScannerConnection();
+        mScanner = new DownloadScanner(this);
 
-        mNotifier = new DownloadNotification(this, mSystemFacade);
-        mSystemFacade.cancelAllNotifications();
+        mNotifier = new DownloadNotifier(this);
+        mNotifier.cancelAll();
 
-        updateFromProvider();
+        mObserver = new DownloadManagerContentObserver();
+        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+                true, mObserver);
     }
 
     @Override
@@ -225,15 +178,16 @@ public class DownloadService extends Service {
         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");
         }
@@ -241,266 +195,181 @@ public class DownloadService extends Service {
     }
 
     /**
-     * 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");
-        }
-
-        public void run() {
-            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+    /**
+     * 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),
+                MINUTE_IN_MILLIS);
+    }
 
-            trimDatabase();
-            removeSpuriousFiles();
-
-            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");
-                    }
-                    if (!mPendingUpdate) {
-                        mUpdateThread = null;
-                        if (!keepService) {
-                            stopSelf();
-                        }
-                        if (wakeUp != Long.MAX_VALUE) {
-                            scheduleAlarm(wakeUp);
-                        }
-                        return;
-                    }
-                    mPendingUpdate = false;
-                }
+    private static final int MSG_UPDATE = 1;
+    private static final int MSG_FINAL_UPDATE = 2;
 
-                long now = mSystemFacade.currentTimeMillis();
-                boolean mustScan = false;
-                keepService = false;
-                wakeUp = Long.MAX_VALUE;
-                Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
+    private Handler.Callback mUpdateCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message msg) {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
 
-                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);
-
-                    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 = insertDownload(reader, now);
-                        }
-
-                        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();
-                }
+            final int startId = msg.arg1;
+            if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId);
 
-                for (Long id : idsNoLongerInDatabase) {
-                    deleteDownload(id);
-                }
+            // 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.
 
-                // 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.updateNotification(mDownloads.values());
-                if (mustScan) {
-                    bindMediaScanner();
-                } else {
-                    mMediaScannerConnection.disconnectMediaScanner();
-                }
+            // TODO: switch to asking real tasks to derive active state
+            // TODO: handle media scanner timeouts
 
-                // 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, true, false)) {
-                                    throw new IllegalStateException("scanFile failed!");
-                                }
-                            } else {
-                                // this file should NOT be scanned. delete the file.
-                                Helpers.deleteFile(getContentResolver(), info.mId, info.mFileName,
-                                        info.mMimeType);
-                            }
-                        } else {
-                            // yes it has mediaProviderUri column already filled in.
-                            // delete it from MediaProvider database and then from downloads table
-                            // in DownProvider database (the order of deletion is important).
-                            getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
-                                    null);
-                            getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                                    Downloads.Impl._ID + " = ? ",
-                                    new String[]{String.valueOf(info.mId)});
-                        }
-                    }
-                }
+            final boolean isActive;
+            synchronized (mDownloads) {
+                isActive = updateLocked();
             }
-        }
 
-        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);
+            if (msg.what == MSG_FINAL_UPDATE) {
+                Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
+                        + "; someone didn't update correctly.");
             }
-        }
 
-        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;
-            }
+            if (isActive) {
+                // Still doing useful work, keep service alive. These active
+                // tasks will trigger another update pass when they're finished.
 
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
+                // Enqueue delayed update pass to catch finished operations that
+                // didn't trigger an update pass; these are bugs.
+                enqueueFinalUpdate();
+
+            } 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.
+
+                if (stopSelfResult(startId)) {
+                    if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
+                    mUpdateThread.quit();
+                }
             }
 
-            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));
+            return true;
         }
-    }
+    };
 
     /**
-     * Removes files that may have been left behind in the cache directory
+     * 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 void removeSpuriousFiles() {
-        File[] files = Environment.getDownloadCacheDirectory().listFiles();
-        if (files == null) {
-            // The cache folder doesn't appear to exist (this is likely the case
-            // when running the simulator).
-            return;
-        }
-        HashSet<String> fileSet = new HashSet<String>();
-        for (int i = 0; i < files.length; i++) {
-            if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
-                continue;
-            }
-            if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
-                continue;
-            }
-            fileSet.add(files[i].getPath());
-        }
+    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);
 
-        Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                new String[] { Downloads.Impl._DATA }, null, null, null);
-        if (cursor != null) {
-            if (cursor.moveToFirst()) {
-                do {
-                    fileSet.remove(cursor.getString(0));
-                } while (cursor.moveToNext());
+                    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();
         }
-        Iterator<String> iterator = fileSet.iterator();
-        while (iterator.hasNext()) {
-            String filename = iterator.next();
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "deleting spurious file " + filename);
-            }
-            new File(filename).delete();
-        }
-    }
 
-    /**
-     * Drops old rows from the database to prevent it from growing too large
-     */
-    private void trimDatabase() {
-        Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
-                new String[] { Downloads.Impl._ID },
-                Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
-                Downloads.Impl.COLUMN_LAST_MODIFICATION);
-        if (cursor == null) {
-            // This isn't good - if we can't do basic queries in our database, nothing's gonna work
-            Log.e(Constants.TAG, "null cursor in trimDatabase");
-            return;
+        // Clean up stale downloads that disappeared
+        for (Long id : staleIds) {
+            deleteDownloadLocked(id);
         }
-        if (cursor.moveToFirst()) {
-            int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
-            int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
-            while (numDelete > 0) {
-                Uri downloadUri = ContentUris.withAppendedId(
-                        Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
-                getContentResolver().delete(downloadUri, null, null);
-                if (!cursor.moveToNext()) {
-                    break;
-                }
-                numDelete--;
+
+        // 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(TAG, "scheduling start in " + nextActionMillis + "ms");
             }
+
+            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));
         }
-        cursor.close();
+
+        return isActive;
     }
 
     /**
      * Keeps a local copy of the info about a download, and initiates the
      * download if appropriate.
      */
-    private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
-        DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
+    private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
+        final DownloadInfo info = reader.newDownloadInfo(
+                this, mSystemFacade, mStorageManager, mNotifier);
         mDownloads.put(info.mId, info);
 
         if (Constants.LOGVV) {
-            info.logVerboseInfo();
+            Log.v(Constants.TAG, "processing inserted download " + info.mId);
         }
 
-        info.startIfReady(now);
         return info;
     }
 
@@ -508,101 +377,51 @@ public class DownloadService extends Service {
      * 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);
-
-        boolean lostVisibility =
-                oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
-                && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
-                && Downloads.Impl.isStatusCompleted(info.mStatus);
-        boolean justCompleted =
-                !Downloads.Impl.isStatusCompleted(oldStatus)
-                && Downloads.Impl.isStatusCompleted(info.mStatus);
-        if (lostVisibility || justCompleted) {
-            mSystemFacade.cancelNotification(info.mId);
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "processing updated download " + info.mId +
+                    ", status: " + info.mStatus);
         }
-
-        info.startIfReady(now);
     }
 
     /**
      * Removes the local copy of the info about a download.
      */
-    private void deleteDownload(long id) {
+    private void deleteDownloadLocked(long id) {
         DownloadInfo info = mDownloads.get(id);
-        if (info.shouldScanFile()) {
-            scanFile(info, false, false);
-        }
         if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
             info.mStatus = Downloads.Impl.STATUS_CANCELED;
         }
         if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
-            new File(info.mFileName).delete();
+            if (Constants.LOGVV) {
+                Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
+            }
+            deleteFileIfExists(info.mFileName);
         }
-        mSystemFacade.cancelNotification(info.mId);
         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;
+    private void deleteFileIfExists(String path) {
+        if (!TextUtils.isEmpty(path)) {
+            if (Constants.LOGVV) {
+                Log.d(TAG, "deleteFileIfExists() deleting " + path);
             }
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "Scanning file " + info.mFileName);
+            final File file = new File(path);
+            if (file.exists() && !file.delete()) {
+                Log.w(TAG, "file: '" + path + "' couldn't be deleted");
             }
-            try {
-                final Uri key = info.getAllDownloadsUri();
-                final String mimeType = info.mMimeType;
-                final ContentResolver resolver = getContentResolver();
-                final long id = info.mId;
-                mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
-                        new IMediaScannerListener.Stub() {
-                            public void scanCompleted(String path, Uri uri) {
-                                if (uri != null && updateDatabase) {
-                                    // file is scanned and mediaprovider returned uri. store it in downloads
-                                    // table (i.e., update this downloaded file's row)
-                                    ContentValues values = new ContentValues();
-                                    values.put(Constants.MEDIA_SCANNED, 1);
-                                    values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
-                                            uri.toString());
-                                    getContentResolver().update(key, values, null, null);
-                                } else if (uri == null && deleteFile) {
-                                    // callback returned NO uri..that means this file doesn't
-                                    // exist in MediaProvider. but it still needs to be deleted
-                                    // TODO don't scan files that are not scannable by MediaScanner.
-                                    //      create a public method in MediaFile.java to return false
-                                    //      if the given file's mimetype is not any of the types
-                                    //      the mediaprovider is interested in.
-                                    Helpers.deleteFile(resolver, id, path, mimeType);
-                                }
-                            }
-                        });
-                return true;
-            } catch (RemoteException e) {
-                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
-                return false;
+        }
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        synchronized (mDownloads) {
+            final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
+            Collections.sort(ids);
+            for (Long id : ids) {
+                final DownloadInfo info = mDownloads.get(id);
+                info.dump(pw);
             }
         }
     }