RecyclerView: dont clear app provided accessibility delegate

If app provides accessibility delegate in onCreateView, the delegate
is unexpectedly cleared when item is sent to RecyclerViewPool.

Test: notClearCustomViewDelegate clearItemDelegateWhenGoesToPool
Bug: 37672983
Change-Id: I688f94272956501daf8bd133acf6d15310335d6f
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index cb8c141..58641c8 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -5415,7 +5415,7 @@
             mAdapter.bindViewHolder(holder, offsetPosition);
             long endBindNs = getNanoTime();
             mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
-            attachAccessibilityDelegate(holder.itemView);
+            attachAccessibilityDelegateOnBind(holder);
             if (mState.isPreLayout()) {
                 holder.mPreLayoutPosition = position;
             }
@@ -5694,14 +5694,16 @@
             return holder;
         }
 
-        private void attachAccessibilityDelegate(View itemView) {
+        private void attachAccessibilityDelegateOnBind(ViewHolder holder) {
             if (isAccessibilityEnabled()) {
+                final View itemView = holder.itemView;
                 if (ViewCompat.getImportantForAccessibility(itemView)
                         == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
                     ViewCompat.setImportantForAccessibility(itemView,
                             ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
                 }
                 if (!ViewCompat.hasAccessibilityDelegate(itemView)) {
+                    holder.addFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
                     ViewCompat.setAccessibilityDelegate(itemView,
                             mAccessibilityDelegate.getItemDelegate());
                 }
@@ -5902,7 +5904,10 @@
          */
         void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
             clearNestedRecyclerViewIfNotNested(holder);
-            ViewCompat.setAccessibilityDelegate(holder.itemView, null);
+            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
+                holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
+                ViewCompat.setAccessibilityDelegate(holder.itemView, null);
+            }
             if (dispatchRecycled) {
                 dispatchViewRecycled(holder);
             }
@@ -10352,6 +10357,12 @@
          */
         static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
 
+        /**
+         * Flags that RecyclerView assigned {@link RecyclerViewAccessibilityDelegate
+         * #getItemDelegate()} in onBindView when app does not provide a delegate.
+         */
+        static final int FLAG_SET_A11Y_ITEM_DELEGATE = 1 << 14;
+
         private int mFlags;
 
         private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java
index bd14d2c..54bedb0 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java
@@ -18,6 +18,11 @@
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -25,12 +30,16 @@
 
 import android.annotation.TargetApi;
 import android.os.Build;
+import android.support.annotation.RequiresApi;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.view.AccessibilityDelegateCompat;
 import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -150,4 +159,144 @@
             }
         }
     }
