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