Prevent RecyclerView from doing another layout while processing updates

This CL fixes a bug in RecyclerView where it would try to consume
pending udpates twice if it receives a call to layout (focus search etc)
while processing updates.

Bug: 32316425
Test: RecyclerViewLayoutTest::triggerFocusSearchInOnRecycledCallback
Change-Id: I3dccc7fb8ef5fffc260220254a1b6605d88b6e77
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 7bb8a3f..e159ac6 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -1577,6 +1577,7 @@
                 .hasAnyUpdateTypes(UpdateOp.ADD | UpdateOp.REMOVE | UpdateOp.MOVE)) {
             TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
             eatRequestLayout();
+            onEnterLayoutOrScroll();
             mAdapterHelper.preProcess();
             if (!mLayoutRequestEaten) {
                 if (hasUpdatedView()) {
@@ -1587,6 +1588,7 @@
                 }
             }
             resumeRequestLayout(true);
+            onExitLayoutOrScroll();
             TraceCompat.endSection();
         } else if (mAdapterHelper.hasPendingUpdates()) {
             TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 770ab71..80adf64 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -97,6 +97,64 @@
     }
 
     @Test
+    public void triggerFocusSearchInOnRecycledCallback() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity()) {
+            @Override
+            void consumePendingUpdateOperations() {
+                try {
+                    super.consumePendingUpdateOperations();
+                } catch (Throwable t) {
+                    postExceptionToInstrumentation(t);
+                }
+            }
+        };
+        final AtomicBoolean receivedOnRecycled = new AtomicBoolean(false);
+        final TestAdapter adapter = new TestAdapter(20) {
+            @Override
+            public void onViewRecycled(TestViewHolder holder) {
+                super.onViewRecycled(holder);
+                if (receivedOnRecycled.getAndSet(true)) {
+                    return;
+                }
+                rv.focusSearch(rv.getChildAt(0), View.FOCUS_FORWARD);
+            }
+        };
+        final AtomicInteger layoutCnt = new AtomicInteger(5);
+        TestLayoutManager tlm = new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                detachAndScrapAttachedViews(recycler);
+                layoutRange(recycler, 0, layoutCnt.get());
+                layoutLatch.countDown();
+            }
+        };
+        rv.setLayoutManager(tlm);
+        rv.setAdapter(adapter);
+        tlm.expectLayouts(1);
+        setRecyclerView(rv);
+        tlm.waitForLayout(2);
+
+        layoutCnt.set(4);
+        tlm.expectLayouts(1);
+        requestLayoutOnUIThread(rv);
+        tlm.waitForLayout(1);
+
+        assertThat("test sanity", rv.mRecycler.mCachedViews.size(), is(1));
+        tlm.expectLayouts(1);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                adapter.notifyItemChanged(4);
+                rv.smoothScrollBy(0, 1);
+            }
+        });
+        checkForMainThreadException();
+        tlm.waitForLayout(2);
+        assertThat("test sanity", rv.mRecycler.mCachedViews.size(), is(0));
+        assertThat(receivedOnRecycled.get(), is(true));
+    }
+
+    @Test
     public void detachAttachGetReadyWithoutChanges() throws Throwable {
         detachAttachGetReady(false, false, false);
     }