Fix notification bugs, cleanup DownloadService + DownloadReceiver
Steve Howard [Wed, 15 Sep 2010 19:29:50 +0000 (12:29 -0700)]
This change started out just fixing a few regressions related to
notifications:

* Browser downloads weren't picking up a title from the determined
  filename.  This was due to my change to default the title field to
  "" instead of null.

* Notification click/hide events weren't being handled properly.  This
  was due to previous change to the URI structure of DownloadProvider.
  DownloadReceiver needed to be changed to perform queries through
  /all_downloads URIs, like all other parts of the download manager
  code.  I did some general refactoring of the DownloadReceiver code
  while I was there.

* The code in DownloadNotification wasn't picking up some updates to
  downloads properly.  This was due to my change to make
  DownloadNotification use the DownloadInfo objects rather than
  querying the database directly, so that it could make use of info
  provided by the DownloadThread that didn't go into the DB.  Fixing
  this didn't turn out to be all that complicated, but along the way
  to figuring this out I made some substantial refactoring in
  DownloadService which made it much cleaner anyway and eliminated a
  lot of duplication.  That's something that had to happen eventually,
  so I'm leaving it all in.

Change-Id: I847ccf80e3d928c84e36bc24791b33204104e1b2

src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadNotification.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadReceiver.java
src/com/android/providers/downloads/DownloadService.java
src/com/android/providers/downloads/RealSystemFacade.java
src/com/android/providers/downloads/SystemFacade.java
tests/src/com/android/providers/downloads/FakeSystemFacade.java
ui/src/com/android/providers/downloads/ui/DownloadList.java

index 0cf025b..eb9ac4b 100644 (file)
 
 package com.android.providers.downloads;
 
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.database.CharArrayBuffer;
 import android.database.Cursor;
+import android.drm.mobile1.DrmRawContent;
 import android.net.ConnectivityManager;
 import android.net.DownloadManager;
 import android.net.Uri;
@@ -36,7 +39,127 @@ import java.util.Map;
  * Stores information about an individual download.
  */
 public class DownloadInfo {
-    public int mId;
+    public static class Reader {
+        private ContentResolver mResolver;
+        private Cursor mCursor;
+        private CharArrayBuffer mOldChars;
+        private CharArrayBuffer mNewChars;
+
+        public Reader(ContentResolver resolver, Cursor cursor) {
+            mResolver = resolver;
+            mCursor = cursor;
+        }
+
+        public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade) {
+            DownloadInfo info = new DownloadInfo(context, systemFacade);
+            updateFromDatabase(info);
+            readRequestHeaders(info);
+            return info;
+        }
+
+        public void updateFromDatabase(DownloadInfo info) {
+            info.mId = getLong(Downloads.Impl._ID);
+            info.mUri = getString(info.mUri, Downloads.Impl.COLUMN_URI);
+            info.mNoIntegrity = getInt(Downloads.Impl.COLUMN_NO_INTEGRITY) == 1;
+            info.mHint = getString(info.mHint, Downloads.Impl.COLUMN_FILE_NAME_HINT);
+            info.mFileName = getString(info.mFileName, Downloads.Impl._DATA);
+            info.mMimeType = getString(info.mMimeType, Downloads.Impl.COLUMN_MIME_TYPE);
+            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);
+            int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT);
+            info.mRetryAfter = retryRedirect & 0xfffffff;
+            info.mRedirectCount = retryRedirect >> 28;
+            info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION);
+            info.mPackage = getString(info.mPackage, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
+            info.mClass = getString(info.mClass, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
+            info.mExtras = getString(info.mExtras, Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
+            info.mCookies = getString(info.mCookies, Downloads.Impl.COLUMN_COOKIE_DATA);
+            info.mUserAgent = getString(info.mUserAgent, Downloads.Impl.COLUMN_USER_AGENT);
+            info.mReferer = getString(info.mReferer, Downloads.Impl.COLUMN_REFERER);
+            info.mTotalBytes = getLong(Downloads.Impl.COLUMN_TOTAL_BYTES);
+            info.mCurrentBytes = getLong(Downloads.Impl.COLUMN_CURRENT_BYTES);
+            info.mETag = getString(info.mETag, Constants.ETAG);
+            info.mMediaScanned = getInt(Constants.MEDIA_SCANNED) == 1;
+            info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
+            info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
+            info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
+            info.mTitle = getString(info.mTitle, Downloads.Impl.COLUMN_TITLE);
+            info.mDescription = getString(info.mDescription, Downloads.Impl.COLUMN_DESCRIPTION);
+
+            synchronized (this) {
+                info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL);
+            }
+        }
+
+        private void readRequestHeaders(DownloadInfo info) {
+            info.mRequestHeaders.clear();
+            Uri headerUri = Uri.withAppendedPath(
+                    info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT);
+            Cursor cursor = mResolver.query(headerUri, null, null, null, null);
+            try {
+                int headerIndex =
+                        cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER);
+                int valueIndex =
+                        cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE);
+                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                    info.mRequestHeaders.put(
+                            cursor.getString(headerIndex), cursor.getString(valueIndex));
+                }
+            } finally {
+                cursor.close();
+            }
+
+            if (info.mCookies != null) {
+                info.mRequestHeaders.put("Cookie", info.mCookies);
+            }
+            if (info.mReferer != null) {
+                info.mRequestHeaders.put("Referer", info.mReferer);
+            }
+        }
+
+        /**
+         * Returns a String that holds the current value of the column, optimizing for the case
+         * where the value hasn't changed.
+         */
+        private String getString(String old, String column) {
+            int index = mCursor.getColumnIndexOrThrow(column);
+            if (old == null) {
+                return mCursor.getString(index);
+            }
+            if (mNewChars == null) {
+                mNewChars = new CharArrayBuffer(128);
+            }
+            mCursor.copyStringToBuffer(index, mNewChars);
+            int length = mNewChars.sizeCopied;
+            if (length != old.length()) {
+                return new String(mNewChars.data, 0, length);
+            }
+            if (mOldChars == null || mOldChars.sizeCopied < length) {
+                mOldChars = new CharArrayBuffer(length);
+            }
+            char[] oldArray = mOldChars.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;
+        }
+
+        private Integer getInt(String column) {
+            return mCursor.getInt(mCursor.getColumnIndexOrThrow(column));
+        }
+
+        private Long getLong(String column) {
+            return mCursor.getLong(mCursor.getColumnIndexOrThrow(column));
+        }
+    }
+
+    public long mId;
     public String mUri;
     public boolean mNoIntegrity;
     public String mHint;
