(GB/GBMR) (do not merge) delete file from disk when deleting from db
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadService.java
index 2e713fb..95d07d6 100644 (file)
 
 package com.android.providers.downloads;
 
-import com.google.android.collect.Lists;
-import com.google.common.annotations.VisibleForTesting;
-
 import android.app.AlarmManager;
 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.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.database.CharArrayBuffer;
 import android.database.ContentObserver;
 import android.database.Cursor;
-import android.drm.mobile1.DrmRawContent;
+import android.media.IMediaScannerListener;
 import android.media.IMediaScannerService;
 import android.net.Uri;
 import android.os.Environment;
@@ -42,24 +37,26 @@ import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
 import android.provider.Downloads;
-import android.util.Config;
+import android.text.TextUtils;
 import android.util.Log;
 
+import com.google.android.collect.Maps;
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.File;
-import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 
 /**
  * Performs the background downloads requested by applications that use the Downloads provider.
  */
 public class DownloadService extends Service {
-
-    /* ------------ Constants ------------ */
-
-    /* ------------ Members ------------ */
+    /** amount of time to wait to connect to MediaScannerService before timing out */
+    private static final long WAIT_TIMEOUT = 10 * 1000;
 
     /** Observer to get notified when the content observer's data changes */
     private DownloadManagerContentObserver mObserver;
@@ -68,18 +65,19 @@ public class DownloadService extends Service {
     private DownloadNotification mNotifier;
 
     /**
-     * The Service's view of the list of downloads. This is kept independently
-     * from the content provider, and the Service only initiates downloads
-     * based on this data, so that it can deal with situation where the data
-     * in the content provider changes or disappears.
+     * The Service's view of the list of downloads, mapping download IDs to the corresponding info
+     * object. This is kept independently from the content provider, and the Service only initiates
+     * downloads based on this data, so that it can deal with situation where the data in the
+     * content provider changes or disappears.
      */
-    private ArrayList<DownloadInfo> mDownloads;
+    private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
 
     /**
      * The thread that updates the internal download list from the content
      * provider.
      */
-    private UpdateThread mUpdateThread;
+    @VisibleForTesting
+    UpdateThread mUpdateThread;
 
     /**
      * Whether the internal download list should be updated from the content
@@ -100,21 +98,9 @@ public class DownloadService extends Service {
      */
     private IMediaScannerService mMediaScannerService;
 
-    /**
-     * Array used when extracting strings from content provider
-     */
-    private CharArrayBuffer oldChars;
-
-    /**
-     * Array used when extracting strings from content provider
-     */
-    private CharArrayBuffer mNewChars;
-
     @VisibleForTesting
     SystemFacade mSystemFacade;
 
-    /* ------------ Inner Classes ------------ */
-
     /**
      * Receives notifications when the data in the content provider changes
      */
@@ -146,17 +132,23 @@ public class DownloadService extends Service {
             if (Constants.LOGVV) {
                 Log.v(Constants.TAG, "Connected to Media Scanner");
             }
-            mMediaScannerConnecting = false;
             synchronized (DownloadService.this) {
-                mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
-                if (mMediaScannerService != null) {
-                    updateFromProvider();
+                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) {
@@ -165,26 +157,31 @@ public class DownloadService extends Service {
                     try {
                         unbindService(this);
                     } catch (IllegalArgumentException ex) {
-                        if (Constants.LOGV) {
-                            Log.v(Constants.TAG, "unbindService threw up: " + 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) {
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "Disconnected from Media Scanner");
-            }
-            synchronized (DownloadService.this) {
-                mMediaScannerService = null;
+            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();
+                }
             }
         }
     }
 
-    /* ------------ Methods ------------ */
-
     /**
      * Returns an IBinder instance when someone wants to connect to this
      * service. Binding to this service is not allowed.
@@ -205,38 +202,31 @@ public class DownloadService extends Service {
         }
 
         if (mSystemFacade == null) {
-            mSystemFacade = new RealSystemFacade();
+            mSystemFacade = new RealSystemFacade(this);
         }
 
-        mDownloads = Lists.newArrayList();
-
         mObserver = new DownloadManagerContentObserver();
-        getContentResolver().registerContentObserver(Downloads.Impl.CONTENT_URI,
+        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                 true, mObserver);
 
         mMediaScannerService = null;
         mMediaScannerConnecting = false;
         mMediaScannerConnection = new MediaScannerConnection();
 
-        mNotifier = new DownloadNotification(this);
-        mNotifier.mNotificationMgr.cancelAll();
-        mNotifier.updateNotification();
+        mNotifier = new DownloadNotification(this, mSystemFacade);
+        mSystemFacade.cancelAllNotifications();
 
-        trimDatabase();
-        removeSpuriousFiles();
         updateFromProvider();
     }
 
-    /**
-     * Responds to a call to startService
-     */
-    public void onStart(Intent intent, int startId) {
-        super.onStart(intent, startId);
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        int returnValue = super.onStartCommand(intent, flags, startId);
         if (Constants.LOGVV) {
             Log.v(Constants.TAG, "Service onStart");
         }
-
         updateFromProvider();
+        return returnValue;
     }
 
     /**
@@ -258,7 +248,7 @@ public class DownloadService extends Service {
             mPendingUpdate = true;
             if (mUpdateThread == null) {
                 mUpdateThread = new UpdateThread();
-                mUpdateThread.start();
+                mSystemFacade.startThread(mUpdateThread);
             }
         }
     }
@@ -271,6 +261,9 @@ public class DownloadService extends Service {
         public void run() {
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
 
+            trimDatabase();
+            removeSpuriousFiles();
+
             boolean keepService = false;
             // for each update from the database, remember which download is
             // supposed to get restarted soonest in the future
@@ -287,194 +280,141 @@ public class DownloadService extends Service {
                             stopSelf();
                         }
                         if (wakeUp != Long.MAX_VALUE) {
-                            AlarmManager alarms =
-                                    (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-                            if (alarms == null) {
-                                Log.e(Constants.TAG, "couldn't get alarm manager");
-                            } else {
-                                if (Constants.LOGV) {
-                                    Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "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));
-                            }
+                            scheduleAlarm(wakeUp);
                         }
-                        oldChars = null;
-                        mNewChars = null;
                         return;
                     }
                     mPendingUpdate = false;
                 }
-                boolean networkAvailable = Helpers.isNetworkAvailable(DownloadService.this);
-                boolean networkRoaming = Helpers.isNetworkRoaming(DownloadService.this);
-                long now = mSystemFacade.currentTimeMillis();
 
-                Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
-                        null, null, null, Downloads.Impl._ID);
+                long now = mSystemFacade.currentTimeMillis();
+                boolean mustScan = false;
+                keepService = false;
+                wakeUp = Long.MAX_VALUE;
+                Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
 
+                Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+                        null, null, null, null);
                 if (cursor == null) {
-                    // TODO: this doesn't look right, it'd leave the loop in an inconsistent state
-                    return;
+                    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);
+                        }
 
-                cursor.moveToFirst();
-
-                int arrayPos = 0;
+                        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();
+                }
 
-                boolean mustScan = false;
-                keepService = false;
-                wakeUp = Long.MAX_VALUE;
+                for (Long id : idsNoLongerInDatabase) {
+                    deleteDownload(id);
+                }
 
-                boolean isAfterLast = cursor.isAfterLast();
-
-                int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
-
-                /*
-                 * Walk the cursor and the local array to keep them in sync. The key
-                 *     to the algorithm is that the ids are unique and sorted both in
-                 *     the cursor and in the array, so that they can be processed in
-                 *     order in both sources at the same time: at each step, both
-                 *     sources point to the lowest id that hasn't been processed from
-                 *     that source, and the algorithm processes the lowest id from
-                 *     those two possibilities.
-                 * At each step:
-                 * -If the array contains an entry that's not in the cursor, remove the
-                 *     entry, move to next entry in the array.
-                 * -If the array contains an entry that's in the cursor, nothing to do,
-                 *     move to next cursor row and next array entry.
-                 * -If the cursor contains an entry that's not in the array, insert
-                 *     a new entry in the array, move to next cursor row and next
-                 *     array entry.
-                 */
-                while (!isAfterLast || arrayPos < mDownloads.size()) {
-                    if (isAfterLast) {
-                        // We're beyond the end of the cursor but there's still some
-                        //     stuff in the local array, which can only be junk
-                        if (Constants.LOGVV) {
-                            int arrayId = ((DownloadInfo) mDownloads.get(arrayPos)).mId;
-                            Log.v(Constants.TAG, "Array update: trimming " +
-                                    arrayId + " @ "  + arrayPos);
-                        }
-                        if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
-                            scanFile(null, arrayPos);
+                // 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;
                         }
-                        deleteDownload(arrayPos); // this advances in the array
-                    } else {
-                        int id = cursor.getInt(idColumn);
-
-                        if (arrayPos == mDownloads.size()) {
-                            insertDownload(cursor, arrayPos, networkAvailable, networkRoaming, now);
-                            if (Constants.LOGVV) {
-                                Log.v(Constants.TAG, "Array update: appending " +
-                                        id + " @ " + arrayPos);
-                            }
-                            if (shouldScanFile(arrayPos)
-                                    && (!mediaScannerConnected() || !scanFile(cursor, arrayPos))) {
-                                mustScan = true;
-                                keepService = true;
-                            }
-                            if (visibleNotification(arrayPos)) {
-                                keepService = true;
-                            }
-                            long next = nextAction(arrayPos, now);
-                            if (next == 0) {
-                                keepService = true;
-                            } else if (next > 0 && next < wakeUp) {
-                                wakeUp = next;
-                            }
-                            ++arrayPos;
-                            cursor.moveToNext();
-                            isAfterLast = cursor.isAfterLast();
-                        } else {
-                            int arrayId = mDownloads.get(arrayPos).mId;
+                    }
+                }
+                mNotifier.updateNotification(mDownloads.values());
+                if (mustScan) {
+                    bindMediaScanner();
+                } else {
+                    mMediaScannerConnection.disconnectMediaScanner();
+                }
 
-                            if (arrayId < id) {
-                                // The array entry isn't in the cursor
-                                if (Constants.LOGVV) {
-                                    Log.v(Constants.TAG, "Array update: removing " + arrayId
-                                            + " @ " + arrayPos);
-                                }
-                                if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
-                                    scanFile(null, arrayPos);
-                                }
-                                deleteDownload(arrayPos); // this advances in the array
-                            } else if (arrayId == id) {
-                                // This cursor row already exists in the stored array
-                                updateDownload(
-                                        cursor, arrayPos,
-                                        networkAvailable, networkRoaming, now);
-                                if (shouldScanFile(arrayPos)
-                                        && (!mediaScannerConnected()
-                                                || !scanFile(cursor, arrayPos))) {
-                                    mustScan = true;
-                                    keepService = true;
+                // 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!");
                                 }
-                                if (visibleNotification(arrayPos)) {
-                                    keepService = true;
-                                }
-                                long next = nextAction(arrayPos, now);
-                                if (next == 0) {
-                                    keepService = true;
-                                } else if (next > 0 && next < wakeUp) {
-                                    wakeUp = next;
-                                }
-                                ++arrayPos;
-                                cursor.moveToNext();
-                                isAfterLast = cursor.isAfterLast();
                             } else {
-                                // This cursor entry didn't exist in the stored array
-                                if (Constants.LOGVV) {
-                                    Log.v(Constants.TAG, "Array update: inserting " +
-                                            id + " @ " + arrayPos);
-                                }
-                                insertDownload(
-                                        cursor, arrayPos,
-                                        networkAvailable, networkRoaming, now);
-                                if (shouldScanFile(arrayPos)
-                                        && (!mediaScannerConnected()
-                                                || !scanFile(cursor, arrayPos))) {
-                                    mustScan = true;
-                                    keepService = true;
-                                }
-                                if (visibleNotification(arrayPos)) {
-                                    keepService = true;
-                                }
-                                long next = nextAction(arrayPos, now);
-                                if (next == 0) {
-                                    keepService = true;
-                                } else if (next > 0 && next < wakeUp) {
-                                    wakeUp = next;
-                                }
-                                ++arrayPos;
-                                cursor.moveToNext();
-                                isAfterLast = cursor.isAfterLast();
+                                // 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);
+                            // the following deletes the file and then deletes it from downloads db
+                            Helpers.deleteFile(getContentResolver(), info.mId, info.mFileName,
+                                    info.mMimeType);
                         }
                     }
                 }
+            }
+        }
 
