Caching the FixedSizeRemoteViewsCaches across rotation

-> This prevents unnecessary flashing of collection widgets on rotation

Change-Id: Id29d4952aa640ca90b31dc3e02e2402cc0fb84d3
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 19aef8e..437da59 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -16,8 +16,6 @@
 
 package android.widget;
 
-import com.android.internal.R;
-
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.TypedArray;
@@ -69,6 +67,8 @@
 import android.view.inputmethod.InputConnectionWrapper;
 import android.view.inputmethod.InputMethodManager;
 
+import com.android.internal.R;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -1813,6 +1813,10 @@
         }
         ss.checkedItemCount = mCheckedItemCount;
 
+        if (mRemoteAdapter != null) {
+            mRemoteAdapter.saveRemoteViewsCache();
+        }
+
         return ss;
     }
 
@@ -5974,6 +5978,9 @@
         mDeferNotifyDataSetChanged = false;
         // Otherwise, create a new RemoteViewsAdapter for binding
         mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this);
+        if (mRemoteAdapter.isDataReady()) {
+            setAdapter(mRemoteAdapter);
+        }
     }
 
     /**
diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java
index c557963..2266cea 100644
--- a/core/java/android/widget/AdapterViewAnimator.java
+++ b/core/java/android/widget/AdapterViewAnimator.java
@@ -813,6 +813,9 @@
     @Override
     public Parcelable onSaveInstanceState() {
         Parcelable superState = super.onSaveInstanceState();
+        if (mRemoteViewsAdapter != null) {
+            mRemoteViewsAdapter.saveRemoteViewsCache();
+        }
         return new SavedState(superState, mWhichChild);
     }
 
@@ -984,6 +987,9 @@
         mDeferNotifyDataSetChanged = false;
         // Otherwise, create a new RemoteViewsAdapter for binding
         mRemoteViewsAdapter = new RemoteViewsAdapter(getContext(), intent, this);
+        if (mRemoteViewsAdapter.isDataReady()) {
+            setAdapter(mRemoteViewsAdapter);
+        }
     }
 
     @Override
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index aa95397..e9c753a 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -29,12 +29,16 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.util.Log;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
+import android.widget.RemoteViewsService.RemoteViewsFactory;
 
 import com.android.internal.widget.IRemoteViewsAdapterConnection;
 import com.android.internal.widget.IRemoteViewsFactory;
@@ -83,6 +87,26 @@
     private Handler mWorkerQueue;
     private Handler mMainQueue;
 
+    // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data
+    // structures;
+    private static final HashMap<Pair<Intent.FilterComparison, Integer>, Parcel>
+            sCachedRemoteViewsCaches = new HashMap<Pair<Intent.FilterComparison, Integer>,
+            Parcel>();
+    private static final HashMap<Pair<Intent.FilterComparison, Integer>, Runnable>
+            sRemoteViewsCacheRemoveRunnables = new HashMap<Pair<Intent.FilterComparison, Integer>,
+            Runnable>();
+    private static HandlerThread sCacheRemovalThread;
+    private static Handler sCacheRemovalQueue;
+
+    // We keep the cache around for a duration after onSaveInstanceState for use on re-inflation.
+    // If a new RemoteViewsAdapter with the same intent / widget id isn't constructed within this
+    // duration, the cache is dropped.
+    private static final int REMOTE_VIEWS_CACHE_DURATION = 5000;
+
+    // Used to indicate to the AdapterView that it can use this Adapter immediately after
+    // construction (happens when we have a cached FixedSizeRemoteViewsCache).
+    private boolean mDataReady = false;
+
     /**
      * An interface for the RemoteAdapter to notify other classes when adapters
      * are actually connected to/disconnected from their actual services.
@@ -331,7 +355,7 @@
     /**
      * The meta-data associated with the cache in it's current state.
      */
