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.junit.Assert.assertEquals;
20import static org.junit.Assert.assertFalse;
21import static org.junit.Assert.assertNotNull;
22import static org.junit.Assert.assertTrue;
23
24import android.os.Build;
25import android.support.test.filters.MediumTest;
26import android.view.View;
27import android.view.accessibility.AccessibilityEvent;
28
29import androidx.core.view.AccessibilityDelegateCompat;
30import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
31
32import org.junit.Test;
33import org.junit.runner.RunWith;
34import org.junit.runners.Parameterized;
35
36import java.util.ArrayList;
37import java.util.List;
38import java.util.concurrent.atomic.AtomicBoolean;
39
40@MediumTest
41@RunWith(Parameterized.class)
42public class RecyclerViewAccessibilityTest extends BaseRecyclerViewInstrumentationTest {
43    private static final boolean SUPPORTS_COLLECTION_INFO =
44            Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
45    private final boolean mVerticalScrollBefore;
46    private final boolean mHorizontalScrollBefore;
47    private final boolean mVerticalScrollAfter;
48    private final boolean mHorizontalScrollAfter;
49
50    public RecyclerViewAccessibilityTest(boolean verticalScrollBefore,
51            boolean horizontalScrollBefore, boolean verticalScrollAfter,
52            boolean horizontalScrollAfter) {
53        mVerticalScrollBefore = verticalScrollBefore;
54        mHorizontalScrollBefore = horizontalScrollBefore;
55        mVerticalScrollAfter = verticalScrollAfter;
56        mHorizontalScrollAfter = horizontalScrollAfter;
57    }
58
59    @Parameterized.Parameters(name = "vBefore={0},vAfter={1},hBefore={2},hAfter={3}")
60    public static List<Object[]> getParams() {
61        List<Object[]> params = new ArrayList<>();
62        for (boolean vBefore : new boolean[]{true, false}) {
63            for (boolean vAfter : new boolean[]{true, false}) {
64                for (boolean hBefore : new boolean[]{true, false}) {
65                    for (boolean hAfter : new boolean[]{true, false}) {
66                        params.add(new Object[]{vBefore, hBefore, vAfter, hAfter});
67                    }
68                }
69            }
70        }
71        return params;
72    }
73
74    @Test
75    public void onInitializeAccessibilityNodeInfoTest() throws Throwable {
76        final RecyclerView recyclerView = new RecyclerView(getActivity()) {
77            @Override
78            public boolean canScrollHorizontally(int direction) {
79                return direction < 0 && mHorizontalScrollBefore ||
80                        direction > 0 && mHorizontalScrollAfter;
81            }
82
83            @Override
84            public boolean canScrollVertically(int direction) {
85                return direction < 0 && mVerticalScrollBefore ||
86                        direction > 0 && mVerticalScrollAfter;
87            }
88        };
89        final TestAdapter adapter = new TestAdapter(10);
90        final AtomicBoolean hScrolledBack = new AtomicBoolean(false);
91        final AtomicBoolean vScrolledBack = new AtomicBoolean(false);
92        final AtomicBoolean hScrolledFwd = new AtomicBoolean(false);
93        final AtomicBoolean vScrolledFwd = new AtomicBoolean(false);
94        recyclerView.setAdapter(adapter);
95        recyclerView.setLayoutManager(new TestLayoutManager() {
96
97            @Override
98            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
99                layoutRange(recycler, 0, 5);
100            }
101
102            @Override
103            public RecyclerView.LayoutParams generateDefaultLayoutParams() {
104                return new RecyclerView.LayoutParams(-1, -1);
105            }
106
107            @Override
108            public boolean canScrollVertically() {
109                return mVerticalScrollAfter || mVerticalScrollBefore;
110            }
111
112            @Override
113            public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
114                    RecyclerView.State state) {
115                if (dx > 0) {
116                    hScrolledFwd.set(true);
117                } else if (dx < 0) {
118                    hScrolledBack.set(true);
119                }
120                return 0;
121            }
122
123            @Override
124            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
125                    RecyclerView.State state) {
126                if (dy > 0) {
127                    vScrolledFwd.set(true);
128                } else if (dy < 0) {
129                    vScrolledBack.set(true);
130                }
131                return 0;
132            }
133
134            @Override
135            public boolean canScrollHorizontally() {
136                return mHorizontalScrollAfter || mHorizontalScrollBefore;
137            }
138        });
139        setRecyclerView(recyclerView);
140        final RecyclerViewAccessibilityDelegate delegateCompat = recyclerView
141                .getCompatAccessibilityDelegate();
142        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
143        mActivityRule.runOnUiThread(new Runnable() {
144            @Override
145            public void run() {
146                delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info);
147            }
148        });
149        assertEquals(mHorizontalScrollAfter || mHorizontalScrollBefore
150                || mVerticalScrollAfter || mVerticalScrollBefore, info.isScrollable());
151        assertEquals(mHorizontalScrollBefore || mVerticalScrollBefore,
152                (info.getActions() & AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) != 0);
153        assertEquals(mHorizontalScrollAfter || mVerticalScrollAfter,
154                (info.getActions() & AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) != 0);
155        if (SUPPORTS_COLLECTION_INFO) {
156            final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = info
157                    .getCollectionInfo();
158            assertNotNull(collectionInfo);
159            if (recyclerView.getLayoutManager().canScrollVertically()) {
160                assertEquals(adapter.getItemCount(), collectionInfo.getRowCount());
161            }
162            if (recyclerView.getLayoutManager().canScrollHorizontally()) {
163                assertEquals(adapter.getItemCount(), collectionInfo.getColumnCount());
164            }
165        }
166
167        final AccessibilityEvent event = AccessibilityEvent.obtain();
168        mActivityRule.runOnUiThread(new Runnable() {
169            @Override
170            public void run() {
171                delegateCompat.onInitializeAccessibilityEvent(recyclerView, event);
172            }
173        });
174        assertEquals(event.isScrollable(), mVerticalScrollAfter || mHorizontalScrollAfter
175                || mVerticalScrollBefore || mHorizontalScrollBefore);
176        assertEquals(event.getItemCount(), adapter.getItemCount());
177
178        getInstrumentation().waitForIdleSync();
179        if (SUPPORTS_COLLECTION_INFO) {
180            for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
181                final View view = mRecyclerView.getChildAt(i);
182                final AccessibilityNodeInfoCompat childInfo = AccessibilityNodeInfoCompat.obtain();
183                mActivityRule.runOnUiThread(new Runnable() {
184                    @Override
185                    public void run() {
186                        delegateCompat.getItemDelegate().
187                                onInitializeAccessibilityNodeInfo(view, childInfo);
188                    }
189                });
190                final AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfo
191                        = childInfo.getCollectionItemInfo();
192                assertNotNull(collectionItemInfo);
193                if (recyclerView.getLayoutManager().canScrollHorizontally()) {
194                    assertEquals(i, collectionItemInfo.getColumnIndex());
195                } else {
196                    assertEquals(0, collectionItemInfo.getColumnIndex());
197                }
198
199                if (recyclerView.getLayoutManager().canScrollVertically()) {
200                    assertEquals(i, collectionItemInfo.getRowIndex());
201                } else {
202                    assertEquals(0, collectionItemInfo.getRowIndex());
203                }
204            }
205        }
206
207        mActivityRule.runOnUiThread(new Runnable() {
208            @Override
209            public void run() {
210
211            }
212        });
213        hScrolledBack.set(false);
214        vScrolledBack.set(false);
215        hScrolledFwd.set(false);
216        vScrolledBack.set(false);
217        performAccessibilityAction(delegateCompat, recyclerView,
218                AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
219        assertEquals(mHorizontalScrollBefore, hScrolledBack.get());
220        assertEquals(mVerticalScrollBefore, vScrolledBack.get());
221        assertEquals(false, hScrolledFwd.get());
222        assertEquals(false, vScrolledFwd.get());
223
224        hScrolledBack.set(false);
225        vScrolledBack.set(false);
226        hScrolledFwd.set(false);
227        vScrolledBack.set(false);
228        performAccessibilityAction(delegateCompat, recyclerView,
229                AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
230        assertEquals(false, hScrolledBack.get());
231        assertEquals(false, vScrolledBack.get());
232        assertEquals(mHorizontalScrollAfter, hScrolledFwd.get());
233        assertEquals(mVerticalScrollAfter, vScrolledFwd.get());
234    }
235
236    @Test
237    public void ignoreAccessibilityIfAdapterHasChanged() throws Throwable {
238        final RecyclerView recyclerView = new RecyclerView(getActivity()) {
239            //@Override
240            @Override
241            public boolean canScrollHorizontally(int direction) {
242                return true;
243            }
244
245            //@Override
246            @Override
247            public boolean canScrollVertically(int direction) {
248                return true;
249            }
250        };
251        final DumbLayoutManager layoutManager = new DumbLayoutManager();
252        final TestAdapter adapter = new TestAdapter(10);
253        recyclerView.setAdapter(adapter);
254        recyclerView.setLayoutManager(layoutManager);
255        layoutManager.expectLayouts(1);
256        setRecyclerView(recyclerView);
257        layoutManager.waitForLayout(1);
258
259        final RecyclerViewAccessibilityDelegate delegateCompat = recyclerView
260                .getCompatAccessibilityDelegate();
261        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
262        mActivityRule.runOnUiThread(new Runnable() {
263            @Override
264            public void run() {
265                delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info);
266            }
267        });
268        assertTrue("test sanity", info.isScrollable());
269        final AccessibilityNodeInfoCompat info2 = AccessibilityNodeInfoCompat.obtain();
270        mActivityRule.runOnUiThread(new Runnable() {
271            @Override
272            public void run() {
273                try {
274                    adapter.deleteAndNotify(1, 1);
275                } catch (Throwable throwable) {
276                    postExceptionToInstrumentation(throwable);
277                }
278                delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info2);
279                assertFalse("info should not be filled if data is out of date",
280                        info2.isScrollable());
281            }
282        });
283        checkForMainThreadException();
284    }
285
286    boolean performAccessibilityAction(final AccessibilityDelegateCompat delegate,
287            final RecyclerView recyclerView, final int action) throws Throwable {
288        final boolean[] result = new boolean[1];
289        mActivityRule.runOnUiThread(new Runnable() {
290            @Override
291            public void run() {
292                result[0] = delegate.performAccessibilityAction(recyclerView, action, null);
293            }
294        });
295        getInstrumentation().waitForIdleSync();
296        Thread.sleep(250);
297        return result[0];
298    }
299}
300