-                mNotifier.updateNotification();
+        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 (mustScan) {
-                    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);
-                    }
-                } else {
-                    mMediaScannerConnection.disconnectMediaScanner();
-                }
+        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;
+            }
 
-                cursor.close();
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "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));
         }
     }
 
@@ -488,7 +428,7 @@ public class DownloadService extends Service {
             // when running the simulator).
             return;
         }
-        HashSet<String> fileSet = new HashSet();
+        HashSet<String> fileSet = new HashSet<String>();
         for (int i = 0; i < files.length; i++) {
             if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
                 continue;
@@ -499,7 +439,7 @@ public class DownloadService extends Service {
             fileSet.add(files[i].getPath());
         }
 
-        Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+        Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                 new String[] { Downloads.Impl._DATA }, null, null, null);
         if (cursor != null) {
             if (cursor.moveToFirst()) {
@@ -523,7 +463,7 @@ public class DownloadService extends Service {
      * Drops old rows from the database to prevent it from growing too large
      */
     private void trimDatabase() {
-        Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+        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);
@@ -536,9 +476,9 @@ public class DownloadService extends Service {
             int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
             int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
             while (numDelete > 0) {
-                getContentResolver().delete(
-                        ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI,
-                        cursor.getLong(columnId)), null, null);
+                Uri downloadUri = ContentUris.withAppendedId(
+                        Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
+                getContentResolver().delete(downloadUri, null, null);
                 if (!cursor.moveToNext()) {
                     break;
                 }
@@ -552,320 +492,120 @@ public class DownloadService extends Service {
      * Keeps a local copy of the info about a download, and initiates the
      * download if appropriate.
      */
-    private void insertDownload(
-            Cursor cursor, int arrayPos,
-            boolean networkAvailable, boolean networkRoaming, long now) {
-        DownloadInfo info = new DownloadInfo(getContentResolver(), cursor);
+    private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
+        DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
+        mDownloads.put(info.mId, info);
 
         if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "Service adding new entry");
-            Log.v(Constants.TAG, "ID      : " + info.mId);
-            Log.v(Constants.TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
-            Log.v(Constants.TAG, "NO_INTEG: " + info.mNoIntegrity);
-            Log.v(Constants.TAG, "HINT    : " + info.mHint);
-            Log.v(Constants.TAG, "FILENAME: " + info.mFileName);
-            Log.v(Constants.TAG, "MIMETYPE: " + info.mMimeType);
-            Log.v(Constants.TAG, "DESTINAT: " + info.mDestination);
-            Log.v(Constants.TAG, "VISIBILI: " + info.mVisibility);
-            Log.v(Constants.TAG, "CONTROL : " + info.mControl);
-            Log.v(Constants.TAG, "STATUS  : " + info.mStatus);
-            Log.v(Constants.TAG, "FAILED_C: " + info.mNumFailed);
-            Log.v(Constants.TAG, "RETRY_AF: " + info.mRetryAfter);
-            Log.v(Constants.TAG, "REDIRECT: " + info.mRedirectCount);
-            Log.v(Constants.TAG, "LAST_MOD: " + info.mLastMod);
-            Log.v(Constants.TAG, "PACKAGE : " + info.mPackage);
-            Log.v(Constants.TAG, "CLASS   : " + info.mClass);
-            Log.v(Constants.TAG, "COOKIES : " + ((info.mCookies != null) ? "yes" : "no"));
-            Log.v(Constants.TAG, "AGENT   : " + info.mUserAgent);
-            Log.v(Constants.TAG, "REFERER : " + ((info.mReferer != null) ? "yes" : "no"));
-            Log.v(Constants.TAG, "TOTAL   : " + info.mTotalBytes);
-            Log.v(Constants.TAG, "CURRENT : " + info.mCurrentBytes);
-            Log.v(Constants.TAG, "ETAG    : " + info.mETag);
-            Log.v(Constants.TAG, "SCANNED : " + info.mMediaScanned);
+            info.logVerboseInfo();
         }
 
-        mDownloads.add(arrayPos, info);
-
-        if (info.mStatus == 0
-                && (info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
-                    || info.mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE)
-                && info.mMimeType != null
-                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType)) {
-            // 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 mimetypeIntent = 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
-
-            mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mMimeType);
-            ResolveInfo ri = getPackageManager().resolveActivity(mimetypeIntent,
-                    PackageManager.MATCH_DEFAULT_ONLY);
-            //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
-
-            if (ri == null) {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
-                }
-                info.mStatus = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
-
-                Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
-                ContentValues values = new ContentValues();
-                values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
-                getContentResolver().update(uri, values, null, null);
-                info.sendIntentIfRequested(uri, this);
-                return;
-            }
-        }
-
-        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
-            if (info.isReadyToStart(now)) {
-                if (Constants.LOGV) {
-                    Log.v(Constants.TAG, "Service spawning thread to handle new download " +
-                            info.mId);
-                }
-                if (info.mHasActiveThread) {
-                    throw new IllegalStateException("Multiple threads on same download on insert");
-                }
-                if (info.mStatus != Downloads.Impl.STATUS_RUNNING) {
-                    info.mStatus = Downloads.Impl.STATUS_RUNNING;
-                    ContentValues values = new ContentValues();
-                    values.put(Downloads.Impl.COLUMN_STATUS, info.mStatus);
-                    getContentResolver().update(
-                            ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId),
-                            values, null, null);
-                }
-                DownloadThread downloader = new DownloadThread(this, mSystemFacade, info);
-                info.mHasActiveThread = true;
-                downloader.start();
-            }
-        } else {
-            if (info.mStatus == 0
-                    || info.mStatus == Downloads.Impl.STATUS_PENDING
-                    || info.mStatus == Downloads.Impl.STATUS_RUNNING) {
-                info.mStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
-                Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
-                ContentValues values = new ContentValues();
-                values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_RUNNING_PAUSED);
-                getContentResolver().update(uri, values, null, null);
-            }
-        }
+        info.startIfReady(now);
+        return info;
     }
 
     /**
      * Updates the local copy of the info about a download.
      */
