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