@@ -75,83 +198,10 @@ public class DownloadInfo {
     private SystemFacade mSystemFacade;
     private Context mContext;
 
-    public DownloadInfo(Context context, SystemFacade systemFacade, Cursor cursor) {
+    private DownloadInfo(Context context, SystemFacade systemFacade) {
         mContext = context;
         mSystemFacade = systemFacade;
-
-        int retryRedirect =
-            cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
-        mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
-        mUri = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_URI));
-        mNoIntegrity = cursor.getInt(cursor.getColumnIndexOrThrow(
-                                        Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1;
-        mHint = cursor.getString(cursor.getColumnIndexOrThrow(
-                                        Downloads.Impl.COLUMN_FILE_NAME_HINT));
-        mFileName = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA));
-        mMimeType = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
-        mDestination =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESTINATION));
-        mVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY));
-        mControl = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CONTROL));
-        mStatus = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS));
-        mNumFailed = cursor.getInt(cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS));
-        mRetryAfter = retryRedirect & 0xfffffff;
-        mRedirectCount = retryRedirect >> 28;
-        mLastMod = cursor.getLong(cursor.getColumnIndexOrThrow(
-                                        Downloads.Impl.COLUMN_LAST_MODIFICATION));
-        mPackage = cursor.getString(cursor.getColumnIndexOrThrow(
-                                        Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE));
-        mClass = cursor.getString(cursor.getColumnIndexOrThrow(
-                                        Downloads.Impl.COLUMN_NOTIFICATION_CLASS));
-        mExtras = cursor.getString(cursor.getColumnIndexOrThrow(
-                                        Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS));
-        mCookies =
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_COOKIE_DATA));
-        mUserAgent =
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_USER_AGENT));
-        mReferer = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_REFERER));
-        mTotalBytes =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES));
-        mCurrentBytes =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CURRENT_BYTES));
-        mETag = cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG));
-        mMediaScanned = cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
-        mIsPublicApi = cursor.getInt(
-                cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_IS_PUBLIC_API)) != 0;
-        mAllowedNetworkTypes = cursor.getInt(
-                cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES));
-        mAllowRoaming = cursor.getInt(
-                cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_ALLOW_ROAMING)) != 0;
-        mTitle = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TITLE));
-        mDescription =
-            cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESCRIPTION));
         mFuzz = Helpers.sRandom.nextInt(1001);