-    private void updateDownload(
-            Cursor cursor, int arrayPos,
-            boolean networkAvailable, boolean networkRoaming, long now) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
-        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
-        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
-        info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
-        info.mUri = stringFromCursor(info.mUri, cursor, Downloads.Impl.COLUMN_URI);
-        info.mNoIntegrity = cursor.getInt(cursor.getColumnIndexOrThrow(
-                Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1;
-        info.mHint = stringFromCursor(info.mHint, cursor, Downloads.Impl.COLUMN_FILE_NAME_HINT);
-        info.mFileName = stringFromCursor(info.mFileName, cursor, Downloads.Impl._DATA);
-        info.mMimeType = stringFromCursor(info.mMimeType, cursor, Downloads.Impl.COLUMN_MIME_TYPE);
-        info.mDestination = cursor.getInt(cursor.getColumnIndexOrThrow(
-                Downloads.Impl.COLUMN_DESTINATION));
-        int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(
-                Downloads.Impl.COLUMN_VISIBILITY));
-        if (info.mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
-                && newVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
-                && Downloads.Impl.isStatusCompleted(info.mStatus)) {
-            mNotifier.mNotificationMgr.cancel(info.mId);
-        }
-        info.mVisibility = newVisibility;
-        synchronized (info) {
-            info.mControl = cursor.getInt(cursor.getColumnIndexOrThrow(
-                    Downloads.Impl.COLUMN_CONTROL));
-        }
-        int newStatus = cursor.getInt(statusColumn);
-        if (!Downloads.Impl.isStatusCompleted(info.mStatus) &&
-                    Downloads.Impl.isStatusCompleted(newStatus)) {
-            mNotifier.mNotificationMgr.cancel(info.mId);
-        }
-        info.mStatus = newStatus;
-        info.mNumFailed = cursor.getInt(failedColumn);
-        int retryRedirect =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
-        info.mRetryAfter = retryRedirect & 0xfffffff;
-        info.mRedirectCount = retryRedirect >> 28;
-        info.mLastMod = cursor.getLong(cursor.getColumnIndexOrThrow(
-                Downloads.Impl.COLUMN_LAST_MODIFICATION));
-        info.mPackage = stringFromCursor(
-                info.mPackage, cursor, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
-        info.mClass = stringFromCursor(
-                info.mClass, cursor, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
-        info.mCookies = stringFromCursor(info.mCookies, cursor, Downloads.Impl.COLUMN_COOKIE_DATA);
-        info.mUserAgent = stringFromCursor(
-                info.mUserAgent, cursor, Downloads.Impl.COLUMN_USER_AGENT);
-        info.mReferer = stringFromCursor(info.mReferer, cursor, Downloads.Impl.COLUMN_REFERER);
-        info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
-                Downloads.Impl.COLUMN_TOTAL_BYTES));
-        info.mCurrentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(
-                Downloads.Impl.COLUMN_CURRENT_BYTES));
-        info.mETag = stringFromCursor(info.mETag, cursor, Constants.ETAG);
-        info.mMediaScanned =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
-
-        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
-            if (info.isReadyToRestart(now)) {
-                if (Constants.LOGV) {
-                    Log.v(Constants.TAG, "Service spawning thread to handle updated download " +
-                            info.mId);
-                }
-                if (info.mHasActiveThread) {
-                    throw new IllegalStateException("Multiple threads on same download on update");
-                }
-                info.mStatus = Downloads.Impl.STATUS_RUNNING;
-                ContentValues values = new ContentValues();
-                values.put(Downloads.Impl.COLUMN_STATUS, info.mStatus);
-                getContentResolver().update(
-                        ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId),
-                        values, null, null);
-                DownloadThread downloader = new DownloadThread(this, mSystemFacade, info);
-                info.mHasActiveThread = true;
-                downloader.start();
-            }
-        }
-    }
+    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
+        int oldVisibility = info.mVisibility;
+        int oldStatus = info.mStatus;
 
