1 /** 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 * express or implied. See the License for the specific language governing permissions and 12 * limitations under the License. 13 */ 14 15 package android.accessibilityservice.cts; 16 17 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen; 18 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS; 19 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_RENDERING_INFO_KEY; 20 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH; 21 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX; 22 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; 23 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY; 24 25 import static org.junit.Assert.assertEquals; 26 import static org.junit.Assert.assertFalse; 27 import static org.junit.Assert.assertNotNull; 28 import static org.junit.Assert.assertNull; 29 import static org.junit.Assert.assertTrue; 30 import static org.junit.Assert.fail; 31 import static org.mockito.Mockito.mock; 32 import static org.mockito.Mockito.timeout; 33 import static org.mockito.Mockito.times; 34 import static org.mockito.Mockito.verify; 35 import static org.mockito.Mockito.verifyZeroInteractions; 36 37 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule; 38 import android.accessibilityservice.cts.activities.AccessibilityTextTraversalActivity; 39 import android.app.Instrumentation; 40 import android.app.UiAutomation; 41 import android.graphics.Bitmap; 42 import android.graphics.RectF; 43 import android.os.Bundle; 44 import android.os.Message; 45 import android.os.Parcelable; 46 import android.platform.test.annotations.Presubmit; 47 import android.platform.test.annotations.RequiresFlagsEnabled; 48 import android.platform.test.flag.junit.CheckFlagsRule; 49 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 50 import android.text.SpannableString; 51 import android.text.Spanned; 52 import android.text.TextUtils; 53 import android.text.style.ClickableSpan; 54 import android.text.style.ImageSpan; 55 import android.text.style.ReplacementSpan; 56 import android.text.style.URLSpan; 57 import android.util.DisplayMetrics; 58 import android.util.Size; 59 import android.util.TypedValue; 60 import android.view.View; 61 import android.view.ViewGroup; 62 import android.view.accessibility.AccessibilityManager; 63 import android.view.accessibility.AccessibilityNodeInfo; 64 import android.view.accessibility.AccessibilityNodeProvider; 65 import android.view.accessibility.AccessibilityRequestPreparer; 66 import android.view.inputmethod.EditorInfo; 67 import android.widget.EditText; 68 import android.widget.TextView; 69 70 import androidx.test.InstrumentationRegistry; 71 import androidx.test.filters.FlakyTest; 72 import androidx.test.rule.ActivityTestRule; 73 import androidx.test.runner.AndroidJUnit4; 74 75 import com.android.compatibility.common.util.CddTest; 76 import com.android.compatibility.common.util.TestUtils; 77 78 import org.junit.AfterClass; 79 import org.junit.Before; 80 import org.junit.BeforeClass; 81 import org.junit.Rule; 82 import org.junit.Test; 83 import org.junit.rules.RuleChain; 84 import org.junit.runner.RunWith; 85 86 import java.util.Arrays; 87 import java.util.List; 88 import java.util.concurrent.atomic.AtomicBoolean; 89 import java.util.concurrent.atomic.AtomicReference; 90 91 /** 92 * Test cases for actions taken on text views. 93 */ 94 @RunWith(AndroidJUnit4.class) 95 @CddTest(requirements = {"3.10/C-1-1,C-1-2"}) 96 @Presubmit 97 public class AccessibilityTextActionTest { 98 private static Instrumentation sInstrumentation; 99 private static UiAutomation sUiAutomation; 100 final Object mClickableSpanCallbackLock = new Object(); 101 final AtomicBoolean mClickableSpanCalled = new AtomicBoolean(false); 102 103 @Rule 104 public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); 105 106 private AccessibilityTextTraversalActivity mActivity; 107 108 private ActivityTestRule<AccessibilityTextTraversalActivity> mActivityRule = 109 new ActivityTestRule<>(AccessibilityTextTraversalActivity.class, false, false); 110 111 private AccessibilityDumpOnFailureRule mDumpOnFailureRule = 112 new AccessibilityDumpOnFailureRule(); 113 114 @Rule 115 public final RuleChain mRuleChain = RuleChain 116 .outerRule(mActivityRule) 117 .around(mDumpOnFailureRule); 118 119 @BeforeClass oneTimeSetup()120 public static void oneTimeSetup() throws Exception { 121 sInstrumentation = InstrumentationRegistry.getInstrumentation(); 122 sUiAutomation = sInstrumentation.getUiAutomation(); 123 } 124 125 @Before setUp()126 public void setUp() throws Exception { 127 mActivity = launchActivityAndWaitForItToBeOnscreen( 128 sInstrumentation, sUiAutomation, mActivityRule); 129 mClickableSpanCalled.set(false); 130 } 131 132 @AfterClass postTestTearDown()133 public static void postTestTearDown() { 134 sUiAutomation.destroy(); 135 } 136 137 @Test testNotEditableTextView_shouldNotExposeOrRespondToSetTextAction()138 public void testNotEditableTextView_shouldNotExposeOrRespondToSetTextAction() { 139 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 140 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 141 142 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 143 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 144 145 assertFalse("Standard text view should not support SET_TEXT", text.getActionList() 146 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT)); 147 assertEquals("Standard text view should not support SET_TEXT", 0, 148 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT); 149 Bundle args = new Bundle(); 150 args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, 151 mActivity.getString(R.string.text_input_blah)); 152 assertFalse(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)); 153 154 sInstrumentation.waitForIdleSync(); 155 assertTrue("Text view should not update on failed set text", 156 TextUtils.equals(mActivity.getString(R.string.a_b), textView.getText())); 157 } 158 159 @Test testEditableTextView_shouldExposeAndRespondToSetTextAction()160 public void testEditableTextView_shouldExposeAndRespondToSetTextAction() { 161 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 162 163 sInstrumentation.runOnMainSync(new Runnable() { 164 @Override 165 public void run() { 166 textView.setVisibility(View.VISIBLE); 167 textView.setText(mActivity.getString(R.string.a_b), TextView.BufferType.EDITABLE); 168 } 169 }); 170 171 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 172 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 173 174 assertTrue("Editable text view should support SET_TEXT", text.getActionList() 175 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT)); 176 assertEquals("Editable text view should support SET_TEXT", 177 AccessibilityNodeInfo.ACTION_SET_TEXT, 178 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT); 179 180 Bundle args = new Bundle(); 181 String textToSet = mActivity.getString(R.string.text_input_blah); 182 args.putCharSequence( 183 AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, textToSet); 184 185 assertTrue(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)); 186 187 sInstrumentation.waitForIdleSync(); 188 assertTrue("Editable text should update on set text", 189 TextUtils.equals(textToSet, textView.getText())); 190 } 191 192 @Test testEditText_shouldExposeAndRespondToSetTextAction()193 public void testEditText_shouldExposeAndRespondToSetTextAction() { 194 final EditText editText = (EditText) mActivity.findViewById(R.id.edit); 195 makeTextViewVisibleAndSetText(editText, mActivity.getString(R.string.a_b)); 196 197 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 198 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 199 200 assertTrue("EditText should support SET_TEXT", text.getActionList() 201 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT)); 202 assertEquals("EditText view should support SET_TEXT", 203 AccessibilityNodeInfo.ACTION_SET_TEXT, 204 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT); 205 206 Bundle args = new Bundle(); 207 String textToSet = mActivity.getString(R.string.text_input_blah); 208 args.putCharSequence( 209 AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, textToSet); 210 211 assertTrue(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)); 212 213 sInstrumentation.waitForIdleSync(); 214 assertTrue("EditText should update on set text", 215 TextUtils.equals(textToSet, editText.getText())); 216 } 217 218 @Test testClickableSpan_shouldWorkFromAccessibilityService()219 public void testClickableSpan_shouldWorkFromAccessibilityService() { 220 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 221 final ClickableSpan clickableSpan = new ClickableSpan() { 222 @Override 223 public void onClick(View widget) { 224 assertEquals("Clickable span called back on wrong View", textView, widget); 225 onClickCallback(); 226 } 227 }; 228 final SpannableString textWithClickableSpan = 229 new SpannableString(mActivity.getString(R.string.a_b)); 230 textWithClickableSpan.setSpan(clickableSpan, 0, 1, 0); 231 makeTextViewVisibleAndSetText(textView, textWithClickableSpan); 232 233 ClickableSpan clickableSpanFromA11y 234 = findSingleSpanInViewWithText(R.string.a_b, ClickableSpan.class); 235 clickableSpanFromA11y.onClick(null); 236 assertOnClickCalled(); 237 } 238 239 @Test testUrlSpan_shouldWorkFromAccessibilityService()240 public void testUrlSpan_shouldWorkFromAccessibilityService() { 241 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 242 final String url = "com.android.some.random.url"; 243 final URLSpan urlSpan = new URLSpan(url) { 244 @Override 245 public void onClick(View widget) { 246 assertEquals("Url span called back on wrong View", textView, widget); 247 onClickCallback(); 248 } 249 }; 250 final SpannableString textWithClickableSpan = 251 new SpannableString(mActivity.getString(R.string.a_b)); 252 textWithClickableSpan.setSpan(urlSpan, 0, 1, 0); 253 makeTextViewVisibleAndSetText(textView, textWithClickableSpan); 254 255 URLSpan urlSpanFromA11y = findSingleSpanInViewWithText(R.string.a_b, URLSpan.class); 256 assertEquals(url, urlSpanFromA11y.getURL()); 257 urlSpanFromA11y.onClick(null); 258 259 assertOnClickCalled(); 260 } 261 262 @Test testImageSpan_accessibilityServiceShouldSeeContentDescription()263 public void testImageSpan_accessibilityServiceShouldSeeContentDescription() { 264 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 265 final Bitmap bitmap = Bitmap.createBitmap(/* width= */10, /* height= */10, 266 Bitmap.Config.ARGB_8888); 267 final ImageSpan imageSpan = new ImageSpan(mActivity, bitmap); 268 final String contentDescription = mActivity.getString(R.string.contentDescription); 269 imageSpan.setContentDescription(contentDescription); 270 final SpannableString textWithImageSpan = 271 new SpannableString(mActivity.getString(R.string.a_b)); 272 textWithImageSpan.setSpan(imageSpan, /* start= */0, /* end= */1, /* flags= */0); 273 makeTextViewVisibleAndSetText(textView, textWithImageSpan); 274 275 ReplacementSpan replacementSpanFromA11y = findSingleSpanInViewWithText(R.string.a_b, 276 ReplacementSpan.class); 277 278 assertEquals(contentDescription, replacementSpanFromA11y.getContentDescription()); 279 } 280 281 @Test testTextLocations_textViewShouldProvideWhenRequested()282 public void testTextLocations_textViewShouldProvideWhenRequested() { 283 testTextViewProvidesLocationsWhenRequested(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 284 } 285 286 @Test 287 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocations_textViewShouldProvideWhenRequestedInWindow()288 public void testTextLocations_textViewShouldProvideWhenRequestedInWindow() { 289 testTextViewProvidesLocationsWhenRequested( 290 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 291 } 292 testTextViewProvidesLocationsWhenRequested(String extraDataKey)293 private void testTextViewProvidesLocationsWhenRequested(String extraDataKey) { 294 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 295 // Use text with a strong s, since that gets replaced with a double s for all caps. 296 // That replacement requires us to properly handle the length of the string changing. 297 String stringToSet = mActivity.getString(R.string.german_text_with_strong_s); 298 makeTextViewVisibleAndSetText(textView, stringToSet); 299 sInstrumentation.runOnMainSync(() -> textView.setAllCaps(true)); 300 301 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 302 .findAccessibilityNodeInfosByText(stringToSet).get(0); 303 List<String> textAvailableExtraData = text.getAvailableExtraData(); 304 assertTrue("Text view should offer text location to accessibility", 305 textAvailableExtraData.contains(extraDataKey)); 306 assertNull("Text locations should not be populated by default", 307 text.getExtras().getString(extraDataKey)); 308 309 waitForExtraTextData(text, extraDataKey); 310 assertNodeContainsTextLocationInfoOnOneLineLTR(text, extraDataKey); 311 } 312 313 @Test 314 @FlakyTest testTextLocations_textOutsideOfViewBounds_locationsShouldBeNull()315 public void testTextLocations_textOutsideOfViewBounds_locationsShouldBeNull() { 316 testTextOusideOfViewBounds_locationsInWindowsNull(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 317 } 318 319 @Test 320 @FlakyTest 321 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocations_textOutsideOfViewBounds_locationsInWindowShouldBeNull()322 public void testTextLocations_textOutsideOfViewBounds_locationsInWindowShouldBeNull() { 323 testTextOusideOfViewBounds_locationsInWindowsNull( 324 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 325 } 326 testTextOusideOfViewBounds_locationsInWindowsNull(String extraDataKey)327 private void testTextOusideOfViewBounds_locationsInWindowsNull(String extraDataKey) { 328 final EditText editText = mActivity.findViewById(R.id.edit); 329 makeTextViewVisibleAndSetText(editText, mActivity.getString(R.string.android_wiki)); 330 331 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 332 .findAccessibilityNodeInfosByText( 333 mActivity.getString(R.string.android_wiki)).get(0); 334 List<String> textAvailableExtraData = text.getAvailableExtraData(); 335 assertTrue("Text view should offer text location to accessibility", 336 textAvailableExtraData.contains(extraDataKey)); 337 338 Bundle extras = waitForExtraTextData(text, extraDataKey); 339 Parcelable[] parcelables = extras.getParcelableArray( 340 extraDataKey, RectF.class); 341 assertNotNull(parcelables); 342 final RectF[] locationsBeforeScroll = Arrays.copyOf( 343 parcelables, parcelables.length, RectF[].class); 344 assertEquals(text.getText().length(), locationsBeforeScroll.length); 345 // The first character should be visible immediately 346 assertFalse(locationsBeforeScroll[0].isEmpty()); 347 // Some of the characters should be off the screen, and thus have empty rects. Find the 348 // break point 349 int firstNullRectIndex = -1; 350 for (int i = 1; i < locationsBeforeScroll.length; i++) { 351 boolean isNull = locationsBeforeScroll[i] == null; 352 if (firstNullRectIndex < 0) { 353 if (isNull) { 354 firstNullRectIndex = i; 355 } 356 } else { 357 assertTrue(isNull); 358 } 359 } 360 361 // Scroll down one line 362 sInstrumentation.runOnMainSync(() -> { 363 int[] viewPosition = new int[2]; 364 editText.getLocationOnScreen(viewPosition); 365 final int oneLineDownY = (int) locationsBeforeScroll[0].bottom - viewPosition[1]; 366 editText.scrollTo(0, oneLineDownY + 1); 367 }); 368 369 extras = waitForExtraTextData(text, extraDataKey); 370 parcelables = extras 371 .getParcelableArray(extraDataKey, RectF.class); 372 assertNotNull(parcelables); 373 final RectF[] locationsAfterScroll = Arrays.copyOf( 374 parcelables, parcelables.length, RectF[].class); 375 // Now the first character should be off the screen 376 assertNull(locationsAfterScroll[0]); 377 // The first character that was off the screen should now be on it 378 assertNotNull(locationsAfterScroll[firstNullRectIndex]); 379 } 380 381 @Test testTextLocations_withRequestPreparer_shouldHoldOffUntilReady()382 public void testTextLocations_withRequestPreparer_shouldHoldOffUntilReady() { 383 testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( 384 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 385 } 386 387 @Test 388 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocationsInWindow_withRequestPreparer_shouldHoldOffUntilReady()389 public void testTextLocationsInWindow_withRequestPreparer_shouldHoldOffUntilReady() { 390 testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( 391 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 392 } 393 testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( String extraDataKey)394 private void testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( 395 String extraDataKey) { 396 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 397 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 398 399 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 400 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 401 final List<String> textAvailableExtraData = text.getAvailableExtraData(); 402 final Bundle getTextArgs = getTextLocationArguments(text.getText().length()); 403 404 // Register a request preparer that will capture the message indicating that preparation 405 // is complete 406 final AtomicReference<Message> messageRefForPrepare = new AtomicReference<>(null); 407 // Use mockito's asynchronous signaling 408 Runnable mockRunnableForPrepare = mock(Runnable.class); 409 410 AccessibilityManager a11yManager = 411 mActivity.getSystemService(AccessibilityManager.class); 412 assertNotNull(a11yManager); 413 AccessibilityRequestPreparer requestPreparer = new AccessibilityRequestPreparer( 414 textView, AccessibilityRequestPreparer.REQUEST_TYPE_EXTRA_DATA) { 415 @Override 416 public void onPrepareExtraData(int virtualViewId, 417 String preparedExtraDataKey, Bundle args, Message preparationFinishedMessage) { 418 assertEquals(AccessibilityNodeProvider.HOST_VIEW_ID, virtualViewId); 419 assertEquals(extraDataKey, preparedExtraDataKey); 420 assertEquals(0, args.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX)); 421 assertEquals(text.getText().length(), 422 args.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH)); 423 messageRefForPrepare.set(preparationFinishedMessage); 424 mockRunnableForPrepare.run(); 425 } 426 }; 427 a11yManager.addAccessibilityRequestPreparer(requestPreparer); 428 verify(mockRunnableForPrepare, times(0)).run(); 429 430 // Make the extra data request in another thread 431 Runnable mockRunnableForData = mock(Runnable.class); 432 new Thread(()-> { 433 waitForExtraTextData(text, extraDataKey); 434 mockRunnableForData.run(); 435 }).start(); 436 437 // The extra data request should trigger the request preparer 438 verify(mockRunnableForPrepare, timeout(DEFAULT_TIMEOUT_MS)).run(); 439 // Verify that the request for extra data didn't return. This is a bit racy, as we may still 440 // not catch it if it does return prematurely, but it does provide some protection. 441 sInstrumentation.waitForIdleSync(); 442 verify(mockRunnableForData, times(0)).run(); 443 444 // Declare preparation for the request complete, and verify that it runs to completion 445 messageRefForPrepare.get().sendToTarget(); 446 verify(mockRunnableForData, timeout(DEFAULT_TIMEOUT_MS)).run(); 447 assertNodeContainsTextLocationInfoOnOneLineLTR(text, extraDataKey); 448 a11yManager.removeAccessibilityRequestPreparer(requestPreparer); 449 } 450 451 @Test 452 @FlakyTest testTextLocations_withUnresponsiveRequestPreparer_shouldTimeout()453 public void testTextLocations_withUnresponsiveRequestPreparer_shouldTimeout() { 454 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 455 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 456 457 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 458 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 459 final List<String> textAvailableExtraData = text.getAvailableExtraData(); 460 final Bundle getTextArgs = getTextLocationArguments(text.getText().length()); 461 462 // Use mockito's asynchronous signaling 463 Runnable mockRunnableForPrepare = mock(Runnable.class); 464 465 AccessibilityManager a11yManager = 466 mActivity.getSystemService(AccessibilityManager.class); 467 AccessibilityRequestPreparer requestPreparer = new AccessibilityRequestPreparer( 468 textView, AccessibilityRequestPreparer.REQUEST_TYPE_EXTRA_DATA) { 469 @Override 470 public void onPrepareExtraData(int virtualViewId, 471 String extraDataKey, Bundle args, Message preparationFinishedMessage) { 472 mockRunnableForPrepare.run(); 473 } 474 }; 475 a11yManager.addAccessibilityRequestPreparer(requestPreparer); 476 verify(mockRunnableForPrepare, times(0)).run(); 477 478 // Make the extra data request in another thread 479 Runnable mockRunnableForData = mock(Runnable.class); 480 new Thread(() -> { 481 /* 482 * Don't worry about the return value, as we're timing out. We're just making 483 * sure that we don't hang the system. 484 */ 485 waitForExtraTextData(text, EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 486 mockRunnableForData.run(); 487 }).start(); 488 489 // The extra data request should trigger the request preparer 490 verify(mockRunnableForPrepare, timeout(DEFAULT_TIMEOUT_MS)).run(); 491 492 // Declare preparation for the request complete, and verify that it runs to completion 493 verify(mockRunnableForData, timeout(DEFAULT_TIMEOUT_MS)).run(); 494 a11yManager.removeAccessibilityRequestPreparer(requestPreparer); 495 } 496 497 @Test 498 @FlakyTest testTextLocation_testLocationBoundary_locationShouldBeLimitationLength()499 public void testTextLocation_testLocationBoundary_locationShouldBeLimitationLength() { 500 textTextLocationBoundaryShouldBeLimitedLength(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 501 } 502 503 @Test 504 @FlakyTest 505 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocation_testLocationBoundary_locationInWindowShouldBeLimitationLength()506 public void testTextLocation_testLocationBoundary_locationInWindowShouldBeLimitationLength() { 507 textTextLocationBoundaryShouldBeLimitedLength( 508 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 509 } 510 textTextLocationBoundaryShouldBeLimitedLength(String extraDataKey)511 private void textTextLocationBoundaryShouldBeLimitedLength(String extraDataKey) { 512 final TextView textView = mActivity.findViewById(R.id.text); 513 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 514 515 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 516 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 517 518 Bundle extras = waitForExtraTextData(text, extraDataKey, Integer.MAX_VALUE); 519 520 final Parcelable[] parcelables = extras.getParcelableArray(extraDataKey, RectF.class); 521 assertNotNull(parcelables); 522 final RectF[] locations = Arrays.copyOf(parcelables, parcelables.length, RectF[].class); 523 assertEquals(locations.length, 524 AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH); 525 } 526 527 @Test testEditableTextView_shouldExposeAndRespondToImeEnterAction()528 public void testEditableTextView_shouldExposeAndRespondToImeEnterAction() throws Throwable { 529 final TextView textView = (TextView) mActivity.findViewById(R.id.editText); 530 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 531 sInstrumentation.runOnMainSync(() -> textView.requestFocus()); 532 assertTrue(textView.isFocused()); 533 534 final TextView.OnEditorActionListener mockOnEditorActionListener = 535 mock(TextView.OnEditorActionListener.class); 536 textView.setOnEditorActionListener(mockOnEditorActionListener); 537 verifyZeroInteractions(mockOnEditorActionListener); 538 539 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 540 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 541 verifyImeActionLabel(text, sInstrumentation.getContext().getString( 542 R.string.accessibility_action_ime_enter_label)); 543 text.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER.getId()); 544 verify(mockOnEditorActionListener, times(1)).onEditorAction( 545 textView, EditorInfo.IME_ACTION_UNSPECIFIED, null); 546 547 // Testing custom ime action : IME_ACTION_DONE. 548 sInstrumentation.runOnMainSync(() -> textView.requestFocus()); 549 textView.setImeActionLabel("pinyin", EditorInfo.IME_ACTION_DONE); 550 551 final AccessibilityNodeInfo textNode = sUiAutomation.getRootInActiveWindow() 552 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 553 verifyImeActionLabel(textNode, "pinyin"); 554 textNode.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER.getId()); 555 verify(mockOnEditorActionListener, times(1)).onEditorAction( 556 textView, EditorInfo.IME_ACTION_DONE, null); 557 } 558 559 @Test testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested()560 public void testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested() { 561 final DisplayMetrics displayMetrics = mActivity.getResources().getDisplayMetrics(); 562 final TextView textView = mActivity.findViewById(R.id.text); 563 final String stringToSet = mActivity.getString(R.string.foo_bar_baz); 564 final int expectedWidthInPx = textView.getLayoutParams().width; 565 final int expectedHeightInPx = textView.getLayoutParams().height; 566 final float expectedTextSize = textView.getTextSize(); 567 final float newTextSize = 20f; 568 final float expectedNewTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 569 newTextSize, displayMetrics); 570 makeTextViewVisibleAndSetText(textView, stringToSet); 571 572 final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow() 573 .findAccessibilityNodeInfosByText(stringToSet).get(0); 574 assertTrue("Text view should offer extra data to accessibility ", 575 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY)); 576 577 AccessibilityNodeInfo.ExtraRenderingInfo extraRenderingInfo; 578 assertNull(info.getExtraRenderingInfo()); 579 extraRenderingInfo = waitForExtraRenderingInfo(info); 580 assertNotNull(extraRenderingInfo); 581 assertNotNull(extraRenderingInfo.getLayoutSize()); 582 assertEquals(expectedWidthInPx, extraRenderingInfo.getLayoutSize().getWidth()); 583 assertEquals(expectedHeightInPx, extraRenderingInfo.getLayoutSize().getHeight()); 584 assertEquals(expectedTextSize, extraRenderingInfo.getTextSizeInPx(), 0f); 585 assertEquals(TypedValue.COMPLEX_UNIT_DIP, extraRenderingInfo.getTextSizeUnit()); 586 587 // After changing text size 588 sInstrumentation.runOnMainSync(() -> 589 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, newTextSize)); 590 extraRenderingInfo = waitForExtraRenderingInfo(info); 591 assertEquals(expectedNewTextSize, extraRenderingInfo.getTextSizeInPx(), 0f); 592 assertEquals(TypedValue.COMPLEX_UNIT_SP, extraRenderingInfo.getTextSizeUnit()); 593 } 594 595 @Test testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested()596 public void testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested() { 597 final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow() 598 .findAccessibilityNodeInfosByViewId( 599 "android.accessibilityservice.cts:id/viewGroup").get(0); 600 601 assertTrue("ViewGroup should offer extra data to accessibility", 602 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY)); 603 assertNull(info.getExtraRenderingInfo()); 604 AccessibilityNodeInfo.ExtraRenderingInfo renderingInfo = waitForExtraRenderingInfo(info); 605 assertNotNull(renderingInfo); 606 assertNotNull(renderingInfo.getLayoutSize()); 607 final Size size = renderingInfo.getLayoutSize(); 608 assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, size.getWidth()); 609 assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, size.getHeight()); 610 } 611 verifyImeActionLabel(AccessibilityNodeInfo node, String label)612 private void verifyImeActionLabel(AccessibilityNodeInfo node, String label) { 613 final List<AccessibilityNodeInfo.AccessibilityAction> actionList = node.getActionList(); 614 final int indexOfActionImeEnter = 615 actionList.indexOf(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER); 616 assertTrue(indexOfActionImeEnter >= 0); 617 618 final AccessibilityNodeInfo.AccessibilityAction action = 619 actionList.get(indexOfActionImeEnter); 620 assertEquals(action.getLabel().toString(), label); 621 } 622 getTextLocationArguments(int locationLength)623 private Bundle getTextLocationArguments(int locationLength) { 624 Bundle args = new Bundle(); 625 args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, 0); 626 args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, locationLength); 627 return args; 628 } 629 assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info, String extraDataKey)630 private void assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info, 631 String extraDataKey) { 632 Bundle extras = waitForExtraTextData(info, extraDataKey); 633 final Parcelable[] parcelables = extras.getParcelableArray(extraDataKey, RectF.class); 634 assertNotNull(parcelables); 635 final RectF[] locations = Arrays.copyOf(parcelables, parcelables.length, RectF[].class); 636 assertEquals(info.getText().length(), locations.length); 637 // The text should all be on one line, running left to right 638 for (int i = 0; i < locations.length; i++) { 639 if (i != 0 && locations[i] == null) { 640 // If we run into an off-screen character after at least one on-screen character 641 // then stop checking the rest of the character locations. 642 break; 643 } 644 assertEquals(locations[0].top, locations[i].top, 0.01); 645 assertEquals(locations[0].bottom, locations[i].bottom, 0.01); 646 assertTrue(locations[i].right > locations[i].left); 647 if (i > 0) { 648 assertTrue(locations[i].left > locations[i-1].left); 649 } 650 } 651 } 652 onClickCallback()653 private void onClickCallback() { 654 synchronized (mClickableSpanCallbackLock) { 655 mClickableSpanCalled.set(true); 656 mClickableSpanCallbackLock.notifyAll(); 657 } 658 } 659 assertOnClickCalled()660 private void assertOnClickCalled() { 661 synchronized (mClickableSpanCallbackLock) { 662 long endTime = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS; 663 while (!mClickableSpanCalled.get() && (System.currentTimeMillis() < endTime)) { 664 try { 665 mClickableSpanCallbackLock.wait(endTime - System.currentTimeMillis()); 666 } catch (InterruptedException e) {} 667 } 668 } 669 assert(mClickableSpanCalled.get()); 670 } 671 findSingleSpanInViewWithText(int stringId, Class<T> type)672 private <T> T findSingleSpanInViewWithText(int stringId, Class<T> type) { 673 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 674 .findAccessibilityNodeInfosByText(mActivity.getString(stringId)).get(0); 675 CharSequence accessibilityTextWithSpan = text.getText(); 676 // The span should work even with the node recycled 677 text.recycle(); 678 assertTrue(accessibilityTextWithSpan instanceof Spanned); 679 680 T spans[] = ((Spanned) accessibilityTextWithSpan) 681 .getSpans(0, accessibilityTextWithSpan.length(), type); 682 assertEquals(1, spans.length); 683 return spans[0]; 684 } 685 makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text)686 private void makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text) { 687 sInstrumentation.runOnMainSync(() -> { 688 textView.setVisibility(View.VISIBLE); 689 textView.setText(text); 690 }); 691 sInstrumentation.waitForIdleSync(); 692 } 693 waitForExtraTextData(AccessibilityNodeInfo info, String key)694 private Bundle waitForExtraTextData(AccessibilityNodeInfo info, String key) { 695 return waitForExtraTextData(info, key, info.getText().length()); 696 } 697 waitForExtraTextData(AccessibilityNodeInfo info, String key, int length)698 private Bundle waitForExtraTextData(AccessibilityNodeInfo info, String key, int length) { 699 final Bundle getTextArgs = getTextLocationArguments(length); 700 // Node refresh must succeed and the resulting extras must contain the requested key. 701 try { 702 TestUtils.waitUntil("Timed out waiting for extra data", () -> { 703 info.refreshWithExtraData(key, getTextArgs); 704 return info.getExtras().containsKey(key); 705 }); 706 } catch (Exception e) { 707 fail(e.getMessage()); 708 } 709 710 return info.getExtras(); 711 } 712 waitForExtraRenderingInfo( AccessibilityNodeInfo info)713 private AccessibilityNodeInfo.ExtraRenderingInfo waitForExtraRenderingInfo( 714 AccessibilityNodeInfo info) { 715 // Node refresh must succeed and extraRenderingInfo must not be null. 716 try { 717 TestUtils.waitUntil("Timed out waiting for extra rendering data", () -> { 718 info.refreshWithExtraData( 719 EXTRA_DATA_RENDERING_INFO_KEY, new Bundle()); 720 return info.getExtraRenderingInfo() != null; 721 }); 722 } catch (Exception e) { 723 fail(e.getMessage()); 724 } 725 726 return info.getExtraRenderingInfo(); 727 } 728 } 729