Re-architecting RemoteViewsAdapter internals due to new constraints.

- Respecting the AdapterView/Adapter contract using FrameLayout notification to update individual items
- Initial changes to allow for keeping (currently bitmap only) memory in check when loading numerous RemoteViews
- Fixing issue with leaking activities due to extra service connection references
- Fixing issue with multiple RemoteViewsAdapters being set on the same AdapterView
- Fixing small issue with StackView sometimes requesting indices out of bounds
- Removing background loading, reverting to previous message-queuing method (seems to be performant now)

Change-Id: I42313767aa791dfe35c247c97ae5d64389e6bb4c
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index bb13f1d..c694ff1 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -4367,9 +4367,13 @@
     public void setRemoteViewsAdapter(Intent intent) {
         // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
         // service handling the specified intent.
-        Intent.FilterComparison fc = new Intent.FilterComparison(intent);
-        if (mRemoteAdapter != null && fc.equals(mRemoteAdapter.getRemoteViewsServiceIntent())) {
-            return;
+        if (mRemoteAdapter != null) {
+            Intent.FilterComparison fcNew = new Intent.FilterComparison(intent);
+            Intent.FilterComparison fcOld = new Intent.FilterComparison(
+                    mRemoteAdapter.getRemoteViewsServiceIntent());
+            if (fcNew.equals(fcOld)) {
+                return;
+            }
         }
 
         // Otherwise, create a new RemoteViewsAdapter for binding
diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java
index c08adb2..f245933 100644
--- a/core/java/android/widget/AdapterViewAnimator.java
+++ b/core/java/android/widget/AdapterViewAnimator.java
@@ -389,7 +389,7 @@
         int newWindowStartUnbounded = childIndex - mActiveOffset;
         int newWindowEndUnbounded = newWindowStartUnbounded + mNumActiveViews - 1;
         int newWindowStart = Math.max(0, newWindowStartUnbounded);
-        int newWindowEnd = Math.min(mAdapter.getCount(), newWindowEndUnbounded);
+        int newWindowEnd = Math.min(mAdapter.getCount() - 1, newWindowEndUnbounded);
 
         // This section clears out any items that are in our mActiveViews list
         // but are outside the effective bounds of our window (this is becomes an issue
@@ -592,18 +592,18 @@
          */
         private SavedState(Parcel in) {
             super(in);
-            whichChild = in.readInt();
+            this.whichChild = in.readInt();
         }
 
         @Override
         public void writeToParcel(Parcel out, int flags) {
             super.writeToParcel(out, flags);
-            out.writeInt(whichChild);
+            out.writeInt(this.whichChild);
         }
 
         @Override
         public String toString() {
-            return "AdapterViewAnimator.SavedState{ whichChild = " + whichChild + " }";
+            return "AdapterViewAnimator.SavedState{ whichChild = " + this.whichChild + " }";
         }
 
         public static final Parcelable.Creator<SavedState> CREATOR
@@ -781,10 +781,13 @@
     public void setRemoteViewsAdapter(Intent intent) {
         // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
         // service handling the specified intent.
-        Intent.FilterComparison fc = new Intent.FilterComparison(intent);
-        if (mRemoteViewsAdapter != null &&
-                fc.equals(mRemoteViewsAdapter.getRemoteViewsServiceIntent())) {
-            return;
+        if (mRemoteViewsAdapter != null) {
+            Intent.FilterComparison fcNew = new Intent.FilterComparison(intent);
+            Intent.FilterComparison fcOld = new Intent.FilterComparison(
+                    mRemoteViewsAdapter.getRemoteViewsServiceIntent());
+            if (fcNew.equals(fcOld)) {
+                return;
+            }
         }
 
         // Otherwise, create a new RemoteViewsAdapter for binding
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 945ffeb..9d214fc 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -74,6 +74,11 @@
      */
     private ArrayList<Action> mActions;
     
+    /**
+     * A class to keep track of memory usage by this RemoteViews
+     */
+    private MemoryUsageCounter mMemoryUsageCounter;
+
     
     /**
      * This flag indicates whether this RemoteViews object is being created from a
@@ -118,6 +123,15 @@
         public int describeContents() {
             return 0;
         }
+
+        /**
+         * Overridden by each class to report on it's own memory usage
+         */
+        public void updateMemoryUsageEstimate(MemoryUsageCounter counter) {
+            // We currently only calculate Bitmap memory usage, so by default, don't do anything
+            // here
+            return;
+        }
     }
 
     private class SetEmptyView extends Action {
@@ -525,7 +539,7 @@
                 }
             }
         }
-        
+
         int viewId;
         boolean targetBackground;
         int alpha;
@@ -817,6 +831,35 @@
                 throw new ActionException(ex);
             }
         }