-    /**
-     * Returns a String that holds the current value of the column,
-     * optimizing for the case where the value hasn't changed.
-     */
-    private String stringFromCursor(String old, Cursor cursor, String column) {
-        int index = cursor.getColumnIndexOrThrow(column);
-        if (old == null) {
-            return cursor.getString(index);
-        }
-        if (mNewChars == null) {
-            mNewChars = new CharArrayBuffer(128);
-        }
-        cursor.copyStringToBuffer(index, mNewChars);
-        int length = mNewChars.sizeCopied;
-        if (length != old.length()) {
-            return cursor.getString(index);
-        }
-        if (oldChars == null || oldChars.sizeCopied < length) {
-            oldChars = new CharArrayBuffer(length);
-        }
-        char[] oldArray = oldChars.data;
-        char[] newArray = mNewChars.data;
-        old.getChars(0, length, oldArray, 0);
-        for (int i = length - 1; i >= 0; --i) {
-            if (oldArray[i] != newArray[i]) {
-                return new String(newArray, 0, length);
-            }
-        }
-        return old;
-    }
+        reader.updateFromDatabase(info);
 
-    /**
-     * Removes the local copy of the info about a download.
-     */
-    private void deleteDownload(int arrayPos) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
-        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
-            info.mStatus = Downloads.Impl.STATUS_CANCELED;
-        } else if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL
-                    && info.mFileName != null) {
-            new File(info.mFileName).delete();
+        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);
         }
