Handling the notification for custom ObjectAdapter case.

Change-Id: Ifc3c09aefce1409bb31c0fb5f564db6d704ffae5
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java b/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
index de4546c..5f18285 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
@@ -1,9 +1,11 @@
 package android.support.v17.leanback.app;
 
-import android.support.v17.leanback.widget.DividerRow;
-import android.support.v17.leanback.widget.ListRow;
+import android.os.Handler;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.CursorObjectAdapter;
 import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
 
 /**
  * Wrapper class for {@link ObjectAdapter} used by {@link BrowseFragment} to initialize
@@ -17,62 +19,67 @@
  * bounds to reflect the latest data.
  */
 class ListRowDataAdapter extends ObjectAdapter {
+    public static final int ON_ITEM_RANGE_CHANGED = 2;
+    public static final int ON_ITEM_RANGE_INSERTED = 4;
+    public static final int ON_ITEM_RANGE_REMOVED = 8;
+    public static final int ON_CHANGED = 16;
+
     private final ObjectAdapter mAdapter;
     private int mLastVisibleRowIndex;
+    private Handler mHandler;
+    private Update mPendingUpdate;
+    private int mPendingUpdateCount;
+
+    private static class Update {
+        int eventType;
+        int positionStart;
+        int itemCount;
+
+        public Update(int type, int positionStart, int itemCount) {
+            this.eventType = type;
+            this.positionStart = positionStart;
+            this.itemCount = itemCount;
+        }
+    }
+
+    private Runnable notificationTask = new Runnable() {
+        @Override
+        public void run() {
+            if (mPendingUpdateCount == 0) {
+                return;
+            } else if (mPendingUpdateCount == 1 && mPendingUpdate != null) {
+                doNotify(
+                        mPendingUpdate.eventType,
+                        mPendingUpdate.positionStart,
+                        mPendingUpdate.itemCount);
+            } else {
+                notifyChanged();
+            }
+            mPendingUpdate = null;
+            mPendingUpdateCount = 0;
+        }
+    };
 
     public ListRowDataAdapter(ObjectAdapter adapter) {
         super(adapter.getPresenterSelector());
         this.mAdapter = adapter;
         initialize();
-        mAdapter.registerObserver(new DataObserver() {
 
-            @Override
-            public void onItemRangeChanged(int positionStart, int itemCount) {
-                if (positionStart <= mLastVisibleRowIndex) {
-                    notifyItemRangeChanged(positionStart,
-                            Math.min(itemCount, mLastVisibleRowIndex - positionStart + 1));
-                }
-            }
-
-            @Override
-            public void onItemRangeInserted(int positionStart, int itemCount) {
-                if (positionStart <= mLastVisibleRowIndex) {
-                    mLastVisibleRowIndex += itemCount;
-                    notifyItemRangeInserted(positionStart, itemCount);
-                    return;
-                }
-
-                int lastVisibleRowIndex = mLastVisibleRowIndex;
-                initialize();
-                if (mLastVisibleRowIndex > lastVisibleRowIndex) {
-                    int totalItems = mLastVisibleRowIndex - lastVisibleRowIndex;
-                    notifyItemRangeInserted(lastVisibleRowIndex + 1, totalItems);
-                }
-            }
-
-            @Override
-            public void onItemRangeRemoved(int positionStart, int itemCount) {
-                if (positionStart + itemCount -  1 < mLastVisibleRowIndex) {
-                    mLastVisibleRowIndex -= itemCount;
-                    notifyItemRangeRemoved(positionStart, itemCount);
-                    return;
-                }
-
-                int lastVisibleRowIndex = mLastVisibleRowIndex;
-                initialize();
-                int totalItems = lastVisibleRowIndex - mLastVisibleRowIndex;
-                if (totalItems > 0) {
-                    notifyItemRangeRemoved(
-                            Math.min(lastVisibleRowIndex + 1, positionStart), totalItems);
-                }
-            }
-
-            @Override
-            public void onChanged() {
-                initialize();
-                notifyChanged();
-            }
-        });
+        // If an user implements its own ObjectAdapter, notification corresponding to data
+        // updates can be batched e.g. remove, add might be followed by notifyRemove, notifyAdd.
+        // But underlying data would have changed during the notifyRemove call by the previous add
+        // operation. To handle this case, we enqueue the updates in a queue and have a worker
+        // service that queue. The common case will be to have a single pending update in the queue.
+        // But in case the worker encounters multiple updates in the queue, it will send a
+        // notifyChanged() call to RecyclerView forcing it to do a full refresh.
+        if ((adapter instanceof ArrayObjectAdapter)
+                || (adapter instanceof CursorObjectAdapter)
+                || (adapter instanceof SparseArrayObjectAdapter)) {
+            mAdapter.registerObserver(new SimpleDataObserver());
+        } else {
+            mHandler = new Handler();
+            mAdapter.registerObserver(new QueueBasedDataObserver());
+        }
     }
 
     private void initialize() {
@@ -96,4 +103,120 @@
     public Object get(int index) {
         return mAdapter.get(index);
     }
-}
\ No newline at end of file
+
+    private void doNotify(int eventType, int positionStart, int itemCount) {
+        switch (eventType) {
+            case ON_ITEM_RANGE_CHANGED:
+                notifyItemRangeChanged(positionStart, itemCount);
+                break;
+            case ON_ITEM_RANGE_INSERTED:
+                notifyItemRangeInserted(positionStart, itemCount);
+                break;
+            case ON_ITEM_RANGE_REMOVED:
+                notifyItemRangeRemoved(positionStart, itemCount);
+                break;
+            case ON_CHANGED:
+                notifyChanged();
+            default:
+                throw new IllegalArgumentException("Invalid event type " + eventType);
+        }
+    }
+
+    private class SimpleDataObserver extends DataObserver {
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            if (positionStart <= mLastVisibleRowIndex) {
+                onEventFired(ON_ITEM_RANGE_CHANGED, positionStart,
+                        Math.min(itemCount, mLastVisibleRowIndex - positionStart + 1));
+            }
+        }
+
+        @Override
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            if (positionStart <= mLastVisibleRowIndex) {
+                mLastVisibleRowIndex += itemCount;
+                onEventFired(ON_ITEM_RANGE_INSERTED, positionStart, itemCount);
+                return;
+            }
+
+            int lastVisibleRowIndex = mLastVisibleRowIndex;
+            initialize();
+            if (mLastVisibleRowIndex > lastVisibleRowIndex) {
+                int totalItems = mLastVisibleRowIndex - lastVisibleRowIndex;
+                onEventFired(ON_ITEM_RANGE_INSERTED, lastVisibleRowIndex + 1, totalItems);
+            }
+        }
+
+        @Override
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            if (positionStart + itemCount - 1 < mLastVisibleRowIndex) {
+                mLastVisibleRowIndex -= itemCount;
+                onEventFired(ON_ITEM_RANGE_REMOVED, positionStart, itemCount);
+                return;
+            }
+
+            int lastVisibleRowIndex = mLastVisibleRowIndex;
+            initialize();
+            int totalItems = lastVisibleRowIndex - mLastVisibleRowIndex;
+            if (totalItems > 0) {
+                onEventFired(ON_ITEM_RANGE_REMOVED,
+                        Math.min(lastVisibleRowIndex + 1, positionStart),
+                        totalItems);
+            }
+        }
+
+        @Override
+        public void onChanged() {
+            initialize();
+            onEventFired(ON_CHANGED, -1, -1);
+        }
+
+        protected void onEventFired(int eventType, int positionStart, int itemCount) {
+            doNotify(eventType, positionStart, itemCount);
+        }
+    }
+
+    private class QueueBasedDataObserver extends SimpleDataObserver {
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            incrementAndPost();
+            super.onItemRangeChanged(positionStart, itemCount);
+        }
+
+        @Override
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            incrementAndPost();
+            super.onItemRangeInserted(positionStart, itemCount);
+        }
+
+        @Override
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            incrementAndPost();
+            super.onItemRangeRemoved(positionStart, itemCount);
+        }
+
+        @Override
+        public void onChanged() {
+            incrementAndPost();
+            super.onChanged();
+        }
+
+        @Override
+        protected void onEventFired(
+                final int eventType, final int positionStart, final int itemCount) {
+
+            if (mPendingUpdateCount == 1) {
+                mPendingUpdate = new Update(eventType, positionStart, itemCount);
+            }
+        }
+
+        private void incrementAndPost() {
+            mPendingUpdateCount++;
+            if (mPendingUpdateCount == 1) {
+                mHandler.post(notificationTask);
+            }
+        }
+    }
+}