-
-        readRequestHeaders(mId);
-    }
-
-    private void readRequestHeaders(long downloadId) {
-        Uri headerUri = Uri.withAppendedPath(
-                getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT);
-        Cursor cursor = mContext.getContentResolver().query(headerUri, null, null, null, null);
-        try {
-            int headerIndex =
-                    cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER);
-            int valueIndex =
-                    cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE);
-            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
-                mRequestHeaders.put(cursor.getString(headerIndex), cursor.getString(valueIndex));
-            }
-        } finally {
-            cursor.close();
-        }
-
-        if (mCookies != null) {
-            mRequestHeaders.put("Cookie", mCookies);
-        }
-        if (mReferer != null) {
-            mRequestHeaders.put("Referer", mReferer);
-        }
     }
 
     public Map<String, String> getHeaders() {
@@ -167,7 +217,7 @@ public class DownloadInfo {
         if (mIsPublicApi) {
             intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
             intent.setPackage(mPackage);
-            intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, (long) mId);
+            intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId);
         } else { // legacy behavior
             if (mClass == null) {
                 return;
@@ -393,4 +443,66 @@ public class DownloadInfo {
     public Uri getAllDownloadsUri() {
         return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId);
     }
+
+
+    public void logVerboseInfo() {
+        Log.v(Constants.TAG, "Service adding new entry");
+        Log.v(Constants.TAG, "ID      : " + mId);
+        Log.v(Constants.TAG, "URI     : " + ((mUri != null) ? "yes" : "no"));
+        Log.v(Constants.TAG, "NO_INTEG: " + mNoIntegrity);
+        Log.v(Constants.TAG, "HINT    : " + mHint);
+        Log.v(Constants.TAG, "FILENAME: " + mFileName);
+        Log.v(Constants.TAG, "MIMETYPE: " + mMimeType);
+        Log.v(Constants.TAG, "DESTINAT: " + mDestination);
+        Log.v(Constants.TAG, "VISIBILI: " + mVisibility);
+        Log.v(Constants.TAG, "CONTROL : " + mControl);
+        Log.v(Constants.TAG, "STATUS  : " + mStatus);
+        Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
+        Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
+        Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
+        Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
+        Log.v(Constants.TAG, "PACKAGE : " + mPackage);
+        Log.v(Constants.TAG, "CLASS   : " + mClass);
+        Log.v(Constants.TAG, "COOKIES : " + ((mCookies != null) ? "yes" : "no"));
+        Log.v(Constants.TAG, "AGENT   : " + mUserAgent);
+        Log.v(Constants.TAG, "REFERER : " + ((mReferer != null) ? "yes" : "no"));
+        Log.v(Constants.TAG, "TOTAL   : " + mTotalBytes);
+        Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
+        Log.v(Constants.TAG, "ETAG    : " + mETag);
+        Log.v(Constants.TAG, "SCANNED : " + mMediaScanned);
+    }
+
+    /**
+     * 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"
+     */
+    long nextAction(long now) {
+        if (Downloads.Impl.isStatusCompleted(mStatus)) {
+            return -1;
+        }
+        if (mStatus != Downloads.Impl.STATUS_RUNNING_PAUSED) {
+            return 0;
+        }
+        if (mNumFailed == 0) {
+            return 0;
+        }
+        long when = restartTime();
+        if (when <= now) {
+            return 0;
+        }
+        return when - now;
+    }
+
+    /**
+     * Returns whether a file should be scanned
+     */
+    boolean shouldScanFile() {
+        return !mMediaScanned
+                && mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+                && Downloads.Impl.isStatusSuccess(mStatus)
+                && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mMimeType);
+    }
 }
index 38def59..90c8693 100644 (file)
@@ -26,8 +26,8 @@ import android.provider.Downloads;
 import android.view.View;
 import android.widget.RemoteViews;
 
+import java.util.Collection;
 import java.util.HashMap;
-import java.util.List;
 
 /**
  * This class handles the updating of the Notification Manager for the
@@ -104,12 +104,12 @@ class DownloadNotification {
     /*
      * Update the notification ui.
      */