-        mNotifier.mNotificationMgr.cancel(info.mId);
 
-        mDownloads.remove(arrayPos);
+        info.startIfReady(now);
     }
 
     /**
-     * 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"
+     * Removes the local copy of the info about a download.
      */
-    private long nextAction(int arrayPos, long now) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
-        if (Downloads.Impl.isStatusCompleted(info.mStatus)) {
-            return -1;
+    private void deleteDownload(long id) {
+        DownloadInfo info = mDownloads.get(id);
+        if (info.shouldScanFile()) {
+            scanFile(info, false, false);
         }
-        if (info.mStatus != Downloads.Impl.STATUS_RUNNING_PAUSED) {
-            return 0;
-        }
-        if (info.mNumFailed == 0) {
-            return 0;
+        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
+            info.mStatus = Downloads.Impl.STATUS_CANCELED;
         }
-        long when = info.restartTime();
-        if (when <= now) {
-            return 0;
+        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
+            new File(info.mFileName).delete();
         }
-        return when - now;
-    }
-
-    /**
-     * Returns whether there's a visible notification for this download
-     */
-    private boolean visibleNotification(int arrayPos) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
-        return info.hasCompletionNotification();
-    }
-
-    /**
-     * Returns whether a file should be scanned
-     */
-    private boolean shouldScanFile(int arrayPos) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
-        return !info.mMediaScanned
-                && info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
-                && Downloads.Impl.isStatusSuccess(info.mStatus)
-                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mMimeType);
-    }
-
-    /**
-     * Returns whether we have a live connection to the Media Scanner
-     */
-    private boolean mediaScannerConnected() {
-        return mMediaScannerService != null;
+        mSystemFacade.cancelNotification(info.mId);
+        mDownloads.remove(info.mId);
     }
 
     /**
      * Attempts to scan the file if necessary.
-     * Returns true if the file has been properly scanned.
+     * @return true if the file has been properly scanned.
      */