+
+        @Override
+        public void updateMemoryUsageEstimate(MemoryUsageCounter counter) {
+            // We currently only calculate Bitmap memory usage
+            switch (this.type) {
+                case BITMAP:
+                    if (this.value != null) {
+                        final Bitmap b = (Bitmap) this.value;
+                        final Bitmap.Config c = b.getConfig();
+                        int bpp = 4;
+                        switch (c) {
+                        case ALPHA_8:
+                            bpp = 1;
+                            break;
+                        case RGB_565:
+                        case ARGB_4444:
+                            bpp = 2;
+                            break;
+                        case ARGB_8888:
+                            bpp = 4;
+                            break;
+                        }
+                        counter.bitmapIncrement(b.getWidth() * b.getHeight() * bpp);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
     }
 
     /**
@@ -854,6 +897,13 @@
             }
         }
 
+        @Override
+        public void updateMemoryUsageEstimate(MemoryUsageCounter counter) {
+            if (nestedViews != null) {
+                counter.bitmapIncrement(nestedViews.estimateBitmapMemoryUsage());
+            }
+        }
+
         int viewId;
         RemoteViews nestedViews;
 
@@ -861,6 +911,26 @@
     }
 
     /**
+     * Simple class used to keep track of memory usage in a RemoteViews.
+     *
+     */
+    private class MemoryUsageCounter {
+        public void clear() {
+            mBitmapHeapMemoryUsage = 0;
+        }
+
+        public void bitmapIncrement(int numBytes) {
+            mBitmapHeapMemoryUsage += numBytes;
+        }
+
+        public int getBitmapHeapMemoryUsage() {
+            return mBitmapHeapMemoryUsage;
+        }
+
+        int mBitmapHeapMemoryUsage;
+    }
+
+    /**
      * Create a new RemoteViews object that will display the views contained
      * in the specified layout file.
      * 
@@ -870,6 +940,10 @@
     public RemoteViews(String packageName, int layoutId) {
         mPackage = packageName;
         mLayoutId = layoutId;
+
+        // setup the memory usage statistics
+        mMemoryUsageCounter = new MemoryUsageCounter();
+        recalculateMemoryUsage();
     }
 
     /**
@@ -920,6 +994,10 @@
                 }
             }
         }
+
+        // setup the memory usage statistics
+        mMemoryUsageCounter = new MemoryUsageCounter();
+        recalculateMemoryUsage();
     }
 
     @Override
@@ -928,6 +1006,9 @@
         if (mActions != null) {
             that.mActions = (ArrayList<Action>)mActions.clone();
         }
+
+        // update the memory usage stats of the cloned RemoteViews
+        that.recalculateMemoryUsage();
         return that;
     }
 
@@ -939,7 +1020,7 @@
         return mLayoutId;
     }
 
-    /**
+    /*
      * This flag indicates whether this RemoteViews object is being created from a
      * RemoteViewsService for use as a child of a widget collection. This flag is used
      * to determine whether or not certain features are available, in particular,
@@ -951,6 +1032,28 @@
     }
 
     /**
+     * Updates the memory usage statistics.
+     */
+    private void recalculateMemoryUsage() {
+        mMemoryUsageCounter.clear();
+
+        // Accumulate the memory usage for each action
+        if (mActions != null) {
+            final int count = mActions.size();
+            for (int i= 0; i < count; ++i) {
+                mActions.get(i).updateMemoryUsageEstimate(mMemoryUsageCounter);
+            }
+        }
+    }
+
+    /**
+     * Returns an estimate of the bitmap heap memory usage for this RemoteViews.
+     */
+    int estimateBitmapMemoryUsage() {
+        return mMemoryUsageCounter.getBitmapHeapMemoryUsage();
+    }
+
+    /**
      * Add an action to be executed on the remote side when apply is called.
      * 
      * @param a The action to add
@@ -960,6 +1063,9 @@
             mActions = new ArrayList<Action>();
         }
         mActions.add(a);
+
+        // update the memory usage stats
+        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
     }
 
     /**
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 91b4afa..afb56fc 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -16,7 +16,9 @@
 
 package android.widget;
 
+import java.lang.ref.WeakReference;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Map;
 
@@ -32,8 +34,8 @@
 import android.util.Log;
 import android.view.Gravity;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
 
 import com.android.internal.widget.IRemoteViewsFactory;
 
@@ -48,15 +50,17 @@
     private Context mContext;
     private Intent mIntent;
     private RemoteViewsAdapterServiceConnection mServiceConnection;
-    private RemoteViewsCache mViewCache;
+    private WeakReference<RemoteAdapterConnectionCallback> mCallback;
+    private FixedSizeRemoteViewsCache mCache;
+
+    // The set of requested views that are to be notified when the associated RemoteViews are
+    // loaded.
+    private RemoteViewsFrameLayoutRefSet mRequestedViews;
 
     private HandlerThread mWorkerThread;
     // items may be interrupted within the normally processed queues
     private Handler mWorkerQueue;
     private Handler mMainQueue;
-    // items are never dequeued from the priority queue and must run
-    private Handler mWorkerPriorityQueue;
-    private Handler mMainPriorityQueue;
 
     /**
      * An interface for the RemoteAdapter to notify other classes when adapters
@@ -70,60 +74,89 @@
 
     /**
      * The service connection that gets populated when the RemoteViewsService is
-     * bound.
+     * bound.  This must be a static inner class to ensure that no references to the outer
+     * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being
+     * garbage collected, and would cause us to leak activities due to the caching mechanism for
+     * FrameLayouts in the adapter).
      */
-    private class RemoteViewsAdapterServiceConnection implements ServiceConnection {
+    private static class RemoteViewsAdapterServiceConnection implements ServiceConnection {
         private boolean mConnected;
+        private WeakReference<RemoteViewsAdapter> mAdapter;
         private IRemoteViewsFactory mRemoteViewsFactory;
-        private RemoteAdapterConnectionCallback mCallback;
 
-        public RemoteViewsAdapterServiceConnection(RemoteAdapterConnectionCallback callback) {
-            mCallback = callback;
+        public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) {
+            mAdapter = new WeakReference<RemoteViewsAdapter>(adapter);
         }
 
-        public void onServiceConnected(ComponentName name, IBinder service) {
+        public void onServiceConnected(ComponentName name,
+                IBinder service) {
             mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
             mConnected = true;
 
-            // notifyDataSetChanged should be called first, to ensure that the
-            // views are not updated twice
-            notifyDataSetChanged();
-
-            // post a new runnable to load the appropriate data, then callback
-            mWorkerPriorityQueue.post(new Runnable() {
+            // Queue up work that we need to do for the callback to run
+            final RemoteViewsAdapter adapter = mAdapter.get();
+            if (adapter == null) return;
+            adapter.mWorkerQueue.post(new Runnable() {
                 @Override
                 public void run() {
-                    // we need to get the viewTypeCount specifically, so just get all the
-                    // metadata
-                    mViewCache.requestMetaData();
+                    // Call back to the service to notify that the data set changed
+                    if (adapter.mServiceConnection.isConnected()) {
+                        IRemoteViewsFactory factory =
+                            adapter.mServiceConnection.getRemoteViewsFactory();
+                        try {
+                            // call back to the factory
+                            factory.onDataSetChanged();
+                        } catch (Exception e) {
+                            Log.e(TAG, "Error notifying factory of data set changed in " +
+                                        "onServiceConnected(): " + e.getMessage());
+                            e.printStackTrace();
 
-                    // post a runnable to call the callback on the main thread
-                    mMainPriorityQueue.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (mCallback != null)
-                                mCallback.onRemoteAdapterConnected();
+                            // Return early to prevent anything further from being notified
+                            // (effectively nothing has changed)
+                            return;
                         }
-                    });
+
+                        // Request meta data so that we have up to date data when calling back to
+                        // the remote adapter callback
+                        adapter.updateMetaData();
+
+                        // Post a runnable to call back to the view to notify it that we have
+                        // connected
+                        adapter. mMainQueue.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                final RemoteAdapterConnectionCallback callback =
+                                    adapter.mCallback.get();
+                                if (callback != null) {
+                                    callback.onRemoteAdapterConnected();
+                                }
+                            }
+                        });
+                    }
                 }
             });
-
-            // start the background loader
-            mViewCache.startBackgroundLoader();
         }
 
         public void onServiceDisconnected(ComponentName name) {
-            mRemoteViewsFactory = null;
             mConnected = false;
+            mRemoteViewsFactory = null;
 
-            // clear the main/worker queues
-            mMainQueue.removeMessages(0);
+            final RemoteViewsAdapter adapter = mAdapter.get();
+            if (adapter == null) return;
             
-            // stop the background loader
-            mViewCache.stopBackgroundLoader();
+            // Clear the main/worker queues
+            adapter.mMainQueue.removeMessages(0);
+            adapter.mWorkerQueue.removeMessages(0);
 
-            if (mCallback != null)
-                mCallback.onRemoteAdapterDisconnected();
+            // Clear the cache
+            synchronized (adapter.mCache) {
+                adapter.mCache.reset();
+            }
+
+            final RemoteAdapterConnectionCallback callback = adapter.mCallback.get();
+            if (callback != null) {
+                callback.onRemoteAdapterDisconnected();
+            }
         }
 
         public IRemoteViewsFactory getRemoteViewsFactory() {
@@ -136,532 +169,400 @@
     }
 
     /**
-     * An internal cache of remote views.
+     * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when
+     * they are loaded.
      */
-    private class RemoteViewsCache {
-        private static final String TAG = "RemoteViewsCache";
-
-        private RemoteViewsInfo mViewCacheInfo;
-        private RemoteViewsIndexInfo[] mViewCache;
-        private int[] mTmpViewCacheLoadIndices;
-        private LinkedList<Integer> mViewCacheLoadIndices;
-        private boolean mBackgroundLoaderEnabled;
-
-        // if a user loading view is not provided, then we create a temporary one
-        // for the user using the height of the first view
-        private RemoteViews mUserLoadingView;
-        private RemoteViews mFirstView;
-        private int mFirstViewHeight;
-
-        // determines when the current cache window needs to be updated with new
-        // items (ie. when there is not enough slack)
-        private int mViewCacheStartPosition;
-        private int mViewCacheEndPosition;
-        private int mHalfCacheSize;
-        private int mCacheSlack;
-        private final float mCacheSlackPercentage = 0.75f;
-
-        /**
-         * The data structure stored at each index of the cache. Any member 
-         * that is not invalidated persists throughout the lifetime of the cache.
-         */
-        private class RemoteViewsIndexInfo {
-            FrameLayout flipper;
-            RemoteViews view;
-            long itemId;
-            int typeId;
-
-            RemoteViewsIndexInfo() {
-                invalidate();
-            }
-
-            void set(RemoteViews v, long id) {
-                view = v;
-                itemId = id;
-                if (v != null)
-                    typeId = v.getLayoutId();
-                else
-                    typeId = 0;
-            }
-
-            void invalidate() {
-                view = null;
-                itemId = 0;
-                typeId = 0;
-            }
-
-            final boolean isValid() {
-                return (view != null);
-            }
+    private class RemoteViewsFrameLayout extends FrameLayout {
+        public RemoteViewsFrameLayout(Context context) {
+            super(context);
         }
 
         /**
-         * Remote adapter metadata. Useful for when we have to lock on something
-         * before updating the metadata.
+         * Updates this RemoteViewsFrameLayout depending on the view that was loaded.
+         * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded
+         *             successfully.
          */
-        private class RemoteViewsInfo {
-            int count;
-            int viewTypeCount;
-            boolean hasStableIds;
-            boolean isDataDirty;
-            Map<Integer, Integer> mTypeIdIndexMap;
+        public void onRemoteViewsLoaded(RemoteViews view) {
+            // Remove all the children of this layout first
+            removeAllViews();
+            addView(view.apply(getContext(), this));
+        }
+    }
 
-            RemoteViewsInfo() {
-                count = 0;
-                // by default there is at least one dummy view type
-                viewTypeCount = 1;
-                hasStableIds = true;
-                isDataDirty = false;
-                mTypeIdIndexMap = new HashMap<Integer, Integer>();
+    /**
+     * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the
+     * adapter that have not yet had their RemoteViews loaded.
+     */
+    private class RemoteViewsFrameLayoutRefSet {
+        private HashMap<Integer, LinkedList<RemoteViewsFrameLayout>> mReferences;
+
+        public RemoteViewsFrameLayoutRefSet() {
+            mReferences = new HashMap<Integer, LinkedList<RemoteViewsFrameLayout>>();
+        }
+
+        /**
+         * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter.
+         */
+        public void add(int position, RemoteViewsFrameLayout layout) {
+            final Integer pos = position;
+            LinkedList<RemoteViewsFrameLayout> refs;
+
+            // Create the list if necessary
+            if (mReferences.containsKey(pos)) {
+                refs = mReferences.get(pos);
+            } else {
+                refs = new LinkedList<RemoteViewsFrameLayout>();
+                mReferences.put(pos, refs);
             }
+
+            // Add the references to the list
+            refs.add(layout);
         }
 
-        public RemoteViewsCache(int halfCacheSize) {
-            mHalfCacheSize = halfCacheSize;
-            mCacheSlack = Math.round(mCacheSlackPercentage * mHalfCacheSize);
-            mViewCacheStartPosition = 0;
-            mViewCacheEndPosition = -1;
-            mBackgroundLoaderEnabled = false;
-
-            // initialize the cache
-            int cacheSize = 2 * mHalfCacheSize + 1;
-            mViewCacheInfo = new RemoteViewsInfo();
-            mViewCache = new RemoteViewsIndexInfo[cacheSize];
-            for (int i = 0; i < mViewCache.length; ++i) {
-                mViewCache[i] = new RemoteViewsIndexInfo();
-            }
-            mTmpViewCacheLoadIndices = new int[cacheSize];
-            mViewCacheLoadIndices = new LinkedList<Integer>();
-        }
-
-        private final boolean contains(int position) {
-            return (mViewCacheStartPosition <= position) && (position <= mViewCacheEndPosition);
-        }
-
-        private final boolean containsAndIsValid(int position) {
-            if (contains(position)) {
-                RemoteViewsIndexInfo indexInfo = mViewCache[getCacheIndex(position)];
-                if (indexInfo.isValid()) {
-                    return true;
+        /**
+         * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that
+         * the associated RemoteViews has loaded.
+         */
+        public void notifyOnRemoteViewsLoaded(int position, RemoteViews view, int typeId) {
+            final Integer pos = position;
+            if (mReferences.containsKey(pos)) {
+                // Notify all the references for that position of the newly loaded RemoteViews
+                final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(pos);
+                for (final RemoteViewsFrameLayout ref : refs) {
+                    ref.onRemoteViewsLoaded(view);
                 }
+                refs.clear();
+
+                // Remove this set from the original mapping
+                mReferences.remove(pos);
             }
-            return false;
         }
 
-        private final int getCacheIndex(int position) {
-            // take the modulo of the position
-            final int cacheSize = mViewCache.length;
-            return (cacheSize + (position % cacheSize)) % cacheSize;
+        /**
+         * Removes all references to all RemoteViewsFrameLayouts returned by the adapter.
+         */
+        public void clear() {
+            // We currently just clear the references, and leave all the previous layouts returned
+            // in their default state of the loading view.
+            mReferences.clear();
+        }
+    }
+
+    /**
+     * The meta-data associated with the cache in it's current state.
+     */
+    private class RemoteViewsMetaData {
+        int count;
+        int viewTypeCount;
+        boolean hasStableIds;
+        boolean isDataDirty;
+
+        // Used to determine how to construct loading views.  If a loading view is not specified
+        // by the user, then we try and load the first view, and use its height as the height for
+        // the default loading view.
+        RemoteViews mUserLoadingView;
+        RemoteViews mFirstView;
+        int mFirstViewHeight;
+
+        // A mapping from type id to a set of unique type ids
+        private Map<Integer, Integer> mTypeIdIndexMap;
+
+        public RemoteViewsMetaData() {
+            reset();
         }
 
-        public void requestMetaData() {
-            if (mServiceConnection.isConnected()) {
-                try {
-                    IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+        public void reset() {
+            count = 0;
+            // by default there is at least one dummy view type
+            viewTypeCount = 1;
+            hasStableIds = true;
+            isDataDirty = false;
+            mUserLoadingView = null;
+            mFirstView = null;
+            mFirstViewHeight = 0;
+            mTypeIdIndexMap = new HashMap<Integer, Integer>();
+        }
 
-                    // get the properties/first view (so that we can use it to
-                    // measure our dummy views)
-                    boolean hasStableIds = factory.hasStableIds();
-                    int viewTypeCount = factory.getViewTypeCount();
-                    int count = factory.getCount();
-                    RemoteViews loadingView = factory.getLoadingView();
-                    RemoteViews firstView = null;
-                    if ((count > 0) && (loadingView == null)) {
-                        firstView = factory.getViewAt(0);
-                    }
-                    synchronized (mViewCacheInfo) {
-                        RemoteViewsInfo info = mViewCacheInfo;
-                        info.hasStableIds = hasStableIds;
-                        info.viewTypeCount = viewTypeCount + 1;
-                        info.count = count;
-                        mUserLoadingView = loadingView;
-                        if (firstView != null) {
-                            mFirstView = firstView;
-                            mFirstViewHeight = -1;
-                        }
-                    }
-                } catch (Exception e) {
-                    // print the error
-                    Log.e(TAG, "Error in requestMetaData(): " + e.getMessage());
+        public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) {
+            mUserLoadingView = loadingView;
+            if (firstView != null) {
+                mFirstView = firstView;
+                mFirstViewHeight = -1;
+            }
+        }
 
-                    // reset any members after the failed call
-                    synchronized (mViewCacheInfo) {
-                        RemoteViewsInfo info = mViewCacheInfo;
-                        info.hasStableIds = false;
-                        info.viewTypeCount = 1;
-                        info.count = 0;
-                        mUserLoadingView = null;
+        public int getMappedViewType(int typeId) {
+            if (mTypeIdIndexMap.containsKey(typeId)) {
+                return mTypeIdIndexMap.get(typeId);
+            } else {
+                // We +1 because the loading view always has view type id of 0
+                int incrementalTypeId = mTypeIdIndexMap.size() + 1;
+                mTypeIdIndexMap.put(typeId, incrementalTypeId);
+                return incrementalTypeId;
+            }
+        }
+
+        private RemoteViewsFrameLayout createLoadingView(int position, View convertView,
+                ViewGroup parent) {
+            // Create and return a new FrameLayout, and setup the references for this position
+            final Context context = parent.getContext();
+            RemoteViewsFrameLayout layout = new RemoteViewsFrameLayout(context);
+
+            // Create a new loading view
+            synchronized (mCache) {
+                if (mUserLoadingView != null) {
+                    // A user-specified loading view
+                    View loadingView = mUserLoadingView.apply(parent.getContext(), parent);
+                    loadingView.setTag(new Integer(0));
+                    layout.addView(loadingView);
+                } else {
+                    // A default loading view
+                    // Use the size of the first row as a guide for the size of the loading view
+                    if (mFirstViewHeight < 0) {
+                        View firstView = mFirstView.apply(parent.getContext(), parent);
+                        firstView.measure(
+                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                        mFirstViewHeight = firstView.getMeasuredHeight();
                         mFirstView = null;
-                        mFirstViewHeight = -1;
-                    }
-                }
-            }
-        }
-
-        protected void onNotifyDataSetChanged() {
-            // we mark the data as dirty so that the next call to fetch views will result in
-            // an onDataSetDirty() call from the adapter
-            synchronized (mViewCacheInfo) {
-                mViewCacheInfo.isDataDirty = true;
-            }
-        }
-
-        private void updateNotifyDataSetChanged() {
-            // actually calls through to the factory to notify it to update
-            if (mServiceConnection.isConnected()) {
-                IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
-                try {
-                    // call back to the factory
-                    factory.onDataSetChanged();
-                } catch (Exception e) {
-                    // print the error
-                    Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
-
-                    // return early to prevent container from being notified (nothing has changed)
-                    return;
-                }
-            }
-
-            // re-request the new metadata (only after the notification to the factory)
-            requestMetaData();
-
-            // post a new runnable on the main thread to propagate the notification back
-            // to the base adapter
-            mMainQueue.post(new Runnable() {
-                @Override
-                public void run() {
-                   completeNotifyDataSetChanged();
-                }
-            });
-        }
-
-        protected void updateRemoteViewsInfo(int position) {
-            if (mServiceConnection.isConnected()) {
-                IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
-
-                // load the item information
-                RemoteViews remoteView = null;
-                long itemId = 0;
-                try {
-                    remoteView = factory.getViewAt(position);
-                    itemId = factory.getItemId(position);
-                } catch (Exception e) {
-                    // print the error
-                    Log.e(TAG, "Error in updateRemoteViewsInfo(" + position + "): " +
-                            e.getMessage());
-                    e.printStackTrace();
-
-                    // return early to prevent additional work in re-centering the view cache, and
-                    // swapping from the loading view
-                    return;
-                }
-
-                synchronized (mViewCache) {
-                    // skip if the window has moved
-                    if (position < mViewCacheStartPosition || position > mViewCacheEndPosition)
-                        return;
-
-                    final int positionIndex = position;
-                    final int cacheIndex = getCacheIndex(position);
-                    mViewCache[cacheIndex].set(remoteView, itemId);
-
-                    // notify the main thread when done loading
-                    // flush pending updates
-                    mMainQueue.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            // swap the loader view for this view
-                            synchronized (mViewCache) {
-                                if (containsAndIsValid(positionIndex)) {
-                                    RemoteViewsIndexInfo indexInfo = mViewCache[cacheIndex];
-                                    FrameLayout flipper = indexInfo.flipper;
-
-                                    // update the flipper
-                                    flipper.getChildAt(0).setVisibility(View.GONE);
-                                    boolean addNewView = true;
-                                    if (flipper.getChildCount() > 1) {
-                                        View v = flipper.getChildAt(1);
-                                        int typeId = ((Integer) v.getTag()).intValue();
-                                        if (typeId == indexInfo.typeId) {
-                                            // we can reapply since it is the same type
-                                            indexInfo.view.reapply(mContext, v);
-                                            v.setVisibility(View.VISIBLE);
-                                            if (v.getAnimation() != null) 
-                                                v.buildDrawingCache();
-                                            addNewView = false;
-                                        } else {
-                                            flipper.removeViewAt(1);
-                                        }
-                                    }
-                                    if (addNewView) {
-                                        View v = indexInfo.view.apply(mContext, flipper);
-                                        v.setTag(new Integer(indexInfo.typeId));
-                                        flipper.addView(v);
-                                    }
-                                }
-                            }
-                        }
-                    });
-                }
-            }
-        }
-
-        private RemoteViewsIndexInfo requestCachedIndexInfo(final int position) {
-            int indicesToLoadCount = 0;
-
-            synchronized (mViewCache) {
-                if (containsAndIsValid(position)) {
-                    // return the info if it exists in the window and is loaded
-                    return mViewCache[getCacheIndex(position)];
-                }
-
-                // if necessary update the window and load the new information
-                int centerPosition = (mViewCacheEndPosition + mViewCacheStartPosition) / 2;
-                if ((mViewCacheEndPosition <= mViewCacheStartPosition) || (Math.abs(position - centerPosition) > mCacheSlack)) {
-                    int newStartPosition = position - mHalfCacheSize;
-                    int newEndPosition = position + mHalfCacheSize;
-                    int frameSize = mHalfCacheSize / 4;
-                    int frameCount = (int) Math.ceil(mViewCache.length / (float) frameSize);
-
-                    // prune/add before the current start position
-                    int effectiveStart = Math.max(newStartPosition, 0);
-                    int effectiveEnd = Math.min(newEndPosition, getCount() - 1);
-
-                    // invalidate items in the queue
-                    int overlapStart = Math.max(mViewCacheStartPosition, effectiveStart);
-                    int overlapEnd = Math.min(Math.max(mViewCacheStartPosition, mViewCacheEndPosition), effectiveEnd);
-                    for (int i = 0; i < (frameSize * frameCount); ++i) {
-                        int index = newStartPosition + ((i % frameSize) * frameCount + (i / frameSize));
-                        
-                        if (index <= newEndPosition) {
-                            if ((overlapStart <= index) && (index <= overlapEnd)) {
-                                // load the stuff in the middle that has not already
-                                // been loaded
-                                if (!mViewCache[getCacheIndex(index)].isValid()) {
-                                    mTmpViewCacheLoadIndices[indicesToLoadCount++] = index;
-                                }
-                            } else if ((effectiveStart <= index) && (index <= effectiveEnd)) {
-                                // invalidate and load all new effective items
-                                mViewCache[getCacheIndex(index)].invalidate();
-                                mTmpViewCacheLoadIndices[indicesToLoadCount++] = index;
-                            } else {
-                                // invalidate all other cache indices (outside the effective start/end)
-                                // but don't load
-                                mViewCache[getCacheIndex(index)].invalidate();
-                            }
-                        }
                     }
 
-                    mViewCacheStartPosition = newStartPosition;
-                    mViewCacheEndPosition = newEndPosition;
+                    // Compose the loading view text
+                    TextView textView = new TextView(parent.getContext());
+                    textView.setText(com.android.internal.R.string.loading);
+                    textView.setHeight(mFirstViewHeight);
+                    textView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL);
+                    textView.setTextSize(18.0f);
+                    textView.setTextColor(Color.argb(96, 255, 255, 255));
+                    textView.setShadowLayer(2.0f, 0.0f, 1.0f, Color.BLACK);
+                    textView.setTag(new Integer(0));
+
+                    layout.addView(textView);
                 }
             }
 
-            // post items to be loaded
-            int length = 0;
-            synchronized (mViewCacheInfo) {
-                length = mViewCacheInfo.count;
-            }
-            if (indicesToLoadCount > 0) {
-                synchronized (mViewCacheLoadIndices) {
-                    mViewCacheLoadIndices.clear();
-                    for (int i = 0; i < indicesToLoadCount; ++i) {
-                        final int index = mTmpViewCacheLoadIndices[i];
-                        if (0 <= index && index < length) {
-                            mViewCacheLoadIndices.addLast(index);
-                        }
-                    }
-                }
+            return layout;
+        }
+    }
+
+    /**
+     * The meta-data associated with a single item in the cache.
+     */
+    private class RemoteViewsIndexMetaData {
+        int typeId;
+        long itemId;
+
+        public RemoteViewsIndexMetaData(RemoteViews v, long itemId) {
+            set(v, itemId);
+        }
+
+        public void set(RemoteViews v, long id) {
+            itemId = id;
+            if (v != null)
+                typeId = v.getLayoutId();
+            else
+                typeId = 0;
+        }
+    }
+
+    /**
+     *
+     */
+    private class FixedSizeRemoteViewsCache {
+        private static final String TAG = "FixedSizeRemoteViewsCache";
+
+        // The meta data related to all the RemoteViews, ie. count, is stable, etc.
+        private RemoteViewsMetaData mMetaData;
+
+        // The cache/mapping of position to RemoteViewsMetaData.  This set is guaranteed to be
+        // greater than or equal to the set of RemoteViews.
+        // Note: The reason that we keep this separate from the RemoteViews cache below is that this
+        // we still need to be able to access the mapping of position to meta data, without keeping
+        // the heavy RemoteViews around.  The RemoteViews cache is trimmed to fixed constraints wrt.
+        // memory and size, but this metadata cache will retain information until the data at the
+        // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged).
+        private HashMap<Integer, RemoteViewsIndexMetaData> mIndexMetaData;
+
+        // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses
+        // too much memory.
+        private HashMap<Integer, RemoteViews> mIndexRemoteViews;
+
+        // The set of indices that have been explicitly requested by the collection view
+        private HashSet<Integer> mRequestedIndices;
+
+        // The set of indices to load, including those explicitly requested, as well as those
+        // determined by the preloading algorithm to be prefetched
+        private HashSet<Integer> mLoadIndices;
+
+        // The lower and upper bounds of the preloaded range
+        private int mPreloadLowerBound;
+        private int mPreloadUpperBound;
+
+        // The bounds of this fixed cache, we will try and fill as many items into the cache up to
+        // the maxCount number of items, or the maxSize memory usage.
+        // The maxCountSlack is used to determine if a new position in the cache to be loaded is
+        // sufficiently ouside the old set, prompting a shifting of the "window" of items to be
+        // preloaded.
+        private int mMaxCount;
+        private int mMaxCountSlack;
+        private static final float sMaxCountSlackPercent = 0.75f;
+        private static final int sMaxMemoryUsage = 1024 * 1024;
+
+        public FixedSizeRemoteViewsCache(int maxCacheSize) {
+            mMaxCount = maxCacheSize;
+            mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2));
+            mPreloadLowerBound = 0;
+            mPreloadUpperBound = -1;
+            mMetaData = new RemoteViewsMetaData();
+            mIndexMetaData = new HashMap<Integer, RemoteViewsIndexMetaData>();
+            mIndexRemoteViews = new HashMap<Integer, RemoteViews>();
+            mRequestedIndices = new HashSet<Integer>();
+            mLoadIndices = new HashSet<Integer>();
+        }
+
+        public void insert(int position, RemoteViews v, long itemId) {
+            // Trim the cache if we go beyond the count
+            if (mIndexRemoteViews.size() >= mMaxCount) {
+                mIndexRemoteViews.remove(getFarthestPositionFrom(position));
             }
 
-            // return null so that a dummy view can be retrieved
+            // Trim the cache if we go beyond the available memory size constraints
+            while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryUsage) {
+                // Note: This is currently the most naive mechanism for deciding what to prune when
+                // we hit the memory limit.  In the future, we may want to calculate which index to
+                // remove based on both its position as well as it's current memory usage, as well
+                // as whether it was directly requested vs. whether it was preloaded by our caching
+                // mechanism.
+                mIndexRemoteViews.remove(getFarthestPositionFrom(position));
+            }
+
+            // Update the metadata cache
+            if (mIndexMetaData.containsKey(position)) {
+                final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position);
+                metaData.set(v, itemId);
+            } else {
+                mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId));
+            }
+            mIndexRemoteViews.put(position, v);
+        }
+
+        public RemoteViewsMetaData getMetaData() {
+            return mMetaData;
+        }
+        public RemoteViews getRemoteViewsAt(int position) {
+            if (mIndexRemoteViews.containsKey(position)) {
+                return mIndexRemoteViews.get(position);
+            }
+            return null;
+        }
+        public RemoteViewsIndexMetaData getMetaDataAt(int position) {
+            if (mIndexMetaData.containsKey(position)) {
+                return mIndexMetaData.get(position);
+            }
             return null;
         }
 
-        public View getView(int position, View convertView, ViewGroup parent) {
-            if (mServiceConnection.isConnected()) {
-                // create the flipper views if necessary (we have to do this now
-                // for all the flippers while we have the reference to the parent)
-                initializeLoadingViews(parent);
-
-                // request the item from the cache (queueing it to load if not
-                // in the cache already)
-                RemoteViewsIndexInfo indexInfo = requestCachedIndexInfo(position);
-
-                // update the flipper appropriately
-                synchronized (mViewCache) {
-                    int cacheIndex = getCacheIndex(position);
-                    FrameLayout flipper = mViewCache[cacheIndex].flipper;
-                    flipper.setVisibility(View.VISIBLE);
-                    flipper.setAlpha(1.0f);
-
-                    if (indexInfo == null) {
-                        // hide the item view and show the loading view
-                        flipper.getChildAt(0).setVisibility(View.VISIBLE);
-                        for (int i = 1; i < flipper.getChildCount(); ++i) {
-                            flipper.getChildAt(i).setVisibility(View.GONE);
-                        }
-                    } else {
-                        // hide the loading view and show the item view
-                        for (int i = 0; i < flipper.getChildCount() - 1; ++i) {
-                            flipper.getChildAt(i).setVisibility(View.GONE);
-                        }
-                        flipper.getChildAt(flipper.getChildCount() - 1).setVisibility(View.VISIBLE);
-                    }
-                    return flipper;
+        private int getRemoteViewsBitmapMemoryUsage() {
+            // Calculate the memory usage of all the RemoteViews bitmaps being cached
+            int mem = 0;
+            for (Integer i : mIndexRemoteViews.keySet()) {
+                final RemoteViews v = mIndexRemoteViews.get(i);
+                mem += v.estimateBitmapMemoryUsage();
+            }
+            return mem;
+        }
+        private int getFarthestPositionFrom(int pos) {
+            // Find the index farthest away and remove that
+            int maxDist = 0;
+            int maxDistIndex = -1;
+            for (int i : mIndexRemoteViews.keySet()) {
+                int dist = Math.abs(i-pos);
+                if (dist > maxDist) {
+                    maxDistIndex = i;
+                    maxDist = dist;
                 }
             }
-            return new View(mContext);
+            return maxDistIndex;
         }
 
-        private void initializeLoadingViews(ViewGroup parent) {
-            // ensure that the cache has the appropriate initial flipper
-            synchronized (mViewCache) {
-                if (mViewCache[0].flipper == null) {
-                    for (int i = 0; i < mViewCache.length; ++i) {
-                        FrameLayout flipper = new FrameLayout(mContext);
-                        if (mUserLoadingView != null) {
-                            // use the user-specified loading view
-                            flipper.addView(mUserLoadingView.apply(mContext, parent));
-                        } else {
-                            // calculate the original size of the first row for the loader view
-                            synchronized (mViewCacheInfo) {
-                                if (mFirstViewHeight < 0) {
-                                    View firstView = mFirstView.apply(mContext, parent);
-                                    firstView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
-                                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
-                                    mFirstViewHeight = firstView.getMeasuredHeight();
-                                }
-                            }
-
-                            // construct a new loader and add it to the flipper as the fallback
-                            // default view
-                            TextView textView = new TextView(mContext);
-                            textView.setText("Loading...");
-                            textView.setHeight(mFirstViewHeight);
-                            textView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL);
-                            textView.setTextSize(18.0f);
-                            textView.setTextColor(Color.argb(96, 255, 255, 255));
-                            textView.setShadowLayer(2.0f, 0.0f, 1.0f, Color.BLACK);
-
-                            flipper.addView(textView);
-                        }
-                        mViewCache[i].flipper = flipper;
-                    }
+        public void queueRequestedPositionToLoad(int position) {
+            synchronized (mLoadIndices) {
+                mRequestedIndices.add(position);
+                mLoadIndices.add(position);
+            }
+        }
+        public void queuePositionsToBePreloadedFromRequestedPosition(int position) {
+            // Check if we need to preload any items
+            if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) {
+                int center = (mPreloadUpperBound + mPreloadLowerBound) / 2;
+                if (Math.abs(position - center) < mMaxCountSlack) {
+                    return;
                 }
             }
-        }
 
-        public void startBackgroundLoader() {
-            // initialize the worker runnable
-            mBackgroundLoaderEnabled = true;
-            mWorkerQueue.post(new Runnable() {
-                @Override
-                public void run() {
-                    while (mBackgroundLoaderEnabled) {
-                        // notify the RemoteViews factory if necessary
-                        boolean isDataDirty = false;
-                        synchronized (mViewCacheInfo) {
-                            isDataDirty = mViewCacheInfo.isDataDirty;
-                            mViewCacheInfo.isDataDirty = false;
-                        }
-                        if (isDataDirty) {
-                            updateNotifyDataSetChanged();
-                        }
-
-                        int index = -1;
-                        synchronized (mViewCacheLoadIndices) {
-                            if (!mViewCacheLoadIndices.isEmpty()) {
-                                index = mViewCacheLoadIndices.removeFirst();
-                            }
-                        }
-                        if (index < 0) {
-                            // there were no items to load, so sleep for a bit
-                            try {
-                                Thread.sleep(10);
-                            } catch (InterruptedException e) {
-                                e.printStackTrace();
-                            }
-                        } else {
-                            // otherwise, try and load the item
-                            updateRemoteViewsInfo(index);
-                        }
-                    }
-                }
-            });
-        }
-
-        public void stopBackgroundLoader() {
-            // clear the items to be loaded
-            mBackgroundLoaderEnabled = false;
-            synchronized (mViewCacheLoadIndices) {
-                mViewCacheLoadIndices.clear();
+            int count = 0;
+            synchronized (mMetaData) {
+                count = mMetaData.count;
             }
-        }
+            synchronized (mLoadIndices) {
+                mLoadIndices.clear();
 
-        public long getItemId(int position) {
-            synchronized (mViewCache) {
-                if (containsAndIsValid(position)) {
-                    return mViewCache[getCacheIndex(position)].itemId;
-                }
-            }
-            return 0;
-        }
+                // Add all the requested indices
+                mLoadIndices.addAll(mRequestedIndices);
 
-        public int getItemViewType(int position) {
-            // synchronize to ensure that the type id/index map is updated synchronously
-            synchronized (mViewCache) {
-                if (containsAndIsValid(position)) {
-                    int viewId = mViewCache[getCacheIndex(position)].typeId;
-                    Map<Integer, Integer> typeMap = mViewCacheInfo.mTypeIdIndexMap;
-                    // we +1 because the default dummy view get view type 0
-                    if (typeMap.containsKey(viewId)) {
-                        return typeMap.get(viewId);
-                    } else {
-                        int newIndex = typeMap.size() + 1;
-                        typeMap.put(viewId, newIndex);
-                        return newIndex;
-                    }
-                }
-            }
-            // return the type of the default item
-            return 0;
-        }
-
-        public int getCount() {
-            synchronized (mViewCacheInfo) {
-                return mViewCacheInfo.count;
-            }
-        }
-
-        public int getViewTypeCount() {
-            synchronized (mViewCacheInfo) {
-                return mViewCacheInfo.viewTypeCount;
-            }
-        }
-
-        public boolean hasStableIds() {
-            synchronized (mViewCacheInfo) {
-                return mViewCacheInfo.hasStableIds;
-            }
-        }
-
-        public void flushCache() {
-            // clear the items to be loaded
-            synchronized (mViewCacheLoadIndices) {
-                mViewCacheLoadIndices.clear();
-            }
-
-            synchronized (mViewCache) {
-                // flush the internal cache and invalidate the adapter for future loads
-                mMainQueue.removeMessages(0);
-
-                for (int i = 0; i < mViewCache.length; ++i) {
-                    mViewCache[i].invalidate();
+                // Add all the preload indices
+                int halfMaxCount = mMaxCount / 2;
+                mPreloadLowerBound = position - halfMaxCount;
+                mPreloadUpperBound = position + halfMaxCount;
+                int effectiveLowerBound = Math.max(0, mPreloadLowerBound);
+                int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1);
+                for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) {
+                    mLoadIndices.add(i);
                 }
 
-                mViewCacheStartPosition = 0;
-                mViewCacheEndPosition = -1;
+                // But remove all the indices that have already been loaded and are cached
+                mLoadIndices.removeAll(mIndexRemoteViews.keySet());
+            }
+        }
+        public int getNextIndexToLoad() {
+            // We try and prioritize items that have been requested directly, instead
+            // of items that are loaded as a result of the caching mechanism
+            synchronized (mLoadIndices) {
+                // Prioritize requested indices to be loaded first
+                if (!mRequestedIndices.isEmpty()) {
+                    Integer i = mRequestedIndices.iterator().next();
+                    mRequestedIndices.remove(i);
+                    mLoadIndices.remove(i);
+                    return i.intValue();
+                }
+
+                // Otherwise, preload other indices as necessary
+                if (!mLoadIndices.isEmpty()) {
+                    Integer i = mLoadIndices.iterator().next();
+                    mLoadIndices.remove(i);
+                    return i.intValue();
+                }
+
+                return -1;
+            }
+        }
+
+        public boolean containsRemoteViewAt(int position) {
+            return mIndexRemoteViews.containsKey(position);
+        }
+        public boolean containsMetaDataAt(int position) {
+            return mIndexMetaData.containsKey(position);
+        }
+
+        public void reset() {
+            mPreloadLowerBound = 0;
+            mPreloadUpperBound = -1;
+            mIndexRemoteViews.clear();
+            mIndexMetaData.clear();
+            mMetaData.reset();
+            synchronized (mLoadIndices) {
+                mRequestedIndices.clear();
+                mLoadIndices.clear();
             }
         }
     }
@@ -672,24 +573,126 @@
         if (mIntent == null) {
             throw new IllegalArgumentException("Non-null Intent must be specified.");
         }
+        mRequestedViews = new RemoteViewsFrameLayoutRefSet();
 
         // initialize the worker thread
         mWorkerThread = new HandlerThread("RemoteViewsCache-loader");
         mWorkerThread.start();
         mWorkerQueue = new Handler(mWorkerThread.getLooper());
-        mWorkerPriorityQueue = new Handler(mWorkerThread.getLooper());
         mMainQueue = new Handler(Looper.myLooper());
-        mMainPriorityQueue = new Handler(Looper.myLooper());
 
         // initialize the cache and the service connection on startup
-        mViewCache = new RemoteViewsCache(25);
-        mServiceConnection = new RemoteViewsAdapterServiceConnection(callback);
+        mCache = new FixedSizeRemoteViewsCache(50);
+        mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback);
+        mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
         requestBindService();
     }
 
-    protected void finalize() throws Throwable {
-        // remember to unbind from the service when finalizing
-        unbindService();
+    private void loadNextIndexInBackground() {
+        mWorkerQueue.post(new Runnable() {
+            @Override
+            public void run() {
+                boolean isDataDirty = false;
+
+                // If the data set has changed, then notify the remote factory so that it can
+                // update its internals first.
+                final RemoteViewsMetaData metaData = mCache.getMetaData();
+                synchronized (metaData) {
+                    isDataDirty = metaData.isDataDirty;
+                    metaData.isDataDirty = false;
+                }
+                if (isDataDirty) {
+                    completeNotifyDataSetChanged();
+                }
+
+                // Get the next index to load
+                int position = -1;
+                synchronized (mCache) {
+                    position = mCache.getNextIndexToLoad();
+                }
+                if (position > -1) {
+                    // Load the item, and notify any existing RemoteViewsFrameLayouts
+                    updateRemoteViews(position);
+
+                    // Queue up for the next one to load
+                    loadNextIndexInBackground();
+                }
+            }
+        });
+    }
+
+    private void updateMetaData() {
+        if (mServiceConnection.isConnected()) {
+            try {
+                IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+
+                // get the properties/first view (so that we can use it to
+                // measure our dummy views)
+                boolean hasStableIds = factory.hasStableIds();
+                int viewTypeCount = factory.getViewTypeCount();
+                int count = factory.getCount();
+                RemoteViews loadingView = factory.getLoadingView();
+                RemoteViews firstView = null;
+                if ((count > 0) && (loadingView == null)) {
+                    firstView = factory.getViewAt(0);
+                }
+                final RemoteViewsMetaData metaData = mCache.getMetaData();
+                synchronized (metaData) {
+                    metaData.hasStableIds = hasStableIds;
+                    metaData.viewTypeCount = viewTypeCount + 1;
+                    metaData.count = count;
+                    metaData.setLoadingViewTemplates(loadingView, firstView);
+                }
+            } catch (Exception e) {
+                // print the error
+                Log.e(TAG, "Error in requestMetaData(): " + e.getMessage());
+
+                // reset any members after the failed call
+                final RemoteViewsMetaData metaData = mCache.getMetaData();
+                synchronized (metaData) {
+                    metaData.reset();
+                }
+            }
+        }
+    }
+
+    private void updateRemoteViews(final int position) {
+        if (mServiceConnection.isConnected()) {
+            IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+
+            // Load the item information from the remote service
+            RemoteViews remoteViews = null;
+            long itemId = 0;
+            try {
+                remoteViews = factory.getViewAt(position);
+                itemId = factory.getItemId(position);
+            } catch (Exception e) {
+                // Print the error
+                Log.e(TAG, "Error in updateRemoteViewsInfo(" + position + "): " +
+                        e.getMessage());
+                e.printStackTrace();
+
+                // Return early to prevent additional work in re-centering the view cache, and
+                // swapping from the loading view
+                return;
+            }
+
+            synchronized (mCache) {
+                // Cache the RemoteViews we loaded
+                mCache.insert(position, remoteViews, itemId);
+
+                // Notify all the views that we have previously returned for this index that
+                // there is new data for it.
+                final RemoteViews rv = remoteViews;
+                final int typeId = mCache.getMetaDataAt(position).typeId;
+                mMainQueue.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId);
+                    }
+                });
+            }
+        }
     }
 
     public Intent getRemoteViewsServiceIntent() {
@@ -698,37 +701,132 @@
 
     public int getCount() {
         requestBindService();
-        return mViewCache.getCount();
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.count;
+        }
     }
 
     public Object getItem(int position) {
-        // disallow arbitrary object to be associated with an item for the time being
+        // Disallow arbitrary object to be associated with an item for the time being
         return null;
     }
 
     public long getItemId(int position) {
         requestBindService();
-        return mViewCache.getItemId(position);
+        synchronized (mCache) {
+            if (mCache.containsMetaDataAt(position)) {
+                return mCache.getMetaDataAt(position).itemId;
+            }
+            return 0;
+        }
     }
 
     public int getItemViewType(int position) {
         requestBindService();
-        return mViewCache.getItemViewType(position);
+        int typeId = 0;
+        synchronized (mCache) {
+            if (mCache.containsMetaDataAt(position)) {
+                typeId = mCache.getMetaDataAt(position).typeId;
+            } else {
+                return 0;
+            }
+        }
+
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.getMappedViewType(typeId);
+        }
+    }
+
+    /**
+     * Returns the item type id for the specified convert view.  Returns -1 if the convert view
+     * is invalid.
+     */
+    private int getConvertViewTypeId(View convertView) {
+        int typeId = -1;
+        if (convertView != null && convertView.getTag() != null) {
+            typeId = (Integer) convertView.getTag();
+        }
+        return typeId;
     }
 
     public View getView(int position, View convertView, ViewGroup parent) {
         requestBindService();
-        return mViewCache.getView(position, convertView, parent);
+        if (mServiceConnection.isConnected()) {
+            // "Request" an index so that we can queue it for loading, initiate subsequent
+            // preloading, etc.
+            synchronized (mCache) {
+                // Queue up other indices to be preloaded based on this position
+                mCache.queuePositionsToBePreloadedFromRequestedPosition(position);
+
+                RemoteViewsFrameLayout layout = (RemoteViewsFrameLayout) convertView;
+                View convertViewChild = null;
+                int convertViewTypeId = 0;
+                if (convertView != null) {
+                    convertViewChild = layout.getChildAt(0);
+                    convertViewTypeId = getConvertViewTypeId(convertViewChild);
+                }
+
+                // Second, we try and retrieve the RemoteViews from the cache, returning a loading
+                // view and queueing it to be loaded if it has not already been loaded.
+                if (mCache.containsRemoteViewAt(position)) {
+                    Context context = parent.getContext();
+                    RemoteViews rv = mCache.getRemoteViewsAt(position);
+                    int typeId = mCache.getMetaDataAt(position).typeId;
+
+                    // Reuse the convert view where possible
+                    if (convertView != null) {
+                        if (convertViewTypeId == typeId) {
+                            rv.reapply(context, convertViewChild);
+                            return convertView;
+                        }
+                    }
+
+                    // Otherwise, create a new view to be returned
+                    View newView = rv.apply(context, parent);
+                    newView.setTag(new Integer(typeId));
+                    if (convertView != null) {
+                        layout.removeAllViews();
+                    } else {
+                        layout = new RemoteViewsFrameLayout(context);
+                    }
+                    layout.addView(newView);
+                    return layout;
+                } else {
+                    // If the cache does not have the RemoteViews at this position, then create a
+                    // loading view and queue the actual position to be loaded in the background
+                    RemoteViewsFrameLayout loadingView = null;
+                    final RemoteViewsMetaData metaData = mCache.getMetaData();
+                    synchronized (metaData) {
+                        loadingView = metaData.createLoadingView(position, convertView, parent);
+                    }
+
+                    mRequestedViews.add(position, loadingView);
+                    mCache.queueRequestedPositionToLoad(position);
+                    loadNextIndexInBackground();
+
+                    return loadingView;
+                }
+            }
+        }
+        return new View(parent.getContext());
     }
 
     public int getViewTypeCount() {
         requestBindService();
-        return mViewCache.getViewTypeCount();
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.viewTypeCount;
+        }
     }
 
     public boolean hasStableIds() {
         requestBindService();
-        return mViewCache.hasStableIds();
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.hasStableIds;
+        }
     }
 
     public boolean isEmpty() {
@@ -736,14 +834,49 @@
     }
 
     public void notifyDataSetChanged() {
-        // flush the cache so that we can reload new items from the service
-        mViewCache.flushCache();
+        synchronized (mCache) {
+            // Flush the cache so that we can reload new items from the service
+            mCache.reset();
+        }
 
-        // notify the factory that it's data may no longer be valid
-        mViewCache.onNotifyDataSetChanged();
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            // Set flag to calls the remote factory's onDataSetChanged() on the next worker loop
+            metaData.isDataDirty = true;
+        }
+
+        // Note: we do not call super.notifyDataSetChanged() until the RemoteViewsFactory has had
+        // a chance to update itself, and return new meta data associated with the new data.  After
+        // which completeNotifyDataSetChanged() is called.
     }
 
-    public void completeNotifyDataSetChanged() {
+    private void completeNotifyDataSetChanged() {
+        // Complete the actual notifyDataSetChanged() call initiated earlier
+        if (mServiceConnection.isConnected()) {
+            IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+            try {
+                factory.onDataSetChanged();
+            } catch (Exception e) {
+                Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
+
+                // Return early to prevent from further being notified (since nothing has changed)
+                return;
+            }
+        }
+
+        // Re-request the new metadata (only after the notification to the factory)
+        updateMetaData();
+
+        // Propagate the notification back to the base adapter
+        mMainQueue.post(new Runnable() {
+            @Override
+            public void run() {
+                superNotifyDataSetChanged();
+            }
+        });
+    }
+
+    private void superNotifyDataSetChanged() {
         super.notifyDataSetChanged();
     }
 
@@ -755,10 +888,4 @@
 
         return mServiceConnection.isConnected();
     }
-
-    private void unbindService() {
-        if (mServiceConnection.isConnected()) {
-            mContext.unbindService(mServiceConnection);
-        }
-    }
 }