1 /*
2  * Copyright 2023 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 
17 package androidx.recyclerview.widget;
18 
19 import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_ACCESSIBILITY_FOCUS;
20 import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION;
21 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
22 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import android.accessibilityservice.AccessibilityServiceInfo;
27 import android.app.UiAutomation;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.util.Pair;
31 import android.view.View;
32 import android.view.accessibility.AccessibilityEvent;
33 import android.view.accessibility.AccessibilityNodeInfo;
34 
35 import androidx.annotation.RequiresApi;
36 import androidx.test.ext.junit.runners.AndroidJUnit4;
37 import androidx.test.filters.LargeTest;
38 import androidx.test.filters.SdkSuppress;
39 
40 import org.jspecify.annotations.NonNull;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.util.concurrent.TimeoutException;
45 
46 @LargeTest
47 @RunWith(AndroidJUnit4.class)
48 public class GridLayoutManagerUiAutomationTests extends BaseGridLayoutManagerTest {
49 
50     private static final int DEFAULT_ACCESSIBILITY_EVENT_TIMEOUT_MILLIS = 5000;
51 
52     @Test
53     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_withoutSpecifyingDirection()54     public void performActionScrollInDirection_withoutSpecifyingDirection()
55             throws Throwable {
56         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
57         //  earlier android versions.
58 
59         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(6, HORIZONTAL);
60         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
61         final boolean[] returnValue = {false};
62         mActivityRule.runOnUiThread(
63                 () -> {
64                     returnValue[0] = mRecyclerView.getLayoutManager().performAccessibilityAction(
65                             ACTION_SCROLL_IN_DIRECTION.getId(), null);
66                 });
67         assertThat(returnValue[0]).isFalse();
68         assertThat(mGlm.mRowWithAccessibilityFocus).isEqualTo(-1);
69         assertThat(mGlm.mColumnWithAccessibilityFocus).isEqualTo(-1);
70     }
71 
72     @Test
73     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_withInvalidDirection()74     public void performActionScrollInDirection_withInvalidDirection()
75             throws Throwable {
76         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
77         //  earlier android versions.
78 
79         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(6, HORIZONTAL);
80         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
81         runScrollInDirectionAndFail(-1, Pair.create(-1, -1));
82     }
83 
84     @Test
85     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_withoutSettingAccessibilityFocus()86     public void performActionScrollInDirection_withoutSettingAccessibilityFocus()
87             throws Throwable {
88         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
89         //  earlier android version.
90 
91         // Return value of this call is not used.
92         setUpGridLayoutManagerAccessibilityTest(6, HORIZONTAL);
93         runScrollInDirectionAndFail(View.FOCUS_RIGHT, Pair.create(-1, -1));
94 
95     }
96 
97     @Test
98     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_vertical_scrollTargetOnTheSameRow()99     public void performActionScrollInDirection_focusRight_vertical_scrollTargetOnTheSameRow()
100             throws Throwable {
101 
102         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
103         //  earlier android version.
104 
105         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
106         /*
107         This generates the following grid:
108         1   2   3
109         4
110         */
111         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
112         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (3)",
113                 Pair.create(0, 2));
114     }
115 
116     @Test
117     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_vertical_scrollTargetOnTheNextRow()118     public void performActionScrollInDirection_focusRight_vertical_scrollTargetOnTheNextRow()
119             throws Throwable {
120         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
121         //  earlier android version.
122 
123         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
124         /*
125         This generates the following grid:
126         1   2   3
127         4   5
128         */
129         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
130         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (4)",
131                 Pair.create(1, 0));
132     }
133 
134     @Test
135     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_vertical_traversingThroughASpan()136     public void performActionScrollInDirection_focusRight_vertical_traversingThroughASpan()
137             throws Throwable {
138 
139         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
140         //  earlier android versions.
141 
142         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
143         mRecyclerView = setupBasic(new Config(4, 4));
144         mGlm.setOrientation(VERTICAL);
145         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
146             @Override
147             public int getSpanSize(int position) {
148                 if (position == 1) {
149                     return 2;
150                 }
151                 return 1;
152             }
153         });
154         waitForFirstLayout(mRecyclerView);
155         /*
156         This generates the following grid:
157         1   2   2   3
158         4
159         */
160         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
161         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (2)" ,
162                 Pair.create(0, 1));
163 
164         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
165         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (3)" ,
166                 Pair.create(0, 3));
167     }
168 
169     @Test
170     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_vertical_withoutAvailableTarget()171     public void performActionScrollInDirection_focusRight_vertical_withoutAvailableTarget()
172             throws Throwable {
173         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
174         //  earlier android version.
175 
176         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
177         /*
178         This generates the following grid:
179         1   2   3
180         4   5
181         */
182         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
183         runScrollInDirectionAndFail(View.FOCUS_RIGHT, Pair.create(1, 1));
184     }
185 
186     @Test
187     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_horizontal_scrollTargetOnTheSameRow()188     public void performActionScrollInDirection_focusRight_horizontal_scrollTargetOnTheSameRow()
189             throws Throwable {
190         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
191         //  earlier android versions.
192 
193         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
194         /*
195         This generates the following grid:
196         1   4
197         2   5
198         3
199         */
200         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
201         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (4)" ,
202                 Pair.create(0, 1));
203     }
204 
205     @Test
206     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_horizontal_traversingThroughASpan()207     public void performActionScrollInDirection_focusRight_horizontal_traversingThroughASpan()
208             throws Throwable {
209         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
210         //  earlier android versions.
211 
212         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
213         setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
214         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
215             @Override
216             public int getSpanSize(int position) {
217                 if (position == 4) {
218                     return 2;
219                 }
220                 return 1;
221             }
222         });
223         waitForFirstLayout(mRecyclerView);
224         /*
225         This generates the following grid:
226         1   4   6
227         2   5   7
228         3   5   8
229         */
230         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
231         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (5)" ,
232                 Pair.create(2, 1));
233 
234         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
235         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (8)" ,
236                 Pair.create(2, 2));
237     }
238 
239     @Test
240     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_horizontal_withWrapAround()241     public void performActionScrollInDirection_focusRight_horizontal_withWrapAround()
242             throws Throwable {
243         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
244         //  earlier android versions.
245 
246         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
247         setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
248         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
249             @Override
250             public int getSpanSize(int position) {
251                 if (position == 0) {
252                     return 2;
253                 }
254                 return 1;
255             }
256         });
257         waitForFirstLayout(mRecyclerView);
258         /*
259         This generates the following grid:
260         1   3   6
261         1   4   7
262         2   5   8
263         */
264         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(5));
265         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (1)" ,
266                 Pair.create(1, 0));
267 
268         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
269         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_RIGHT, "Item (4)" ,
270                 Pair.create(1, 1));
271     }
272 
273     @Test
274     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusRight_horizontal_withoutAvailableTarget()275     public void performActionScrollInDirection_focusRight_horizontal_withoutAvailableTarget()
276             throws Throwable {
277         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
278         //  earlier android versions.
279 
280         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
281         /*
282         This generates the following grid:
283         1   4
284         2   5
285         3
286         */
287         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
288         runScrollInDirectionAndFail(View.FOCUS_RIGHT);
289         assertThat(mGlm.mRowWithAccessibilityFocus).isEqualTo(2);
290         assertThat(mGlm.mColumnWithAccessibilityFocus).isEqualTo(0);
291     }
292 
293     @Test
294     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_vertical_scrollTargetOnTheSameRow()295     public void performActionScrollInDirection_focusLeft_vertical_scrollTargetOnTheSameRow()
296             throws Throwable {
297 
298         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
299         //  earlier android version.
300 
301         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
302         /*
303         This generates the following grid:
304         1   2   3
305         4
306         */
307         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
308         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (1)",
309                 Pair.create(0, 0));
310     }
311 
312     @Test
313     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_vertical_scrollTargetOnAPreviousRow()314     public void performActionScrollInDirection_focusLeft_vertical_scrollTargetOnAPreviousRow()
315             throws Throwable {
316         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
317         //  earlier android version.
318 
319         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
320         /*
321         This generates the following grid:
322         1   2   3
323         4   5
324         */
325         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
326         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (3)",
327                 Pair.create(0, 2));
328     }
329 
330     @Test
331     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_vertical_traversingThroughASpan()332     public void performActionScrollInDirection_focusLeft_vertical_traversingThroughASpan()
333             throws Throwable {
334 
335         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
336         //  earlier android version.
337 
338         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
339         setUpRecyclerViewAndGridLayoutManager(4, VERTICAL);
340         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
341             @Override
342             public int getSpanSize(int position) {
343                 if (position == 1) {
344                     return 2;
345                 }
346                 return 1;
347             }
348         });
349         waitForFirstLayout(mRecyclerView);
350         /*
351         This generates the following grid:
352         1   2   2
353         3   4
354         */
355         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
356         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (2)",
357                 Pair.create(0, 1));
358     }
359 
360     @Test
361     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_vertical_withoutAvailableTarget()362     public void performActionScrollInDirection_focusLeft_vertical_withoutAvailableTarget()
363             throws Throwable {
364         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
365         //  earlier android version.
366 
367         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
368         /*
369         This generates the following grid:
370         1   2   3
371         4   5
372         */
373         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
374         runScrollInDirectionAndFail(View.FOCUS_LEFT, Pair.create(0, 0));
375     }
376 
377     @Test
378     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_horizontal_scrollTargetOnTheSameRow()379     public void performActionScrollInDirection_focusLeft_horizontal_scrollTargetOnTheSameRow()
380             throws Throwable {
381         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
382         //  earlier android versions.
383 
384         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
385         /*
386         This generates the following grid:
387         1   4
388         2   5
389         3
390         */
391         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
392         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (2)" ,
393                 Pair.create(1, 0));
394     }
395 
396     @Test
397     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_horizontal_traversingThroughASpan()398     public void performActionScrollInDirection_focusLeft_horizontal_traversingThroughASpan()
399             throws Throwable {
400         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
401         //  earlier android versions.
402 
403         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
404         setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
405         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
406             @Override
407             public int getSpanSize(int position) {
408                 if (position == 4) {
409                     return 2;
410                 }
411                 return 1;
412             }
413         });
414         waitForFirstLayout(mRecyclerView);
415         /*
416         This generates the following grid:
417         1   4   6
418         2   5   7
419         3   5   8
420         */
421         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(7));
422         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (5)" ,
423                 Pair.create(2, 1));
424 
425         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
426         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (3)" ,
427                 Pair.create(2, 0));
428     }
429 
430     @Test
431     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_horizontal_withWrapAround()432     public void performActionScrollInDirection_focusLeft_horizontal_withWrapAround()
433             throws Throwable {
434         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
435         //  earlier android versions.
436 
437         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
438         setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
439         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
440             @Override
441             public int getSpanSize(int position) {
442                 if (position == 6) {
443                     return 2;
444                 }
445                 return 1;
446             }
447         });
448         waitForFirstLayout(mRecyclerView);
449         /*
450         This generates the following grid:
451         1   4   7
452         2   5   7
453         3   6   8
454         */
455         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
456         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (7)" ,
457                 Pair.create(1, 2));
458 
459         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(6));
460         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (5)" ,
461                 Pair.create(1, 1));
462     }
463 
464     @Test
465     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusLeft_horizontal_withoutAvailableTarget()466     public void performActionScrollInDirection_focusLeft_horizontal_withoutAvailableTarget()
467             throws Throwable {
468         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
469         //  earlier android versions.
470 
471         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
472         /*
473         This generates the following grid:
474         1   4
475         2   5
476         3
477         */
478         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
479         runScrollInDirectionAndFail(View.FOCUS_LEFT, Pair.create(0, 0));
480     }
481 
482     @Test
483     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusUp_vertical_scrollTargetOnTheSameColumn()484     public void performActionScrollInDirection_focusUp_vertical_scrollTargetOnTheSameColumn()
485             throws Throwable {
486 
487         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
488         //  earlier android version.
489 
490         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
491         /*
492         This generates the following grid:
493         1   2   3
494         4
495         */
496         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
497         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_UP, "Item (1)",
498                 Pair.create(0, 0));
499     }
500 
501     @Test
502     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusUp_vertical_traversingThroughASpan()503     public void performActionScrollInDirection_focusUp_vertical_traversingThroughASpan()
504             throws Throwable {
505 
506         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
507         //  earlier android versions.
508 
509         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
510         mRecyclerView = setupBasic(new Config(3, 8));
511         mGlm.setOrientation(VERTICAL);
512         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
513             @Override
514             public int getSpanSize(int position) {
515                 if (position == 3) {
516                     return 2;
517                 }
518                 return 1;
519             }
520         });
521         waitForFirstLayout(mRecyclerView);
522         /*
523         This generates the following grid:
524         1   2   3
525         4   4   5
526         6   7   8
527         */
528         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(6));
529         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_UP, "Item (4)" ,
530                 Pair.create(1, 1));
531 
532         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
533         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_UP, "Item (2)" ,
534                 Pair.create(0, 1));
535     }
536 
537     @Test
538     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusUp_vertical_withoutAvailableTarget()539     public void performActionScrollInDirection_focusUp_vertical_withoutAvailableTarget()
540             throws Throwable {
541         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
542         //  earlier android version.
543 
544         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
545         /*
546         This generates the following grid:
547         1   2   3
548         4
549         */
550         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
551         runScrollInDirectionAndFail(View.FOCUS_UP, Pair.create(0, 1));
552     }
553 
554     @Test
555     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusUp_horizontal_scrollTargetOnTheSameColumn()556     public void performActionScrollInDirection_focusUp_horizontal_scrollTargetOnTheSameColumn()
557             throws Throwable {
558         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
559         //  earlier android versions.
560 
561         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
562         /*
563         This generates the following grid:
564         1   4
565         2   5
566         3
567         */
568         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
569         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_UP, "Item (1)" ,
570                 Pair.create(0, 0));
571     }
572 
573     @Test
574     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusUp_horizontal_traversingThroughASpan()575     public void performActionScrollInDirection_focusUp_horizontal_traversingThroughASpan()
576             throws Throwable {
577         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
578         //  earlier android versions.
579 
580         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
581         mRecyclerView = setupBasic(new Config(4, 9));
582         mGlm.setOrientation(HORIZONTAL);
583         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
584             @Override
585             public int getSpanSize(int position) {
586                 if (position == 5) {
587                     return 2;
588                 }
589                 return 1;
590             }
591         });
592         waitForFirstLayout(mRecyclerView);
593         /*
594         This generates the following grid:
595         1   5   8
596         2   6   9
597         3   6
598         4   7
599         */
600         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(6));
601         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_UP, "Item (6)" ,
602                 Pair.create(2, 1));
603 
604         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(5));
605         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_UP, "Item (5)" ,
606                 Pair.create(0, 1));
607     }
608 
609     @Test
610     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusUp_horizontal_withoutAvailableTarget()611     public void performActionScrollInDirection_focusUp_horizontal_withoutAvailableTarget()
612             throws Throwable {
613         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
614         //  earlier android versions.
615 
616         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
617         /*
618         This generates the following grid:
619         1   4
620         2   5
621         3
622         */
623         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
624         runScrollInDirectionAndFail(View.FOCUS_UP, Pair.create(0, 1));
625     }
626 
627     @Test
628     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusDown_vertical_scrollTargetOnTheSameColumn()629     public void performActionScrollInDirection_focusDown_vertical_scrollTargetOnTheSameColumn()
630             throws Throwable {
631 
632         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
633         //  earlier android version.
634 
635         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
636         /*
637         This generates the following grid:
638         1   2   3
639         4
640         */
641         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
642         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_DOWN, "Item (4)", Pair.create(1,
643                 0));
644     }
645 
646     @Test
647     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusDown_vertical_traversingThroughASpan()648     public void performActionScrollInDirection_focusDown_vertical_traversingThroughASpan()
649             throws Throwable {
650 
651         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
652         //  earlier android versions.
653 
654         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
655         mRecyclerView = setupBasic(new Config(3, 8));
656         mGlm.setOrientation(VERTICAL);
657         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
658             @Override
659             public int getSpanSize(int position) {
660                 if (position == 3) {
661                     return 2;
662                 }
663                 return 1;
664             }
665         });
666         waitForFirstLayout(mRecyclerView);
667         /*
668         This generates the following grid:
669         1   2   3
670         4   4   5
671         6   7   8
672         */
673         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
674         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_DOWN, "Item (4)" ,
675                 Pair.create(1, 1));
676 
677         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
678         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_DOWN, "Item (7)" ,
679                 Pair.create(2, 1));
680     }
681 
682     @Test
683     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusDown_vertical_withoutAvailableTarget()684     public void performActionScrollInDirection_focusDown_vertical_withoutAvailableTarget()
685             throws Throwable {
686         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
687         //  earlier android version.
688 
689         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
690         /*
691         This generates the following grid:
692         1   2   3
693         4
694         */
695         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
696         runScrollInDirectionAndFail(View.FOCUS_DOWN, Pair.create(0, 1));
697 
698         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
699         runScrollInDirectionAndFail(View.FOCUS_DOWN, Pair.create(1, 0));
700     }
701 
702     @Test
703     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusDown_horizontal_scrollTargetOnTheSameColumn()704     public void performActionScrollInDirection_focusDown_horizontal_scrollTargetOnTheSameColumn()
705             throws Throwable {
706         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
707         //  earlier android versions.
708 
709         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
710         /*
711         This generates the following grid:
712         1   4
713         2   5
714         3
715         */
716         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
717         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_DOWN, "Item (3)" ,
718                 Pair.create(2, 0));
719     }
720 
721     @Test
722     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusDown_horizontal_traversingThroughASpan()723     public void performActionScrollInDirection_focusDown_horizontal_traversingThroughASpan()
724             throws Throwable {
725         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
726         //  earlier android versions.
727 
728         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
729         mRecyclerView = setupBasic(new Config(4, 9));
730         mGlm.setOrientation(HORIZONTAL);
731         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
732             @Override
733             public int getSpanSize(int position) {
734                 if (position == 5) {
735                     return 2;
736                 }
737                 return 1;
738             }
739         });
740         waitForFirstLayout(mRecyclerView);
741         /*
742         This generates the following grid:
743         1   5   8
744         2   6   9
745         3   6
746         4   7
747         */
748         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
749         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_DOWN, "Item (6)" ,
750                 Pair.create(1, 1));
751 
752         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(5));
753         runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_DOWN, "Item (7)" ,
754                 Pair.create(3, 1));
755     }
756 
757     @Test
758     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
performActionScrollInDirection_focusDown_horizontal_withoutAvailableTarget()759     public void performActionScrollInDirection_focusDown_horizontal_withoutAvailableTarget()
760             throws Throwable {
761         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
762         //  earlier android versions.
763 
764         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
765         /*
766         This generates the following grid:
767         1   4
768         2   5
769         3
770         */
771         setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
772         runScrollInDirectionAndFail(View.FOCUS_DOWN, Pair.create(2, 0));
773     }
774 
775     /**
776      * Verifies that a scroll successfully occurs in the specified {@code direction}.
777      *
778      * @param uiAutomation  UiAutomation instance.
779      * @param direction The direction of the scroll.
780      * @param scrollTargetText The text of the view targeted by the scroll.
781      * @throws TimeoutException Exception thrown when an action times out.
782      */
783     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
runScrollInDirectionAndSucceed(UiAutomation uiAutomation, int direction, String scrollTargetText)784     private void runScrollInDirectionAndSucceed(UiAutomation uiAutomation, int direction,
785             String scrollTargetText)
786             throws TimeoutException {
787         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
788         //  earlier android versions.
789 
790         final boolean[] returnValue = {false};
791         AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
792                 () -> mActivityRule.runOnUiThread(() -> {
793                     returnValue[0] =
794                             mRecyclerView.getLayoutManager().performAccessibilityAction(
795                                     ACTION_SCROLL_IN_DIRECTION.getId(),
796                                     bundleWithDirectionArg(direction));
797                 }),
798                 event -> event.getEventType() == AccessibilityEvent.TYPE_VIEW_TARGETED_BY_SCROLL,
799                 DEFAULT_ACCESSIBILITY_EVENT_TIMEOUT_MILLIS);
800 
801         assertThat(scrollTargetText).isEqualTo(awaitedEvent.getSource().getText());
802         assertThat(returnValue[0]).isTrue();
803     }
804 
805     /**
806      * Verifies that a scroll successfully occurs in the specified {@code direction} and that the
807      * values of {@code mRowIndexForAccessibility} and {@code mColumnIndexForAccessibility} are
808      * currectly set.
809      *
810      * @param uiAutomation  UiAutomation instance.
811      * @param direction The direction of the scroll.
812      * @param scrollTargetText The text of the view targeted by the scroll.
813      * @param rowColumn The values for the row and column with accessibility focus.
814      *
815      * @throws TimeoutException Exception thrown when an action times out.
816      */
817     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
runScrollInDirectionAndSucceed(UiAutomation uiAutomation, int direction, String scrollTargetText, Pair<Integer, Integer> rowColumn)818     private void runScrollInDirectionAndSucceed(UiAutomation uiAutomation, int direction,
819             String scrollTargetText, Pair<Integer, Integer> rowColumn)
820             throws TimeoutException {
821         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
822         //  earlier android versions.
823 
824         runScrollInDirectionAndSucceed(uiAutomation, direction, scrollTargetText);
825         assertThat(mGlm.mRowWithAccessibilityFocus).isEqualTo(rowColumn.first);
826         assertThat(mGlm.mColumnWithAccessibilityFocus).isEqualTo(rowColumn.second);
827     }
828 
829     /**
830      * Verifies that a scroll does not occur in the specified {@code direction}.
831      *
832      * @param direction The direction of the scroll.
833      */
834     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
runScrollInDirectionAndFail(int direction)835     private void runScrollInDirectionAndFail(int direction) {
836         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
837         //  earlier android versions.
838 
839         final boolean[] returnValue = {false};
840 
841         mActivityRule.runOnUiThread(
842                 () -> {
843                     returnValue[0] = mRecyclerView.getLayoutManager().performAccessibilityAction(
844                             ACTION_SCROLL_IN_DIRECTION.getId(), bundleWithDirectionArg(direction));
845                 });
846 
847         assertThat(returnValue[0]).isFalse();
848     }
849 
850     /**
851      * Verifies that a scroll does not occur in the specified {@code direction}.
852      *
853      * @param direction The direction of the scroll.
854      * @param rowColumn The values for the row and column with accessibility focus.
855      */
856     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
runScrollInDirectionAndFail(int direction, Pair<Integer, Integer> rowColumn)857     private void runScrollInDirectionAndFail(int direction, Pair<Integer, Integer> rowColumn) {
858         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
859         //  earlier android versions.
860 
861         final boolean[] returnValue = {false};
862 
863         mActivityRule.runOnUiThread(
864                 () -> {
865                     returnValue[0] = mRecyclerView.getLayoutManager().performAccessibilityAction(
866                             ACTION_SCROLL_IN_DIRECTION.getId(), bundleWithDirectionArg(direction));
867                 });
868 
869         assertThat(returnValue[0]).isFalse();
870         assertThat(mGlm.mRowWithAccessibilityFocus).isEqualTo(rowColumn.first);
871         assertThat(mGlm.mColumnWithAccessibilityFocus).isEqualTo(rowColumn.second);
872     }
873 
874     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
setUpGridLayoutManagerAccessibilityTest(int itemCount, int orientation)875     private @NonNull UiAutomation setUpGridLayoutManagerAccessibilityTest(int itemCount,
876             int orientation) throws Throwable {
877         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
878         //  earlier android versions.
879 
880         final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
881         setUpRecyclerViewAndGridLayoutManager(itemCount, orientation);
882         waitForFirstLayout(mRecyclerView);
883         return uiAutomation;
884     }
885 
bundleWithDirectionArg(int direction)886     private Bundle bundleWithDirectionArg(int direction) {
887         Bundle bundle = new Bundle();
888         bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_DIRECTION_INT, direction);
889         return bundle;
890     }
891 
setUpAndReturnUiAutomation()892     private @NonNull UiAutomation setUpAndReturnUiAutomation() {
893         UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
894         final AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
895         info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
896         uiAutomation.setServiceInfo(info);
897         return uiAutomation;
898     }
899 
setAccessibilityFocus(UiAutomation uiAutomation, View source)900     private void setAccessibilityFocus(UiAutomation uiAutomation, View source)
901             throws TimeoutException {
902         AccessibilityEvent awaitedEvent = null;
903         awaitedEvent = uiAutomation.executeAndWaitForEvent(
904                 () -> {
905                     try {
906                         mActivityRule.runOnUiThread(() -> source.performAccessibilityAction(
907                                 ACTION_ACCESSIBILITY_FOCUS.getId(), null));
908                     } catch (Throwable throwable) {
909                         throwable.printStackTrace();
910                     }
911                 },
912                 event -> event.getEventType()
913                         == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
914                 DEFAULT_ACCESSIBILITY_EVENT_TIMEOUT_MILLIS);
915         assertThat(awaitedEvent.getSource().isAccessibilityFocused()).isTrue();
916     }
917 
setUpRecyclerViewAndGridLayoutManager(int itemCount, int orientation)918     private void setUpRecyclerViewAndGridLayoutManager(int itemCount, int orientation)
919             throws Throwable {
920         mRecyclerView = setupBasic(new Config(3, itemCount));
921         mGlm.setOrientation(orientation);
922     }
923 }
924