-    private boolean scanFile(Cursor cursor, int arrayPos) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+    private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
+            final boolean deleteFile) {
         synchronized (this) {
-            if (mMediaScannerService != null) {
-                try {
-                    if (Constants.LOGV) {
-                        Log.v(Constants.TAG, "Scanning file " + info.mFileName);
-                    }
-                    mMediaScannerService.scanFile(info.mFileName, info.mMimeType);
-                    if (cursor != null) {
-                        ContentValues values = new ContentValues();
-                        values.put(Constants.MEDIA_SCANNED, 1);
-                        getContentResolver().update(ContentUris.withAppendedId(
-                                       Downloads.Impl.CONTENT_URI, cursor.getLong(
-                                               cursor.getColumnIndexOrThrow(Downloads.Impl._ID))),
-                                values, null, null);
-                    }
-                    return true;
-                } catch (RemoteException e) {
-                    if (Config.LOGD) {
-                        Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
+            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 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 (updateDatabase) {
+                                    // Mark this as 'scanned' in the database
+                                    // so that it is NOT subject to re-scanning by MediaScanner
+                                    // next time this database 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
+                                    Helpers.deleteFile(resolver, id, path, mimeType);
+                                }
+                            }
+                        });
+                return true;
+            } catch (RemoteException e) {
+                Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
+                return false;
+            }
         }
-        return false;
     }
-
 }