-    public void updateNotification(List<DownloadInfo> downloads) {
+    public void updateNotification(Collection<DownloadInfo> downloads) {
         updateActiveNotification(downloads);
         updateCompletedNotification(downloads);
     }
 
-    private void updateActiveNotification(List<DownloadInfo> downloads) {
+    private void updateActiveNotification(Collection<DownloadInfo> downloads) {
         // Collate the notifications
         mNotifications.clear();
         for (DownloadInfo download : downloads) {
@@ -135,7 +135,6 @@ class DownloadNotification {
                 item.mId = (int) id;
                 item.mPackageName = packageName;
                 item.mDescription = download.mDescription;
-                String className = download.mClass;
                 item.addItem(title, progress, max);
                 mNotifications.put(packageName, item);
             }
@@ -195,7 +194,8 @@ class DownloadNotification {
             Intent intent = new Intent(Constants.ACTION_LIST);
             intent.setClassName("com.android.providers.downloads",
                     DownloadReceiver.class.getName());
-            intent.setData(ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, item.mId));
+            intent.setData(
+                    ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.mId));
             intent.putExtra("multiple", item.mTitleCount > 1);
 
             n.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
@@ -209,7 +209,7 @@ class DownloadNotification {
         return download.mStatus == Downloads.STATUS_RUNNING_PAUSED && download.mPausedReason != null;
     }
 
-    private void updateCompletedNotification(List<DownloadInfo> downloads) {
+    private void updateCompletedNotification(Collection<DownloadInfo> downloads) {
         for (DownloadInfo download : downloads) {
             if (!isCompleteAndVisible(download)) {
                 continue;
@@ -224,7 +224,8 @@ class DownloadNotification {
                 title = mContext.getResources().getString(
                         R.string.download_unknown_title);
             }
-            Uri contentUri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, id);
+            Uri contentUri =
+                ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
             String caption;
             Intent intent;
             if (Downloads.Impl.isStatusError(download.mStatus)) {
index edbfb78..df7ca71 100644 (file)
@@ -827,9 +827,8 @@ public final class DownloadProvider extends ContentProvider {
             if (filename != null) {
                 Cursor c = query(uri, new String[]
                         { Downloads.Impl.COLUMN_TITLE }, null, null, null);
-                if (!c.moveToFirst() || c.getString(0) == null) {
-                    values.put(Downloads.Impl.COLUMN_TITLE,
-                            new File(filename).getName());
+                if (!c.moveToFirst() || c.getString(0).isEmpty()) {
+                    values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
                 }
                 c.close();
             }
index 852c371..d81e026 100644 (file)
@@ -28,7 +28,6 @@ import android.net.DownloadManager;
 import android.net.NetworkInfo;
 import android.net.Uri;
 import android.provider.Downloads;
-import android.util.Config;
 import android.util.Log;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -47,146 +46,148 @@ public class DownloadReceiver extends BroadcastReceiver {
             mSystemFacade = new RealSystemFacade(context);
         }
 
-        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "Receiver onBoot");
-            }
-            context.startService(new Intent(context, DownloadService.class));
-        } else if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "Receiver onConnectivity");
-            }
+        String action = intent.getAction();
+        if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
+            startService(context);
+        } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
             NetworkInfo info = (NetworkInfo)
                     intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
             if (info != null && info.isConnected()) {
-                if (Constants.LOGX) {
-                    if (Helpers.isNetworkAvailable(mSystemFacade)) {
-                        Log.i(Constants.TAG, "Broadcast: Network Up");
-                    } else {
-                        Log.i(Constants.TAG, "Broadcast: Network Up, Actually Down");
-                    }
-                }
-                context.startService(new Intent(context, DownloadService.class));
-            } else {
-                if (Constants.LOGX) {
-                    if (Helpers.isNetworkAvailable(mSystemFacade)) {
-                        Log.i(Constants.TAG, "Broadcast: Network Down, Actually Up");
-                    } else {
-                        Log.i(Constants.TAG, "Broadcast: Network Down");
-                    }
-                }
+                startService(context);
             }
-        } else if (intent.getAction().equals(Constants.ACTION_RETRY)) {
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "Receiver retry");
+        } else if (action.equals(Constants.ACTION_RETRY)) {
+            startService(context);
+        } else if (action.equals(Constants.ACTION_OPEN)
+                || action.equals(Constants.ACTION_LIST)
+                || action.equals(Constants.ACTION_HIDE)) {
+            handleNotificationBroadcast(context, intent);
+        }
+    }
+
+    /**
+     * Handle any broadcast related to a system notification.
+     */
+    private void handleNotificationBroadcast(Context context, Intent intent) {
+        Uri uri = intent.getData();
+        String action = intent.getAction();
+        if (Constants.LOGVV) {
+            if (action.equals(Constants.ACTION_OPEN)) {
+                Log.v(Constants.TAG, "Receiver open for " + uri);
+            } else if (action.equals(Constants.ACTION_LIST)) {
+                Log.v(Constants.TAG, "Receiver list for " + uri);
+            } else { // ACTION_HIDE
+                Log.v(Constants.TAG, "Receiver hide for " + uri);
             }
-            context.startService(new Intent(context, DownloadService.class));
-        } else if (intent.getAction().equals(Constants.ACTION_OPEN)
-                || intent.getAction().equals(Constants.ACTION_LIST)) {
-            if (Constants.LOGVV) {
-                if (intent.getAction().equals(Constants.ACTION_OPEN)) {
-                    Log.v(Constants.TAG, "Receiver open for " + intent.getData());
-                } else {
-                    Log.v(Constants.TAG, "Receiver list for " + intent.getData());
-                }
+        }
+
+        Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+        if (cursor == null) {
+            return;
+        }
+        try {
+            if (!cursor.moveToFirst()) {
+                return;
             }
-            Cursor cursor = context.getContentResolver().query(
-                    intent.getData(), null, null, null, null);
-            if (cursor != null) {
-                if (cursor.moveToFirst()) {
-                    int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
-                    int status = cursor.getInt(statusColumn);
-                    int visibilityColumn =
-                            cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY);
-                    int visibility = cursor.getInt(visibilityColumn);
-                    if (Downloads.Impl.isStatusCompleted(status)
-                            && visibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
-                        ContentValues values = new ContentValues();
-                        values.put(Downloads.Impl.COLUMN_VISIBILITY,
-                                Downloads.Impl.VISIBILITY_VISIBLE);
-                        context.getContentResolver().update(intent.getData(), values, null, null);
-                    }
-
-                    if (intent.getAction().equals(Constants.ACTION_OPEN)) {
-                        int filenameColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._DATA);
-                        int mimetypeColumn =
-                                cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE);
-                        String filename = cursor.getString(filenameColumn);
-                        String mimetype = cursor.getString(mimetypeColumn);
-                        Uri path = Uri.parse(filename);
-                        // If there is no scheme, then it must be a file
-                        if (path.getScheme() == null) {
-                            path = Uri.fromFile(new File(filename));
-                        }
-                        Intent activityIntent = new Intent(Intent.ACTION_VIEW);
-                        activityIntent.setDataAndType(path, mimetype);
-                        activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                        try {
-                            context.startActivity(activityIntent);
-                        } catch (ActivityNotFoundException ex) {
-                            if (Config.LOGD) {
-                                Log.d(Constants.TAG, "no activity for " + mimetype, ex);
-                            }
-                            // nothing anyone can do about this, but we're in a clean state,
-                            //     swallow the exception entirely
-                        }
-                    } else {
-                        int packageColumn = cursor.getColumnIndexOrThrow(
-                                Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
-                        int classColumn = cursor.getColumnIndexOrThrow(
-                                Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
-                        int isPublicApiColumn = cursor.getColumnIndex(
-                                Downloads.Impl.COLUMN_IS_PUBLIC_API);
-                        String pckg = cursor.getString(packageColumn);
-                        String clazz = cursor.getString(classColumn);
-                        boolean isPublicApi = cursor.getInt(isPublicApiColumn) != 0;
-
-                        if (pckg != null) {
-                            Intent appIntent = null;
-                            if (isPublicApi) {
-                                appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
-                                appIntent.setPackage(pckg);
-                            } else if (clazz != null) { // legacy behavior
-                                appIntent = new Intent(Downloads.Impl.ACTION_NOTIFICATION_CLICKED);
-                                appIntent.setClassName(pckg, clazz);
-                                if (intent.getBooleanExtra("multiple", true)) {
-                                    appIntent.setData(Downloads.Impl.CONTENT_URI);
-                                } else {
-                                    appIntent.setData(intent.getData());
-                                }
-                            }
-                            if (appIntent != null) {
-                                mSystemFacade.sendBroadcast(appIntent);
-                            }
-                        }
-                    }
-                }
-                cursor.close();
+
+            if (action.equals(Constants.ACTION_OPEN)) {
+                openDownload(context, cursor);
+                hideNotification(context, uri, cursor);
+            } else if (action.equals(Constants.ACTION_LIST)) {
+                sendNotificationClickedIntent(intent, cursor);
+            } else { // ACTION_HIDE
+                hideNotification(context, uri, cursor);
             }
-            mSystemFacade.cancelNotification((int) ContentUris.parseId(intent.getData()));
-        } else if (intent.getAction().equals(Constants.ACTION_HIDE)) {
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "Receiver hide for " + intent.getData());
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Hide a system notification for a download.
+     * @param uri URI to update the download
+     * @param cursor Cursor for reading the download's fields
+     */
+    private void hideNotification(Context context, Uri uri, Cursor cursor) {
+        mSystemFacade.cancelNotification(ContentUris.parseId(uri));
+
+        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
+        int status = cursor.getInt(statusColumn);
+        int visibilityColumn =
+                cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY);
+        int visibility = cursor.getInt(visibilityColumn);
+        if (Downloads.Impl.isStatusCompleted(status)
+                && visibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
+            ContentValues values = new ContentValues();
+            values.put(Downloads.Impl.COLUMN_VISIBILITY,
+                    Downloads.Impl.VISIBILITY_VISIBLE);
+            context.getContentResolver().update(uri, values, null, null);
+        }
+    }
+
+    /**
+     * Open the download that cursor is currently pointing to, since it's completed notification
+     * has been clicked.
+     */
+    private void openDownload(Context context, Cursor cursor) {
+        String filename = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA));
+        String mimetype =
+            cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
+        Uri path = Uri.parse(filename);
+        // If there is no scheme, then it must be a file
+        if (path.getScheme() == null) {
+            path = Uri.fromFile(new File(filename));
+        }
+
+        Intent activityIntent = new Intent(Intent.ACTION_VIEW);
+        activityIntent.setDataAndType(path, mimetype);
+        activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        try {
+            context.startActivity(activityIntent);
+        } catch (ActivityNotFoundException ex) {
+            Log.d(Constants.TAG, "no activity for " + mimetype, ex);
+        }
+    }
+
+    /**
+     * Notify the owner of a running download that its notification was clicked.
+     * @param intent the broadcast intent sent by the notification manager
+     * @param cursor Cursor for reading the download's fields
+     */
+    private void sendNotificationClickedIntent(Intent intent, Cursor cursor) {
+        String pckg = cursor.getString(
+                cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE));
+        if (pckg == null) {
+            return;
+        }
+
+        String clazz = cursor.getString(
+                cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_NOTIFICATION_CLASS));
+        boolean isPublicApi =
+                cursor.getInt(cursor.getColumnIndex(Downloads.Impl.COLUMN_IS_PUBLIC_API)) != 0;
+
+        Intent appIntent = null;
+        if (isPublicApi) {
+            appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
+            appIntent.setPackage(pckg);
+        } else { // legacy behavior
+            if (clazz == null) {
+                return;
             }
-            Cursor cursor = context.getContentResolver().query(
-                    intent.getData(), null, null, null, null);
-            if (cursor != null) {
-                if (cursor.moveToFirst()) {
-                    int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
-                    int status = cursor.getInt(statusColumn);
-                    int visibilityColumn =
-                            cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY);
-                    int visibility = cursor.getInt(visibilityColumn);
-                    if (Downloads.Impl.isStatusCompleted(status)
-                            && visibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
-                        ContentValues values = new ContentValues();
-                        values.put(Downloads.Impl.COLUMN_VISIBILITY,
-                                Downloads.Impl.VISIBILITY_VISIBLE);
-                        context.getContentResolver().update(intent.getData(), values, null, null);
-                    }
-                }
-                cursor.close();
+            appIntent = new Intent(Downloads.Impl.ACTION_NOTIFICATION_CLICKED);
+            appIntent.setClassName(pckg, clazz);
+            if (intent.getBooleanExtra("multiple", true)) {
+                appIntent.setData(Downloads.Impl.CONTENT_URI);
+            } else {
+                long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
+                appIntent.setData(
+                        ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, downloadId));
             }
         }
