1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.recyclerview.widget;
18
19import static org.hamcrest.CoreMatchers.is;
20import static org.hamcrest.MatcherAssert.assertThat;
21import static org.junit.Assert.assertEquals;
22import static org.junit.Assert.assertFalse;
23import static org.junit.Assert.assertNotNull;
24import static org.junit.Assert.assertTrue;
25import static org.mockito.Matchers.anyInt;
26import static org.mockito.Mockito.never;
27import static org.mockito.Mockito.spy;
28import static org.mockito.Mockito.verify;
29
30import android.os.Build;
31import android.support.test.filters.LargeTest;
32import android.support.test.filters.MediumTest;
33import android.support.test.filters.SdkSuppress;
34import android.support.test.runner.AndroidJUnit4;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.accessibility.AccessibilityNodeInfo;
38
39import androidx.annotation.NonNull;
40import androidx.core.view.AccessibilityDelegateCompat;
41import androidx.core.view.ViewCompat;
42import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
43
44import org.junit.Test;
45import org.junit.runner.RunWith;
46
47import java.util.ArrayList;
48import java.util.Arrays;
49import java.util.List;
50import java.util.concurrent.atomic.AtomicBoolean;
51
52@MediumTest
53@RunWith(AndroidJUnit4.class)
54public class RecyclerViewAccessibilityLifecycleTest extends BaseRecyclerViewInstrumentationTest {
55    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
56    @Test
57    public void dontDispatchChangeDuringLayout() throws Throwable {
58        LayoutAllLayoutManager lm = new LayoutAllLayoutManager();
59        final AtomicBoolean calledA11DuringLayout = new AtomicBoolean(false);
60        final List<Integer> invocations = new ArrayList<>();
61
62        final WrappedRecyclerView recyclerView = new WrappedRecyclerView(getActivity()) {
63            @Override
64            boolean isAccessibilityEnabled() {
65                return true;
66            }
67
68            @Override
69            public boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder,
70                    int importantForAccessibilityBeforeHidden) {
71                invocations.add(importantForAccessibilityBeforeHidden);
72                boolean notified = super.setChildImportantForAccessibilityInternal(viewHolder,
73                        importantForAccessibilityBeforeHidden);
74                if (notified && mRecyclerView.isComputingLayout()) {
75                    calledA11DuringLayout.set(true);
76                }
77                return notified;
78            }
79        };
80        TestAdapter adapter = new TestAdapter(10) {
81            @Override
82            public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
83                    int viewType) {
84                TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
85                ViewCompat.setImportantForAccessibility(vh.itemView,
86                        ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
87                return vh;
88            }
89        };
90        recyclerView.setAdapter(adapter);
91        recyclerView.setLayoutManager(lm);
92        lm.expectLayouts(1);
93        setRecyclerView(recyclerView);
94        lm.waitForLayout(1);
95        assertThat(calledA11DuringLayout.get(), is(false));
96        lm.expectLayouts(1);
97        adapter.deleteAndNotify(2, 2);
98        lm.waitForLayout(2);
99        recyclerView.waitUntilAnimations();
100        assertThat(invocations, is(Arrays.asList(
101                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
102                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
103                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
104                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES)));
105
106        assertThat(calledA11DuringLayout.get(), is(false));
107    }
108
109    @LargeTest
110    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
111    @Test
112    public void processAllViewHolders() {
113        RecyclerView rv = new RecyclerView(getActivity());
114        rv.setLayoutManager(new LinearLayoutManager(getActivity()));
115        View itemView1 = spy(new View(getActivity()));
116        View itemView2 = spy(new View(getActivity()));
117        View itemView3 = spy(new View(getActivity()));
118
119        rv.addView(itemView1);
120        // do not add 2
121        rv.addView(itemView3);
122
123        RecyclerView.ViewHolder vh1 = new RecyclerView.ViewHolder(itemView1) {};
124        vh1.mPendingAccessibilityState = View.IMPORTANT_FOR_ACCESSIBILITY_YES;
125        RecyclerView.ViewHolder vh2 = new RecyclerView.ViewHolder(itemView2) {};
126        vh2.mPendingAccessibilityState = View.IMPORTANT_FOR_ACCESSIBILITY_YES;
127        RecyclerView.ViewHolder vh3 = new RecyclerView.ViewHolder(itemView3) {};
128        vh3.mPendingAccessibilityState = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
129
130        rv.mPendingAccessibilityImportanceChange.add(vh1);
131        rv.mPendingAccessibilityImportanceChange.add(vh2);
132        rv.mPendingAccessibilityImportanceChange.add(vh3);
133        rv.dispatchPendingImportantForAccessibilityChanges();
134
135        verify(itemView1).setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
136        //noinspection WrongConstant
137        verify(itemView2, never()).setImportantForAccessibility(anyInt());
138        verify(itemView3).setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
139        assertThat(rv.mPendingAccessibilityImportanceChange.size(), is(0));
140    }
141
142    public class LayoutAllLayoutManager extends TestLayoutManager {
143        private final boolean mAllowNullLayoutLatch;
144
145        public LayoutAllLayoutManager() {
146            // by default, we don't allow unexpected layouts.
147            this(false);
148        }
149        LayoutAllLayoutManager(boolean allowNullLayoutLatch) {
150            mAllowNullLayoutLatch = allowNullLayoutLatch;
151        }
152
153        @Override
154        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
155            detachAndScrapAttachedViews(recycler);
156            layoutRange(recycler, 0, state.getItemCount());
157            if (!mAllowNullLayoutLatch || layoutLatch != null) {
158                layoutLatch.countDown();
159            }
160        }
161    }
162
163    @Test
164    public void notClearCustomViewDelegate() throws Throwable {
165        final RecyclerView recyclerView = new RecyclerView(getActivity()) {
166            @Override
167            boolean isAccessibilityEnabled() {
168                return true;
169            }
170        };
171        final int[] layoutStart = new int[] {0};
172        final int layoutCount = 5;
173        final TestLayoutManager layoutManager = new TestLayoutManager() {
174            @Override
175            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
176                detachAndScrapAttachedViews(recycler);
177                removeAndRecycleScrapInt(recycler);
178                layoutRange(recycler, layoutStart[0], layoutStart[0] + layoutCount);
179                if (layoutLatch != null) {
180                    layoutLatch.countDown();
181                }
182            }
183        };
184        final AccessibilityDelegateCompat delegateCompat = new AccessibilityDelegateCompat() {
185            @Override
186            public void onInitializeAccessibilityNodeInfo(View host,
187                    AccessibilityNodeInfoCompat info) {
188                super.onInitializeAccessibilityNodeInfo(host, info);
189                info.setChecked(true);
190            }
191        };
192        final TestAdapter adapter = new TestAdapter(100) {
193            @Override
194            public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
195                    int viewType) {
196                TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
197                ViewCompat.setAccessibilityDelegate(vh.itemView, delegateCompat);
198                return vh;
199            }
200        };
201        layoutManager.expectLayouts(1);
202        recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100);
203        recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool
204        recyclerView.setLayoutManager(layoutManager);
205        setRecyclerView(recyclerView);
206         mActivityRule.runOnUiThread(new Runnable() {
207            @Override
208            public void run() {
209                recyclerView.setAdapter(adapter);
210            }
211        });
212        layoutManager.waitForLayout(1);
213
214        assertEquals(layoutCount, recyclerView.getChildCount());
215        final ArrayList<View> children = new ArrayList();
216        mActivityRule.runOnUiThread(new Runnable() {
217            @Override
218            public void run() {
219                for (int i = 0; i < recyclerView.getChildCount(); i++) {
220                    View view = recyclerView.getChildAt(i);
221                    assertEquals(layoutStart[0] + i,
222                            recyclerView.getChildAdapterPosition(view));
223                    AccessibilityNodeInfo info = recyclerView.getChildAt(i)
224                            .createAccessibilityNodeInfo();
225                    assertTrue("custom delegate sets isChecked", info.isChecked());
226                    assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
227                            RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
228                    assertTrue(ViewCompat.hasAccessibilityDelegate(view));
229                    children.add(view);
230                }
231            }
232        });
233
234        // invalidate and start layout at 50, all existing views will goes to recycler and
235        // being reused.
236        layoutStart[0] = 50;
237        layoutManager.expectLayouts(1);
238        adapter.dispatchDataSetChanged();
239        layoutManager.waitForLayout(1);
240        assertEquals(layoutCount, recyclerView.getChildCount());
241        mActivityRule.runOnUiThread(new Runnable() {
242            @Override
243            public void run() {
244                for (int i = 0; i < recyclerView.getChildCount(); i++) {
245                    View view = recyclerView.getChildAt(i);
246                    assertEquals(layoutStart[0] + i,
247                            recyclerView.getChildAdapterPosition(view));
248                    assertTrue(children.contains(view));
249                    AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
250                    assertTrue("custom delegate sets isChecked", info.isChecked());
251                    assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
252                            RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
253                    assertTrue(ViewCompat.hasAccessibilityDelegate(view));
254                }
255            }
256        });
257    }
258
259    @Test
260    public void clearItemDelegateWhenGoesToPool() throws Throwable {
261        final RecyclerView recyclerView = new RecyclerView(getActivity()) {
262            @Override
263            boolean isAccessibilityEnabled() {
264                return true;
265            }
266        };
267        final int firstPassLayoutCount = 5;
268        final int[] layoutCount = new int[] {firstPassLayoutCount};
269        final TestLayoutManager layoutManager = new TestLayoutManager() {
270            @Override
271            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
272                detachAndScrapAttachedViews(recycler);
273                removeAndRecycleScrapInt(recycler);
274                layoutRange(recycler, 0, layoutCount[0]);
275                if (layoutLatch != null) {
276                    layoutLatch.countDown();
277                }
278            }
279        };
280        final TestAdapter adapter = new TestAdapter(100);
281        layoutManager.expectLayouts(1);
282        recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100);
283        recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool
284        recyclerView.setLayoutManager(layoutManager);
285        setRecyclerView(recyclerView);
286        mActivityRule.runOnUiThread(new Runnable() {
287            @Override
288            public void run() {
289                recyclerView.setAdapter(adapter);
290            }
291        });
292        layoutManager.waitForLayout(1);
293
294        assertEquals(firstPassLayoutCount, recyclerView.getChildCount());
295        mActivityRule.runOnUiThread(new Runnable() {
296            @Override
297            public void run() {
298                for (int i = 0; i < recyclerView.getChildCount(); i++) {
299                    View view = recyclerView.getChildAt(i);
300                    assertEquals(i, recyclerView.getChildAdapterPosition(view));
301                    assertTrue(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
302                            RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
303                    assertTrue(ViewCompat.hasAccessibilityDelegate(view));
304                    AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
305                    if (Build.VERSION.SDK_INT >= 19) {
306                        assertNotNull(info.getCollectionItemInfo());
307                    }
308                }
309            }
310        });
311
312        // let all items go to recycler pool
313        layoutManager.expectLayouts(1);
314        layoutCount[0] = 0;
315        adapter.resetItemsTo(new ArrayList());
316        layoutManager.waitForLayout(1);
317        assertEquals(0, recyclerView.getChildCount());
318        assertEquals(firstPassLayoutCount, recyclerView.getRecycledViewPool()
319                .getRecycledViewCount(0));
320        mActivityRule.runOnUiThread(new Runnable() {
321            @Override
322            public void run() {
323                for (int i = 0; i < firstPassLayoutCount; i++) {
324                    RecyclerView.ViewHolder vh = recyclerView.getRecycledViewPool()
325                            .getRecycledView(0);
326                    View view = vh.itemView;
327                    assertEquals(RecyclerView.NO_POSITION,
328                            recyclerView.getChildAdapterPosition(view));
329                    assertFalse(vh.hasAnyOfTheFlags(
330                            RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
331                    assertFalse(ViewCompat.hasAccessibilityDelegate(view));
332                }
333            }
334        });
335
336    }
337}
338