+
+    @Test
+    @RequiresApi(14)
+    public void notClearCustomViewDelegate() throws Throwable {
+        final RecyclerView recyclerView = new RecyclerView(getActivity()) {
+            @Override
+            boolean isAccessibilityEnabled() {
+                return true;
+            }
+        };
+        final int[] layoutStart = new int[] {0};
+        final int layoutCount = 5;
+        final TestLayoutManager layoutManager = new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                detachAndScrapAttachedViews(recycler);
+                removeAndRecycleScrapInt(recycler);
+                layoutRange(recycler, layoutStart[0], layoutStart[0] + layoutCount);
+                if (layoutLatch != null) {
+                    layoutLatch.countDown();
+                }
+            }
+        };
+        final AccessibilityDelegateCompat delegateCompat = new AccessibilityDelegateCompat() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host,
+                    AccessibilityNodeInfoCompat info) {
+                super.onInitializeAccessibilityNodeInfo(host, info);
+                info.setChecked(true);
+            }
+        };
+        final TestAdapter adapter = new TestAdapter(100) {
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                    int viewType) {
+                TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
+                ViewCompat.setAccessibilityDelegate(vh.itemView, delegateCompat);
+                return vh;
+            }
+        };
+        layoutManager.expectLayouts(1);
+        recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100);
+        recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool
+        recyclerView.setLayoutManager(layoutManager);
+        setRecyclerView(recyclerView);
+        recyclerView.setAdapter(adapter);
+        layoutManager.waitForLayout(1);
+
+        assertEquals(layoutCount, recyclerView.getChildCount());
+        ArrayList<View> children = new ArrayList();
+        for (int i = 0; i < recyclerView.getChildCount(); i++) {
+            View view = recyclerView.getChildAt(i);
+            assertEquals(layoutStart[0] + i, recyclerView.getChildAdapterPosition(view));
+            AccessibilityNodeInfo info = recyclerView.getChildAt(i).createAccessibilityNodeInfo();
+            assertTrue("custom delegate sets isChecked", info.isChecked());
+            assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
+                    RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+            assertTrue(ViewCompat.hasAccessibilityDelegate(view));
+            children.add(view);
+        }
+
+        // invalidate and start layout at 50, all existing views will goes to recycler and
+        // being reused.
+        layoutStart[0] = 50;
+        layoutManager.expectLayouts(1);
+        adapter.dispatchDataSetChanged();
+        layoutManager.waitForLayout(1);
+        assertEquals(layoutCount, recyclerView.getChildCount());
+        for (int i = 0; i < recyclerView.getChildCount(); i++) {
+            View view = recyclerView.getChildAt(i);
+            assertEquals(layoutStart[0] + i, recyclerView.getChildAdapterPosition(view));
+            assertTrue(children.contains(view));
+            AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
+            assertTrue("custom delegate sets isChecked", info.isChecked());
+            assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
+                    RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+            assertTrue(ViewCompat.hasAccessibilityDelegate(view));
+        }
+    }
+
+    @Test
+    @RequiresApi(14)
+    public void clearItemDelegateWhenGoesToPool() throws Throwable {
+        final RecyclerView recyclerView = new RecyclerView(getActivity()) {
+            @Override
+            boolean isAccessibilityEnabled() {
+                return true;
+            }
+        };
+        final int firstPassLayoutCount = 5;
+        final int[] layoutCount = new int[] {firstPassLayoutCount};
+        final TestLayoutManager layoutManager = new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                detachAndScrapAttachedViews(recycler);
+                removeAndRecycleScrapInt(recycler);
+                layoutRange(recycler, 0, layoutCount[0]);
+                if (layoutLatch != null) {
+                    layoutLatch.countDown();
+                }
+            }
+        };
+        final TestAdapter adapter = new TestAdapter(100);
+        layoutManager.expectLayouts(1);
+        recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100);
+        recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool
+        recyclerView.setLayoutManager(layoutManager);
+        setRecyclerView(recyclerView);
+        recyclerView.setAdapter(adapter);
+        layoutManager.waitForLayout(1);
+
+        assertEquals(firstPassLayoutCount, recyclerView.getChildCount());
+        for (int i = 0; i < recyclerView.getChildCount(); i++) {
+            View view = recyclerView.getChildAt(i);
+            assertEquals(i, recyclerView.getChildAdapterPosition(view));
+            assertTrue(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
+                    RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+            assertTrue(ViewCompat.hasAccessibilityDelegate(view));
+            AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
+            assertNotNull(info.getCollectionItemInfo());
+        }
+
+        // let all items go to recycler pool
+        layoutManager.expectLayouts(1);
+        layoutCount[0] = 0;
+        adapter.resetItemsTo(new ArrayList());
+        layoutManager.waitForLayout(1);
+        assertEquals(0, recyclerView.getChildCount());
+        assertEquals(firstPassLayoutCount, recyclerView.getRecycledViewPool()
+                .getRecycledViewCount(0));
+        for (int i = 0; i < firstPassLayoutCount; i++) {
+            RecyclerView.ViewHolder vh = recyclerView.getRecycledViewPool().getRecycledView(0);
+            View view = vh.itemView;
+            assertEquals(RecyclerView.NO_POSITION, recyclerView.getChildAdapterPosition(view));
+            assertFalse(vh.hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+            assertFalse(ViewCompat.hasAccessibilityDelegate(view));
+            AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
+            assertNull(info.getCollectionItemInfo());
+        }
+    }
 }