+
+        mSystemFacade.sendBroadcast(appIntent);
+    }
+
+    private void startService(Context context) {
+        context.startService(new Intent(context, DownloadService.class));
     }
 }
index b85fb90..62598b7 100644 (file)
@@ -25,12 +25,8 @@ 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.IMediaScannerService;
 import android.net.Uri;
 import android.os.Environment;
@@ -39,27 +35,22 @@ import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
 import android.provider.Downloads;
-import android.util.Config;
 import android.util.Log;
 
-import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
 import com.google.common.annotations.VisibleForTesting;
 
 import java.io.File;
-import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
+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 ------------ */
-
     /** Observer to get notified when the content observer's data changes */
     private DownloadManagerContentObserver mObserver;
 
@@ -67,12 +58,12 @@ 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
@@ -100,21 +91,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
      */
@@ -183,8 +162,6 @@ public class DownloadService extends Service {
         }
     }
 
-    /* ------------ Methods ------------ */
-
     /**
      * Returns an IBinder instance when someone wants to connect to this
      * service. Binding to this service is not allowed.
@@ -208,8 +185,6 @@ public class DownloadService extends Service {
             mSystemFacade = new RealSystemFacade(this);
         }
 
-        mDownloads = Lists.newArrayList();
-
         mObserver = new DownloadManagerContentObserver();
         getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                 true, mObserver);
@@ -220,23 +195,20 @@ public class DownloadService extends Service {
 
         mNotifier = new DownloadNotification(this, mSystemFacade);
         mSystemFacade.cancelAllNotifications();
-        mNotifier.updateNotification(mDownloads);
 
         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;
     }
 
     /**
@@ -287,189 +259,101 @@ 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;
                 }
+
                 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, Downloads.Impl._ID);
-
+                        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;
-
-                boolean mustScan = false;
-                keepService = false;
-                wakeUp = Long.MAX_VALUE;
-
-                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 (info.shouldScanFile() && !scanFile(info, true)) {
+                            mustScan = true;
+                            keepService = true;
                         }
-                        if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
-                            scanFile(null, arrayPos);
+                        if (info.hasCompletionNotification()) {
+                            keepService = true;
                         }
-                        deleteDownload(arrayPos); // this advances in the array
-                    } else {
-                        int id = cursor.getInt(idColumn);
-
-                        if (arrayPos == mDownloads.size()) {
-                            insertDownload(cursor, arrayPos, 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;
-
-                            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, 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();
-                            } 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, 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();
-                            }
+                        long next = info.nextAction(now);
+                        if (next == 0) {
+                            keepService = true;
+                        } else if (next > 0 && next < wakeUp) {
+                            wakeUp = next;
                         }
                     }
+                } finally {
+                    cursor.close();
+                }
+
+                for (Long id : idsNoLongerInDatabase) {
+                    deleteDownload(id);
                 }
 
-                mNotifier.updateNotification(mDownloads);
+                mNotifier.updateNotification(mDownloads.values());
 
                 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);
-                    }
+                    bindMediaScanner();
                 } else {
                     mMediaScannerConnection.disconnectMediaScanner();
                 }
+            }
+        }
 
-                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);
             }
         }
+
+        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 (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));
+        }
     }
 
     /**
@@ -546,133 +430,40 @@ 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, long now) {
-        DownloadInfo info = new DownloadInfo(this, mSystemFacade, 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);
-        }
-
-        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) {
-                Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
-                info.mStatus = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
-
-                ContentValues values = new ContentValues();
-                values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
-                getContentResolver().update(info.getAllDownloadsUri(), values, null, null);
-                info.sendIntentIfRequested();
-                return;
-            }
+            info.logVerboseInfo();
         }
 
         if (info.isReadyToStart(now)) {
             info.start(now);
         }
+
+        return info;
     }
 
     /**
      * Updates the local copy of the info about a download.
      */