-    private static class RemoteViewsMetaData {
+    private static class RemoteViewsMetaData implements Parcelable {
         int count;
         int viewTypeCount;
         boolean hasStableIds;
@@ -350,6 +374,51 @@
             reset();
         }
 
+        public RemoteViewsMetaData(Parcel src) {
+            count = src.readInt();
+            viewTypeCount = src.readInt();
+            hasStableIds = src.readInt() == 0 ? false : true;
+            mFirstViewHeight = src.readInt();
+            if (src.readInt() != 0) {
+                mUserLoadingView = new RemoteViews(src);
+            }
+            if (src.readInt() != 0) {
+                mFirstView = new RemoteViews(src);
+            }
+            int count = src.readInt();
+            for (int i = 0; i < count; i++) {
+                mTypeIdIndexMap.put(src.readInt(), src.readInt());
+            }
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(count);
+            dest.writeInt(viewTypeCount);
+            dest.writeInt(hasStableIds ? 1 : 0);
+            dest.writeInt(mFirstViewHeight);
+            dest.writeInt(mUserLoadingView != null ? 1 : 0);
+            if (mUserLoadingView != null) {
+                mUserLoadingView.writeToParcel(dest, flags);
+            }
+            dest.writeInt(mFirstView != null ? 1 : 0);
+            if (mFirstView != null) {
+                mFirstView.writeToParcel(dest, flags);
+            }
+
+            int count = mTypeIdIndexMap.size();
+            dest.writeInt(count);
+            for (Integer key: mTypeIdIndexMap.keySet()) {
+                dest.writeInt(key);
+                dest.writeInt(mTypeIdIndexMap.get(key));
+            }
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
         public void set(RemoteViewsMetaData d) {
             synchronized (d) {
                 count = d.count;
@@ -460,7 +529,7 @@
     /**
      * The meta-data associated with a single item in the cache.
      */
-    private static class RemoteViewsIndexMetaData {
+    private static class RemoteViewsIndexMetaData implements Parcelable {
         int typeId;
         long itemId;
         boolean isRequested;
@@ -469,6 +538,22 @@
             set(v, itemId, requested);
         }
 
+        public RemoteViewsIndexMetaData(Parcel src) {
+            typeId = src.readInt();
+            itemId = src.readLong();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(typeId);
+            dest.writeLong(itemId);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
         public void set(RemoteViews v, long id, boolean requested) {
             itemId = id;
             if (v != null) {
@@ -478,12 +563,14 @@
             }
             isRequested = requested;
         }
+
+
     }
 
     /**
      *
      */
-    private static class FixedSizeRemoteViewsCache {
+    private static class FixedSizeRemoteViewsCache implements Parcelable {
         private static final String TAG = "FixedSizeRemoteViewsCache";
 
         // The meta data related to all the RemoteViews, ie. count, is stable, etc.
@@ -545,6 +632,57 @@
             mLoadIndices = new HashSet<Integer>();
         }
 
+        public FixedSizeRemoteViewsCache(Parcel src) {
+            mMaxCount = src.readInt();
+            mMaxCountSlack = src.readInt();
+            mPreloadLowerBound = src.readInt();
+            mPreloadUpperBound = src.readInt();
+            mMetaData = new RemoteViewsMetaData(src);
+            int count = src.readInt();
+            mIndexMetaData = new HashMap<Integer, RemoteViewsIndexMetaData>();
+            for (int i = 0; i < count; i++) {
+                mIndexMetaData.put(src.readInt(), new RemoteViewsIndexMetaData(src));
+            }
+            count = src.readInt();
+            mIndexRemoteViews = new HashMap<Integer, RemoteViews>();
+            for (int i = 0; i < count; i++) {
+                mIndexRemoteViews.put(src.readInt(), new RemoteViews(src));
+            }
+
+            mTemporaryMetaData = new RemoteViewsMetaData();
+            mRequestedIndices = new HashSet<Integer>();
+            mLastRequestedIndex = -1;
+            mLoadIndices = new HashSet<Integer>();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mMaxCount);
+            dest.writeInt(mMaxCountSlack);
+            dest.writeInt(mPreloadLowerBound);
+            dest.writeInt(mPreloadUpperBound);
+            mMetaData.writeToParcel(dest, 0);
+
+            // We write the index data and cache
+            int count = mIndexMetaData.size();
+            dest.writeInt(count);
+            for (Integer key: mIndexMetaData.keySet()) {
+                dest.writeInt(key);
+                mIndexMetaData.get(key).writeToParcel(dest, flags);
+            }
+            count = mIndexRemoteViews.size();
+            dest.writeInt(count);
+            for (Integer key: mIndexRemoteViews.keySet()) {
+                dest.writeInt(key);
+                mIndexRemoteViews.get(key).writeToParcel(dest, flags);
+            }
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
         public void insert(int position, RemoteViews v, long itemId, boolean isRequested) {
             // Trim the cache if we go beyond the count
             if (mIndexRemoteViews.size() >= mMaxCount) {
@@ -747,11 +885,30 @@
         mWorkerQueue = new Handler(mWorkerThread.getLooper());
         mMainQueue = new Handler(Looper.myLooper(), this);
 
+        if (sCacheRemovalThread == null) {
+            sCacheRemovalThread = new HandlerThread("RemoteViewsAdapter-cachePruner");
+            sCacheRemovalThread.start();
+            sCacheRemovalQueue = new Handler(sCacheRemovalThread.getLooper());
+        }
+
         // Initialize the cache and the service connection on startup
-        mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize);
         mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback);
         mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
-        requestBindService();
+
+        Pair<Intent.FilterComparison, Integer> key = new Pair<Intent.FilterComparison, Integer>
+                (new Intent.FilterComparison(mIntent), mAppWidgetId);
+
+        synchronized(sCachedRemoteViewsCaches) {
+            if (sCachedRemoteViewsCaches.containsKey(key)) {
+                Parcel src = sCachedRemoteViewsCaches.get(key);
+                src.setDataPosition(0);
+                mCache = new FixedSizeRemoteViewsCache(src);
+                mDataReady = true;
+            } else {
+                mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize);
+                requestBindService();
+            }
+        }
     }
 
     @Override
@@ -765,6 +922,44 @@
         }
     }
 
+    public boolean isDataReady() {
+        return mDataReady;
+    }
+
+    public void saveRemoteViewsCache() {
+        final Pair<Intent.FilterComparison, Integer> key = new Pair<Intent.FilterComparison,
+                Integer> (new Intent.FilterComparison(mIntent), mAppWidgetId);
+
+        synchronized(sCachedRemoteViewsCaches) {
+            // If we already have a remove runnable posted for this key, remove it.
+            if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
+                sCacheRemovalQueue.removeCallbacks(sRemoteViewsCacheRemoveRunnables.get(key));
+                sRemoteViewsCacheRemoveRunnables.remove(key);
+            }
+
+            Parcel p = Parcel.obtain();
+            synchronized(mCache) {
+                mCache.writeToParcel(p, 0);
+            }
+            sCachedRemoteViewsCaches.put(key, p);
+            Runnable r = new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (sCachedRemoteViewsCaches) {
+                        if (sCachedRemoteViewsCaches.containsKey(key)) {
+                            sCachedRemoteViewsCaches.remove(key);
+                        }
+                        if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
+                            sRemoteViewsCacheRemoveRunnables.remove(key);
+                        }
+                    }
+                }
+            };
+            sRemoteViewsCacheRemoveRunnables.put(key, r);
+            sCacheRemovalQueue.postDelayed(r, REMOTE_VIEWS_CACHE_DURATION);
+        }
+    }
+
     private void loadNextIndexInBackground() {
         mWorkerQueue.post(new Runnable() {
             @Override