-    private void updateDownload(Cursor cursor, int arrayPos, long now) {
-        DownloadInfo info = 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)) {
-            mSystemFacade.cancelNotification(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)) {
+    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);
         }
-        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.isReadyToRestart(now)) {
             info.start(now);
@@ -680,128 +471,48 @@ public class DownloadService extends Service {
     }
 
     /**
-     * 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;
-    }
-
-    /**
      * Removes the local copy of the info about a download.
      */
-    private void deleteDownload(int arrayPos) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+    private void deleteDownload(long id) {
+        DownloadInfo info = mDownloads.get(id);
+        if (info.shouldScanFile()) {
+            scanFile(info, false);
+        }
         if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
             info.mStatus = Downloads.Impl.STATUS_CANCELED;
-        } else if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL
-                    && info.mFileName != null) {
+        }
+        if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
             new File(info.mFileName).delete();
         }
         mSystemFacade.cancelNotification(info.mId);
-
-        mDownloads.remove(arrayPos);
-    }
-
-    /**
-     * 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"
-     */
-    private long nextAction(int arrayPos, long now) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
-        if (Downloads.Impl.isStatusCompleted(info.mStatus)) {
-            return -1;
-        }
-        if (info.mStatus != Downloads.Impl.STATUS_RUNNING_PAUSED) {
-            return 0;
-        }
-        if (info.mNumFailed == 0) {
-            return 0;
-        }
-        long when = info.restartTime();
-        if (when <= now) {
-            return 0;
-        }
-        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;
+        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 = mDownloads.get(arrayPos);
+    private boolean scanFile(DownloadInfo info, boolean updateDatabase) {
         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(info.getAllDownloadsUri(), values, null, null);
-                    }
-                    return true;
-                } catch (RemoteException e) {
-                    Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
+            if (mMediaScannerService == null) {
+                return false;
+            }
+            try {
+                if (Constants.LOGV) {
+                    Log.v(Constants.TAG, "Scanning file " + info.mFileName);
+                }
+                mMediaScannerService.scanFile(info.mFileName, info.mMimeType);
+                if (updateDatabase) {
+                    ContentValues values = new ContentValues();
+                    values.put(Constants.MEDIA_SCANNED, 1);
+                    getContentResolver().update(info.getAllDownloadsUri(), values, null, null);
                 }
+                return true;
+            } catch (RemoteException e) {
+                Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
+                return false;
             }
         }
-        return false;
     }
 
 }
index 710da10..421fc2b 100644 (file)
@@ -81,13 +81,18 @@ class RealSystemFacade implements SystemFacade {
     }
 
     @Override
-    public void postNotification(int id, Notification notification) {
-        mNotificationManager.notify(id, notification);
+    public void postNotification(long id, Notification notification) {
+        /**
+         * TODO: The system notification manager takes ints, not longs, as IDs, but the download
+         * manager uses IDs take straight from the database, which are longs.  This will have to be
+         * dealt with at some point.
+         */
+        mNotificationManager.notify((int) id, notification);
     }
 
     @Override
-    public void cancelNotification(int id) {
-        mNotificationManager.cancel(id);
+    public void cancelNotification(long id) {
+        mNotificationManager.cancel((int) id);
     }
 
     @Override
index c194169..50624c3 100644 (file)
@@ -42,12 +42,12 @@ interface SystemFacade {
     /**
      * Post a system notification to the NotificationManager.
      */
-    public void postNotification(int id, Notification notification);
+    public void postNotification(long id, Notification notification);
 
     /**
      * Cancel a system notification.
      */
-    public void cancelNotification(int id);
+    public void cancelNotification(long id);
 
     /**
      * Cancel all system notifications.
index 40b2a90..d80bd4a 100644 (file)
@@ -19,7 +19,7 @@ public class FakeSystemFacade implements SystemFacade {
     boolean mIsRoaming = false;
     Long mMaxBytesOverMobile = null;
     List<Intent> mBroadcastsSent = new ArrayList<Intent>();
-    Map<Integer,Notification> mActiveNotifications = new HashMap<Integer,Notification>();
+    Map<Long,Notification> mActiveNotifications = new HashMap<Long,Notification>();
     List<Notification> mCanceledNotifications = new ArrayList<Notification>();
     Queue<Thread> mStartedThreads = new LinkedList<Thread>();
 
@@ -54,7 +54,7 @@ public class FakeSystemFacade implements SystemFacade {
     }
 
     @Override
-    public void postNotification(int id, Notification notification) {
+    public void postNotification(long id, Notification notification) {
         if (notification == null) {
             throw new AssertionFailedError("Posting null notification");
         }
@@ -62,7 +62,7 @@ public class FakeSystemFacade implements SystemFacade {
     }
 
     @Override
-    public void cancelNotification(int id) {
+    public void cancelNotification(long id) {
         Notification notification = mActiveNotifications.remove(id);
         if (notification != null) {
             mCanceledNotifications.add(notification);
@@ -71,7 +71,7 @@ public class FakeSystemFacade implements SystemFacade {
 
     @Override
     public void cancelAllNotifications() {
-        for (int id : mActiveNotifications.keySet()) {
+        for (long id : mActiveNotifications.keySet()) {
             cancelNotification(id);
         }
     }
index fce2f16..71e1723 100644 (file)
@@ -19,6 +19,7 @@ package com.android.providers.downloads.ui;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
@@ -365,7 +366,7 @@ public class DownloadList extends Activity
         Intent intent = new Intent("android.intent.action.DOWNLOAD_LIST");
         intent.setClassName("com.android.providers.downloads",
                 "com.android.providers.downloads.DownloadReceiver");
-        intent.setData(Uri.parse(Downloads.Impl.CONTENT_URI + "/" + id));
+        intent.setData(ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id));
         intent.putExtra("multiple", false);
         sendBroadcast(intent);
     }