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.homeScreenOrBust; 18 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS; 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 20 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; 21 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_RENDERING_INFO_KEY; 22 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH; 23 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX; 24 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY; 25 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; 26 27 import static com.google.common.truth.Truth.assertThat; 28 import static com.google.common.truth.Truth.assertWithMessage; 29 30 import static org.junit.Assert.assertEquals; 31 import static org.junit.Assert.assertFalse; 32 import static org.junit.Assert.assertNotNull; 33 import static org.junit.Assert.assertNull; 34 import static org.junit.Assert.assertTrue; 35 import static org.junit.Assert.fail; 36 import static org.junit.Assume.assumeTrue; 37 import static org.mockito.Mockito.mock; 38 import static org.mockito.Mockito.timeout; 39 import static org.mockito.Mockito.times; 40 import static org.mockito.Mockito.verify; 41 import static org.mockito.Mockito.verifyNoMoreInteractions; 42 43 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule; 44 import android.accessibilityservice.AccessibilityServiceInfo; 45 import android.accessibilityservice.cts.activities.AccessibilityTextTraversalActivity; 46 import android.accessibilityservice.cts.activities.AccessibilityTextViewActivity; 47 import android.app.ActivityOptions; 48 import android.app.Instrumentation; 49 import android.app.UiAutomation; 50 import android.graphics.Bitmap; 51 import android.graphics.Rect; 52 import android.graphics.RectF; 53 import android.os.Bundle; 54 import android.os.Message; 55 import android.os.Parcelable; 56 import android.os.SystemClock; 57 import android.platform.test.annotations.Presubmit; 58 import android.platform.test.annotations.RequiresFlagsEnabled; 59 import android.platform.test.flag.junit.CheckFlagsRule; 60 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 61 import android.text.SpannableString; 62 import android.text.Spanned; 63 import android.text.TextUtils; 64 import android.text.style.ClickableSpan; 65 import android.text.style.ImageSpan; 66 import android.text.style.ReplacementSpan; 67 import android.text.style.URLSpan; 68 import android.util.DisplayMetrics; 69 import android.util.Size; 70 import android.util.TypedValue; 71 import android.view.Display; 72 import android.view.View; 73 import android.view.ViewGroup; 74 import android.view.accessibility.AccessibilityManager; 75 import android.view.accessibility.AccessibilityNodeInfo; 76 import android.view.accessibility.AccessibilityNodeProvider; 77 import android.view.accessibility.AccessibilityRequestPreparer; 78 import android.view.accessibility.AccessibilityWindowInfo; 79 import android.view.inputmethod.EditorInfo; 80 import android.widget.EditText; 81 import android.widget.TextView; 82 83 import androidx.lifecycle.Lifecycle; 84 import androidx.test.core.app.ActivityScenario; 85 import androidx.test.ext.junit.rules.ActivityScenarioRule; 86 import androidx.test.ext.junit.runners.AndroidJUnit4; 87 import androidx.test.filters.FlakyTest; 88 import androidx.test.platform.app.InstrumentationRegistry; 89 90 import com.android.compatibility.common.util.CddTest; 91 import com.android.compatibility.common.util.TestUtils; 92 93 import org.junit.AfterClass; 94 import org.junit.Before; 95 import org.junit.BeforeClass; 96 import org.junit.Rule; 97 import org.junit.Test; 98 import org.junit.rules.RuleChain; 99 import org.junit.runner.RunWith; 100 101 import java.util.List; 102 import java.util.concurrent.atomic.AtomicBoolean; 103 import java.util.concurrent.atomic.AtomicReference; 104 105 /** 106 * Test cases for actions taken on text views. 107 */ 108 @RunWith(AndroidJUnit4.class) 109 @CddTest(requirements = {"3.10/C-1-1,C-1-2"}) 110 @Presubmit 111 public class AccessibilityTextActionTest { 112 private static Instrumentation sInstrumentation; 113 private static UiAutomation sUiAutomation; 114 final Object mClickableSpanCallbackLock = new Object(); 115 final AtomicBoolean mClickableSpanCalled = new AtomicBoolean(false); 116 117 @Rule 118 public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); 119 120 private AccessibilityTextTraversalActivity mActivity; 121 122 private ActivityScenarioRule<AccessibilityTextTraversalActivity> mActivityRule = 123 new ActivityScenarioRule<>(AccessibilityTextTraversalActivity.class); 124 125 private AccessibilityDumpOnFailureRule mDumpOnFailureRule = 126 new AccessibilityDumpOnFailureRule(); 127 128 @Rule 129 public final RuleChain mRuleChain = RuleChain 130 .outerRule(mActivityRule) 131 .around(mDumpOnFailureRule); 132 133 @BeforeClass oneTimeSetup()134 public static void oneTimeSetup() throws Exception { 135 sInstrumentation = InstrumentationRegistry.getInstrumentation(); 136 sUiAutomation = sInstrumentation.getUiAutomation(); 137 } 138 139 @Before setUp()140 public void setUp() throws Exception { 141 mActivityRule 142 .getScenario() 143 .moveToState(Lifecycle.State.RESUMED) 144 .onActivity(activity -> mActivity = activity); 145 mClickableSpanCalled.set(false); 146 } 147 148 @AfterClass postTestTearDown()149 public static void postTestTearDown() { 150 sUiAutomation.destroy(); 151 } 152 153 @Test testNotEditableTextView_shouldNotExposeOrRespondToSetTextAction()154 public void testNotEditableTextView_shouldNotExposeOrRespondToSetTextAction() { 155 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 156 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 157 158 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 159 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 160 161 assertFalse("Standard text view should not support SET_TEXT", text.getActionList() 162 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT)); 163 assertEquals("Standard text view should not support SET_TEXT", 0, 164 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT); 165 Bundle args = new Bundle(); 166 args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, 167 mActivity.getString(R.string.text_input_blah)); 168 assertFalse(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)); 169 170 sInstrumentation.waitForIdleSync(); 171 assertTrue("Text view should not update on failed set text", 172 TextUtils.equals(mActivity.getString(R.string.a_b), textView.getText())); 173 } 174 175 @Test testEditableTextView_shouldExposeAndRespondToSetTextAction()176 public void testEditableTextView_shouldExposeAndRespondToSetTextAction() { 177 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 178 179 sInstrumentation.runOnMainSync(new Runnable() { 180 @Override 181 public void run() { 182 textView.setVisibility(View.VISIBLE); 183 textView.setText(mActivity.getString(R.string.a_b), TextView.BufferType.EDITABLE); 184 } 185 }); 186 187 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 188 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 189 190 assertTrue("Editable text view should support SET_TEXT", text.getActionList() 191 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT)); 192 assertEquals("Editable text view should support SET_TEXT", 193 AccessibilityNodeInfo.ACTION_SET_TEXT, 194 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT); 195 196 Bundle args = new Bundle(); 197 String textToSet = mActivity.getString(R.string.text_input_blah); 198 args.putCharSequence( 199 AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, textToSet); 200 201 assertTrue(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)); 202 203 sInstrumentation.waitForIdleSync(); 204 assertTrue("Editable text should update on set text", 205 TextUtils.equals(textToSet, textView.getText())); 206 } 207 208 @Test testEditText_shouldExposeAndRespondToSetTextAction()209 public void testEditText_shouldExposeAndRespondToSetTextAction() { 210 final EditText editText = (EditText) mActivity.findViewById(R.id.edit); 211 makeTextViewVisibleAndSetText(editText, mActivity.getString(R.string.a_b)); 212 213 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 214 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 215 216 assertTrue("EditText should support SET_TEXT", text.getActionList() 217 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT)); 218 assertEquals("EditText view should support SET_TEXT", 219 AccessibilityNodeInfo.ACTION_SET_TEXT, 220 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT); 221 222 Bundle args = new Bundle(); 223 String textToSet = mActivity.getString(R.string.text_input_blah); 224 args.putCharSequence( 225 AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, textToSet); 226 227 assertTrue(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)); 228 229 sInstrumentation.waitForIdleSync(); 230 assertTrue("EditText should update on set text", 231 TextUtils.equals(textToSet, editText.getText())); 232 } 233 234 @Test testClickableSpan_shouldWorkFromAccessibilityService()235 public void testClickableSpan_shouldWorkFromAccessibilityService() { 236 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 237 final ClickableSpan clickableSpan = new ClickableSpan() { 238 @Override 239 public void onClick(View widget) { 240 assertEquals("Clickable span called back on wrong View", textView, widget); 241 onClickCallback(); 242 } 243 }; 244 final SpannableString textWithClickableSpan = 245 new SpannableString(mActivity.getString(R.string.a_b)); 246 textWithClickableSpan.setSpan(clickableSpan, 0, 1, 0); 247 makeTextViewVisibleAndSetText(textView, textWithClickableSpan); 248 249 ClickableSpan clickableSpanFromA11y 250 = findSingleSpanInViewWithText(R.string.a_b, ClickableSpan.class); 251 clickableSpanFromA11y.onClick(null); 252 assertOnClickCalled(); 253 } 254 255 @Test testUrlSpan_shouldWorkFromAccessibilityService()256 public void testUrlSpan_shouldWorkFromAccessibilityService() { 257 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 258 final String url = "com.android.some.random.url"; 259 final URLSpan urlSpan = new URLSpan(url) { 260 @Override 261 public void onClick(View widget) { 262 assertEquals("Url span called back on wrong View", textView, widget); 263 onClickCallback(); 264 } 265 }; 266 final SpannableString textWithClickableSpan = 267 new SpannableString(mActivity.getString(R.string.a_b)); 268 textWithClickableSpan.setSpan(urlSpan, 0, 1, 0); 269 makeTextViewVisibleAndSetText(textView, textWithClickableSpan); 270 271 URLSpan urlSpanFromA11y = findSingleSpanInViewWithText(R.string.a_b, URLSpan.class); 272 assertEquals(url, urlSpanFromA11y.getURL()); 273 urlSpanFromA11y.onClick(null); 274 275 assertOnClickCalled(); 276 } 277 278 @Test testImageSpan_accessibilityServiceShouldSeeContentDescription()279 public void testImageSpan_accessibilityServiceShouldSeeContentDescription() { 280 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 281 final Bitmap bitmap = Bitmap.createBitmap(/* width= */10, /* height= */10, 282 Bitmap.Config.ARGB_8888); 283 final ImageSpan imageSpan = new ImageSpan(mActivity, bitmap); 284 final String contentDescription = mActivity.getString(R.string.contentDescription); 285 imageSpan.setContentDescription(contentDescription); 286 final SpannableString textWithImageSpan = 287 new SpannableString(mActivity.getString(R.string.a_b)); 288 textWithImageSpan.setSpan(imageSpan, /* start= */0, /* end= */1, /* flags= */0); 289 makeTextViewVisibleAndSetText(textView, textWithImageSpan); 290 291 ReplacementSpan replacementSpanFromA11y = findSingleSpanInViewWithText(R.string.a_b, 292 ReplacementSpan.class); 293 294 assertEquals(contentDescription, replacementSpanFromA11y.getContentDescription()); 295 } 296 297 @Test testTextLocations_textViewShouldProvideWhenRequested()298 public void testTextLocations_textViewShouldProvideWhenRequested() { 299 testTextViewProvidesLocationsWhenRequested(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 300 } 301 302 @Test 303 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocations_textViewShouldProvideWhenRequestedInWindow()304 public void testTextLocations_textViewShouldProvideWhenRequestedInWindow() { 305 testTextViewProvidesLocationsWhenRequested( 306 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 307 } 308 testTextViewProvidesLocationsWhenRequested(String extraDataKey)309 private void testTextViewProvidesLocationsWhenRequested(String extraDataKey) { 310 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 311 // Use text with a strong s, since that gets replaced with a double s for all caps. 312 // That replacement requires us to properly handle the length of the string changing. 313 String stringToSet = mActivity.getString(R.string.german_text_with_strong_s); 314 makeTextViewVisibleAndSetText(textView, stringToSet); 315 sInstrumentation.runOnMainSync(() -> textView.setAllCaps(true)); 316 317 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 318 .findAccessibilityNodeInfosByText(stringToSet).get(0); 319 List<String> textAvailableExtraData = text.getAvailableExtraData(); 320 assertTrue("Text view should offer text location to accessibility", 321 textAvailableExtraData.contains(extraDataKey)); 322 assertNull("Text locations should not be populated by default", 323 text.getExtras().getString(extraDataKey)); 324 325 waitForExtraTextData(text, extraDataKey); 326 assertNodeContainsTextLocationInfoOnOneLineLTR(text, extraDataKey); 327 } 328 329 @Test 330 @FlakyTest testTextLocations_textOutsideOfViewBounds_locationsShouldBeNull()331 public void testTextLocations_textOutsideOfViewBounds_locationsShouldBeNull() { 332 testTextOutsideOfViewBounds_locationsInWindowsNull(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 333 } 334 335 @Test 336 @FlakyTest 337 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocations_textOutsideOfViewBounds_locationsInWindowShouldBeNull()338 public void testTextLocations_textOutsideOfViewBounds_locationsInWindowShouldBeNull() { 339 testTextOutsideOfViewBounds_locationsInWindowsNull( 340 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 341 } 342 testTextOutsideOfViewBounds_locationsInWindowsNull(String extraDataKey)343 private void testTextOutsideOfViewBounds_locationsInWindowsNull(String extraDataKey) { 344 final EditText editText = mActivity.findViewById(R.id.edit); 345 makeTextViewVisibleAndSetText(editText, mActivity.getString(R.string.android_wiki)); 346 347 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 348 .findAccessibilityNodeInfosByText( 349 mActivity.getString(R.string.android_wiki)).get(0); 350 List<String> textAvailableExtraData = text.getAvailableExtraData(); 351 assertTrue("Text view should offer text location to accessibility", 352 textAvailableExtraData.contains(extraDataKey)); 353 354 Bundle extras = waitForExtraTextData(text, extraDataKey); 355 Parcelable[] parcelables = extras.getParcelableArray( 356 extraDataKey, RectF.class); 357 assertNotNull(parcelables); 358 final RectF[] locationsBeforeScroll = (RectF[]) parcelables; 359 assertEquals(text.getText().length(), locationsBeforeScroll.length); 360 // The first character should be visible immediately. 361 assertFalse(locationsBeforeScroll[0].isEmpty()); 362 // Some of the characters should be off the screen, and thus have empty rects. Find the 363 // break point. 364 int firstNullRectIndex = -1; 365 for (int i = 1; i < locationsBeforeScroll.length; i++) { 366 boolean isNull = locationsBeforeScroll[i] == null; 367 if (firstNullRectIndex < 0) { 368 if (isNull) { 369 firstNullRectIndex = i; 370 } 371 } else { 372 assertTrue(isNull); 373 } 374 } 375 376 // Scroll down one line. 377 sInstrumentation.runOnMainSync( 378 () -> { 379 // Calculate the height of a line from the relative character heights. 380 int firstLineBottom = (int) locationsBeforeScroll[0].bottom; 381 int i = 1; 382 while ((int) locationsBeforeScroll[i].bottom == firstLineBottom) { 383 i++; 384 } 385 final int oneLineDownY = 386 (int) locationsBeforeScroll[i].bottom - firstLineBottom; 387 editText.scrollTo(0, oneLineDownY + 1); 388 }); 389 390 extras = waitForExtraTextData(text, extraDataKey); 391 parcelables = extras 392 .getParcelableArray(extraDataKey, RectF.class); 393 assertNotNull(parcelables); 394 final RectF[] locationsAfterScroll = (RectF[]) parcelables; 395 // Now the first character should be off the screen. 396 assertNull(locationsAfterScroll[0]); 397 // The first character that was off the screen should now be on it. 398 assertNotNull(locationsAfterScroll[firstNullRectIndex]); 399 } 400 401 @Test testTextLocations_withRequestPreparer_shouldHoldOffUntilReady()402 public void testTextLocations_withRequestPreparer_shouldHoldOffUntilReady() { 403 testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( 404 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 405 } 406 407 @Test 408 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocationsInWindow_withRequestPreparer_shouldHoldOffUntilReady()409 public void testTextLocationsInWindow_withRequestPreparer_shouldHoldOffUntilReady() { 410 testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( 411 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 412 } 413 testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( String extraDataKey)414 private void testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( 415 String extraDataKey) { 416 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 417 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 418 419 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 420 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 421 final List<String> textAvailableExtraData = text.getAvailableExtraData(); 422 final Bundle getTextArgs = getTextLocationArguments(text.getText().length()); 423 424 // Register a request preparer that will capture the message indicating that preparation 425 // is complete 426 final AtomicReference<Message> messageRefForPrepare = new AtomicReference<>(null); 427 // Use mockito's asynchronous signaling 428 Runnable mockRunnableForPrepare = mock(Runnable.class); 429 430 AccessibilityManager a11yManager = 431 mActivity.getSystemService(AccessibilityManager.class); 432 assertNotNull(a11yManager); 433 AccessibilityRequestPreparer requestPreparer = new AccessibilityRequestPreparer( 434 textView, AccessibilityRequestPreparer.REQUEST_TYPE_EXTRA_DATA) { 435 @Override 436 public void onPrepareExtraData(int virtualViewId, 437 String preparedExtraDataKey, Bundle args, Message preparationFinishedMessage) { 438 assertEquals(AccessibilityNodeProvider.HOST_VIEW_ID, virtualViewId); 439 assertEquals(extraDataKey, preparedExtraDataKey); 440 assertEquals(0, args.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX)); 441 assertEquals(text.getText().length(), 442 args.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH)); 443 messageRefForPrepare.set(preparationFinishedMessage); 444 mockRunnableForPrepare.run(); 445 } 446 }; 447 a11yManager.addAccessibilityRequestPreparer(requestPreparer); 448 verify(mockRunnableForPrepare, times(0)).run(); 449 450 // Make the extra data request in another thread 451 Runnable mockRunnableForData = mock(Runnable.class); 452 new Thread(()-> { 453 waitForExtraTextData(text, extraDataKey); 454 mockRunnableForData.run(); 455 }).start(); 456 457 // The extra data request should trigger the request preparer 458 verify(mockRunnableForPrepare, timeout(DEFAULT_TIMEOUT_MS)).run(); 459 // Verify that the request for extra data didn't return. This is a bit racy, as we may still 460 // not catch it if it does return prematurely, but it does provide some protection. 461 sInstrumentation.waitForIdleSync(); 462 verify(mockRunnableForData, times(0)).run(); 463 464 // Declare preparation for the request complete, and verify that it runs to completion 465 messageRefForPrepare.get().sendToTarget(); 466 verify(mockRunnableForData, timeout(DEFAULT_TIMEOUT_MS)).run(); 467 assertNodeContainsTextLocationInfoOnOneLineLTR(text, extraDataKey); 468 a11yManager.removeAccessibilityRequestPreparer(requestPreparer); 469 } 470 471 @Test 472 @FlakyTest testTextLocations_withUnresponsiveRequestPreparer_shouldTimeout()473 public void testTextLocations_withUnresponsiveRequestPreparer_shouldTimeout() { 474 final TextView textView = (TextView) mActivity.findViewById(R.id.text); 475 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 476 477 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 478 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 479 final List<String> textAvailableExtraData = text.getAvailableExtraData(); 480 final Bundle getTextArgs = getTextLocationArguments(text.getText().length()); 481 482 // Use mockito's asynchronous signaling 483 Runnable mockRunnableForPrepare = mock(Runnable.class); 484 485 AccessibilityManager a11yManager = 486 mActivity.getSystemService(AccessibilityManager.class); 487 AccessibilityRequestPreparer requestPreparer = new AccessibilityRequestPreparer( 488 textView, AccessibilityRequestPreparer.REQUEST_TYPE_EXTRA_DATA) { 489 @Override 490 public void onPrepareExtraData(int virtualViewId, 491 String extraDataKey, Bundle args, Message preparationFinishedMessage) { 492 mockRunnableForPrepare.run(); 493 } 494 }; 495 a11yManager.addAccessibilityRequestPreparer(requestPreparer); 496 verify(mockRunnableForPrepare, times(0)).run(); 497 498 // Make the extra data request in another thread 499 Runnable mockRunnableForData = mock(Runnable.class); 500 new Thread(() -> { 501 /* 502 * Don't worry about the return value, as we're timing out. We're just making 503 * sure that we don't hang the system. 504 */ 505 waitForExtraTextData(text, EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 506 mockRunnableForData.run(); 507 }).start(); 508 509 // The extra data request should trigger the request preparer 510 verify(mockRunnableForPrepare, timeout(DEFAULT_TIMEOUT_MS)).run(); 511 512 // Declare preparation for the request complete, and verify that it runs to completion 513 verify(mockRunnableForData, timeout(DEFAULT_TIMEOUT_MS)).run(); 514 a11yManager.removeAccessibilityRequestPreparer(requestPreparer); 515 } 516 517 @Test 518 @FlakyTest testTextLocation_testLocationBoundary_locationShouldBeLimitationLength()519 public void testTextLocation_testLocationBoundary_locationShouldBeLimitationLength() { 520 textTextLocationBoundaryShouldBeLimitedLength(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 521 } 522 523 @Test 524 @FlakyTest 525 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocation_testLocationBoundary_locationInWindowShouldBeLimitationLength()526 public void testTextLocation_testLocationBoundary_locationInWindowShouldBeLimitationLength() { 527 textTextLocationBoundaryShouldBeLimitedLength( 528 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 529 } 530 textTextLocationBoundaryShouldBeLimitedLength(String extraDataKey)531 private void textTextLocationBoundaryShouldBeLimitedLength(String extraDataKey) { 532 final TextView textView = mActivity.findViewById(R.id.text); 533 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 534 535 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 536 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 537 538 Bundle extras = waitForExtraTextData(text, extraDataKey, Integer.MAX_VALUE); 539 540 final Parcelable[] parcelables = extras.getParcelableArray(extraDataKey, RectF.class); 541 assertNotNull(parcelables); 542 final RectF[] locations = (RectF[]) parcelables; 543 assertEquals(locations.length, 544 AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH); 545 } 546 547 @Test testTextLocations_inFreeform_screenCoordinates()548 public void testTextLocations_inFreeform_screenCoordinates() throws Exception { 549 final int top = 100; 550 final int left = 200; 551 try (ActivityScenario<AccessibilityTextViewActivity> scenario = 552 launchTextViewActivityInFreeform(left, top)) { 553 scenario.onActivity( 554 textViewActivity -> { 555 // Waits for the node to be on-screen. 556 final AccessibilityNodeInfo info = 557 findNodeByText(textViewActivity.getString(R.string.foo_bar_baz)); 558 Bundle extras = 559 waitForExtraTextData(info, EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 560 final Parcelable[] parcelables = 561 extras.getParcelableArray( 562 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, RectF.class); 563 assertThat(parcelables).isNotNull(); 564 final RectF[] charLocations = (RectF[]) parcelables; 565 assertThat(charLocations).hasLength(info.getText().length()); 566 567 Rect windowBounds = new Rect(); 568 info.getWindow().getBoundsInScreen(windowBounds); 569 assertThat(windowBounds.left).isWithin(1).of(left); 570 assertThat(windowBounds.top).isWithin(1).of(top); 571 572 Rect nodeBoundsInScreen = new Rect(); 573 info.getBoundsInScreen(nodeBoundsInScreen); 574 575 for (RectF location : charLocations) { 576 // The character locations are within the window's location 577 // when both are represented in screen coordinates. 578 assertWithMessage( 579 "windowBounds %s contains character location %s", 580 windowBounds, location) 581 .that(new RectF(windowBounds).contains(location)) 582 .isTrue(); 583 584 // Double-check that the screen coordinates of the character are within 585 // the screen coordinates of the node. 586 assertWithMessage( 587 "nodeBoundsInScreen %s contains character location %s", 588 nodeBoundsInScreen, location) 589 .that(new RectF(nodeBoundsInScreen).contains(location)) 590 .isTrue(); 591 } 592 }); 593 } 594 } 595 596 @Test 597 @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API) testTextLocations_inFreeform_windowCoordinates()598 public void testTextLocations_inFreeform_windowCoordinates() throws Exception { 599 final int top = 100; 600 final int left = 200; 601 try (ActivityScenario<AccessibilityTextViewActivity> scenario = 602 launchTextViewActivityInFreeform(left, top)) { 603 scenario.onActivity( 604 textViewActivity -> { 605 // Waits for the node to be on-screen. 606 final AccessibilityNodeInfo info = 607 findNodeByText(textViewActivity.getString(R.string.foo_bar_baz)); 608 Bundle extras = 609 waitForExtraTextData( 610 info, EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY); 611 final Parcelable[] parcelables = 612 extras.getParcelableArray( 613 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY, 614 RectF.class); 615 assertThat(parcelables).isNotNull(); 616 final RectF[] charLocations = (RectF[]) parcelables; 617 assertThat(charLocations).hasLength(info.getText().length()); 618 619 // Check the window is in the right part of the screen. 620 Rect windowBounds = new Rect(); 621 info.getWindow().getBoundsInScreen(windowBounds); 622 assertThat(windowBounds.left).isWithin(1).of(left); 623 assertThat(windowBounds.top).isWithin(1).of(top); 624 625 // The first primary location should be at the left edge of the window. 626 assertThat(charLocations[0].left).isLessThan(1); 627 628 Rect nodeBoundsInWindow = new Rect(); 629 info.getBoundsInWindow(nodeBoundsInWindow); 630 631 for (RectF location : charLocations) { 632 // Check that the window coordinates of the character are within the 633 // window coordinates of the node. 634 assertWithMessage( 635 "nodeBoundsInWindow %s contains character location %s", 636 nodeBoundsInWindow, location) 637 .that(new RectF(nodeBoundsInWindow).contains(location)) 638 .isTrue(); 639 } 640 }); 641 } 642 } 643 launchTextViewActivityInFreeform( int left, int top)644 private ActivityScenario<AccessibilityTextViewActivity> launchTextViewActivityInFreeform( 645 int left, int top) { 646 assumeTrue( 647 sInstrumentation 648 .getContext() 649 .getPackageManager() 650 .hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)); 651 homeScreenOrBust(sInstrumentation.getContext(), sUiAutomation); 652 mActivityRule.getScenario().close(); 653 654 final ActivityOptions options = ActivityOptions.makeBasic(); 655 options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); 656 options.setLaunchBounds(new Rect(left, top, left + 400, top + 400)); 657 options.setLaunchDisplayId(Display.DEFAULT_DISPLAY); 658 return ActivityScenario.launch(AccessibilityTextViewActivity.class, options.toBundle()); 659 } 660 661 @Test testEditableTextView_shouldExposeAndRespondToImeEnterAction()662 public void testEditableTextView_shouldExposeAndRespondToImeEnterAction() throws Throwable { 663 final TextView textView = (TextView) mActivity.findViewById(R.id.editText); 664 makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b)); 665 sInstrumentation.runOnMainSync(() -> textView.requestFocus()); 666 assertTrue(textView.isFocused()); 667 668 final TextView.OnEditorActionListener mockOnEditorActionListener = 669 mock(TextView.OnEditorActionListener.class); 670 textView.setOnEditorActionListener(mockOnEditorActionListener); 671 verifyNoMoreInteractions(mockOnEditorActionListener); 672 673 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 674 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 675 verifyImeActionLabel(text, sInstrumentation.getContext().getString( 676 R.string.accessibility_action_ime_enter_label)); 677 text.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER.getId()); 678 verify(mockOnEditorActionListener, times(1)).onEditorAction( 679 textView, EditorInfo.IME_ACTION_UNSPECIFIED, null); 680 681 // Testing custom ime action : IME_ACTION_DONE. 682 sInstrumentation.runOnMainSync(() -> textView.requestFocus()); 683 textView.setImeActionLabel("pinyin", EditorInfo.IME_ACTION_DONE); 684 685 final AccessibilityNodeInfo textNode = sUiAutomation.getRootInActiveWindow() 686 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0); 687 verifyImeActionLabel(textNode, "pinyin"); 688 textNode.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER.getId()); 689 verify(mockOnEditorActionListener, times(1)).onEditorAction( 690 textView, EditorInfo.IME_ACTION_DONE, null); 691 } 692 693 @Test testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested()694 public void testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested() { 695 final DisplayMetrics displayMetrics = mActivity.getResources().getDisplayMetrics(); 696 final TextView textView = mActivity.findViewById(R.id.text); 697 final String stringToSet = mActivity.getString(R.string.foo_bar_baz); 698 final int expectedWidthInPx = textView.getLayoutParams().width; 699 final int expectedHeightInPx = textView.getLayoutParams().height; 700 final float expectedTextSize = textView.getTextSize(); 701 final float newTextSize = 20f; 702 final float expectedNewTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 703 newTextSize, displayMetrics); 704 makeTextViewVisibleAndSetText(textView, stringToSet); 705 706 final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow() 707 .findAccessibilityNodeInfosByText(stringToSet).get(0); 708 assertTrue("Text view should offer extra data to accessibility ", 709 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY)); 710 711 AccessibilityNodeInfo.ExtraRenderingInfo extraRenderingInfo; 712 assertNull(info.getExtraRenderingInfo()); 713 extraRenderingInfo = waitForExtraRenderingInfo(info); 714 assertNotNull(extraRenderingInfo); 715 assertNotNull(extraRenderingInfo.getLayoutSize()); 716 assertEquals(expectedWidthInPx, extraRenderingInfo.getLayoutSize().getWidth()); 717 assertEquals(expectedHeightInPx, extraRenderingInfo.getLayoutSize().getHeight()); 718 assertEquals(expectedTextSize, extraRenderingInfo.getTextSizeInPx(), 0f); 719 assertEquals(TypedValue.COMPLEX_UNIT_DIP, extraRenderingInfo.getTextSizeUnit()); 720 721 // After changing text size 722 sInstrumentation.runOnMainSync(() -> 723 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, newTextSize)); 724 extraRenderingInfo = waitForExtraRenderingInfo(info); 725 assertEquals(expectedNewTextSize, extraRenderingInfo.getTextSizeInPx(), 0f); 726 assertEquals(TypedValue.COMPLEX_UNIT_SP, extraRenderingInfo.getTextSizeUnit()); 727 } 728 729 @Test testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested()730 public void testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested() { 731 final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow() 732 .findAccessibilityNodeInfosByViewId( 733 "android.accessibilityservice.cts:id/viewGroup").get(0); 734 735 assertTrue("ViewGroup should offer extra data to accessibility", 736 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY)); 737 assertNull(info.getExtraRenderingInfo()); 738 AccessibilityNodeInfo.ExtraRenderingInfo renderingInfo = waitForExtraRenderingInfo(info); 739 assertNotNull(renderingInfo); 740 assertNotNull(renderingInfo.getLayoutSize()); 741 final Size size = renderingInfo.getLayoutSize(); 742 assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, size.getWidth()); 743 assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, size.getHeight()); 744 } 745 verifyImeActionLabel(AccessibilityNodeInfo node, String label)746 private void verifyImeActionLabel(AccessibilityNodeInfo node, String label) { 747 final List<AccessibilityNodeInfo.AccessibilityAction> actionList = node.getActionList(); 748 final int indexOfActionImeEnter = 749 actionList.indexOf(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER); 750 assertTrue(indexOfActionImeEnter >= 0); 751 752 final AccessibilityNodeInfo.AccessibilityAction action = 753 actionList.get(indexOfActionImeEnter); 754 assertEquals(action.getLabel().toString(), label); 755 } 756 getTextLocationArguments(int locationLength)757 private Bundle getTextLocationArguments(int locationLength) { 758 Bundle args = new Bundle(); 759 args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, 0); 760 args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, locationLength); 761 return args; 762 } 763 assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info, String extraDataKey)764 private void assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info, 765 String extraDataKey) { 766 Bundle extras = waitForExtraTextData(info, extraDataKey); 767 final Parcelable[] parcelables = extras.getParcelableArray(extraDataKey, RectF.class); 768 assertNotNull(parcelables); 769 final RectF[] locations = (RectF[]) parcelables; 770 assertEquals(info.getText().length(), locations.length); 771 // The text should all be on one line, running left to right 772 for (int i = 0; i < locations.length; i++) { 773 if (i != 0 && locations[i] == null) { 774 // If we run into an off-screen character after at least one on-screen character 775 // then stop checking the rest of the character locations. 776 break; 777 } 778 assertEquals(locations[0].top, locations[i].top, 0.01); 779 assertEquals(locations[0].bottom, locations[i].bottom, 0.01); 780 assertTrue(locations[i].right > locations[i].left); 781 if (i > 0) { 782 assertTrue(locations[i].left > locations[i-1].left); 783 } 784 } 785 } 786 onClickCallback()787 private void onClickCallback() { 788 synchronized (mClickableSpanCallbackLock) { 789 mClickableSpanCalled.set(true); 790 mClickableSpanCallbackLock.notifyAll(); 791 } 792 } 793 assertOnClickCalled()794 private void assertOnClickCalled() { 795 synchronized (mClickableSpanCallbackLock) { 796 long endTime = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS; 797 while (!mClickableSpanCalled.get() && (System.currentTimeMillis() < endTime)) { 798 try { 799 mClickableSpanCallbackLock.wait(endTime - System.currentTimeMillis()); 800 } catch (InterruptedException e) {} 801 } 802 } 803 assert(mClickableSpanCalled.get()); 804 } 805 findSingleSpanInViewWithText(int stringId, Class<T> type)806 private <T> T findSingleSpanInViewWithText(int stringId, Class<T> type) { 807 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 808 .findAccessibilityNodeInfosByText(mActivity.getString(stringId)).get(0); 809 CharSequence accessibilityTextWithSpan = text.getText(); 810 // The span should work even with the node recycled 811 text.recycle(); 812 assertTrue(accessibilityTextWithSpan instanceof Spanned); 813 814 T spans[] = ((Spanned) accessibilityTextWithSpan) 815 .getSpans(0, accessibilityTextWithSpan.length(), type); 816 assertEquals(1, spans.length); 817 return spans[0]; 818 } 819 makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text)820 private void makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text) { 821 sInstrumentation.runOnMainSync(() -> { 822 textView.setVisibility(View.VISIBLE); 823 textView.setText(text); 824 }); 825 sInstrumentation.waitForIdleSync(); 826 } 827 waitForExtraTextData(AccessibilityNodeInfo info, String key)828 private Bundle waitForExtraTextData(AccessibilityNodeInfo info, String key) { 829 return waitForExtraTextData(info, key, info.getText().length()); 830 } 831 waitForExtraTextData(AccessibilityNodeInfo info, String key, int length)832 private Bundle waitForExtraTextData(AccessibilityNodeInfo info, String key, int length) { 833 final Bundle getTextArgs = getTextLocationArguments(length); 834 // Node refresh must succeed and the resulting extras must contain the requested key. 835 try { 836 TestUtils.waitUntil("Timed out waiting for extra data", () -> { 837 info.refreshWithExtraData(key, getTextArgs); 838 return info.getExtras().containsKey(key); 839 }); 840 } catch (Exception e) { 841 fail(e.getMessage()); 842 } 843 844 return info.getExtras(); 845 } 846 waitForExtraRenderingInfo( AccessibilityNodeInfo info)847 private AccessibilityNodeInfo.ExtraRenderingInfo waitForExtraRenderingInfo( 848 AccessibilityNodeInfo info) { 849 // Node refresh must succeed and extraRenderingInfo must not be null. 850 try { 851 TestUtils.waitUntil("Timed out waiting for extra rendering data", () -> { 852 info.refreshWithExtraData( 853 EXTRA_DATA_RENDERING_INFO_KEY, new Bundle()); 854 return info.getExtraRenderingInfo() != null; 855 }); 856 } catch (Exception e) { 857 fail(e.getMessage()); 858 } 859 860 return info.getExtraRenderingInfo(); 861 } 862 findNodeByText(String text)863 private AccessibilityNodeInfo findNodeByText(String text) { 864 AccessibilityServiceInfo serviceInfo = sUiAutomation.getServiceInfo(); 865 serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 866 sUiAutomation.setServiceInfo(serviceInfo); 867 868 for (int attempts = 0; attempts < 5; attempts++) { 869 // Find the AccessibilityNodeInfo within a window with the text. 870 List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 871 int numWindows = windows.size(); 872 873 for (int i = 0; i < numWindows; i++) { 874 AccessibilityWindowInfo window = windows.get(i); 875 AccessibilityNodeInfo root = window.getRoot(); 876 if (root == null) { 877 continue; 878 } 879 List<AccessibilityNodeInfo> infos = root.findAccessibilityNodeInfosByText(text); 880 if (!infos.isEmpty()) { 881 return infos.getFirst(); 882 } 883 } 884 // Wait for the system to settle. 885 SystemClock.sleep(1000); 886 } 887 fail("Unable to find AccessibilityNodeInfo with text " + text); 888 return null; 889 } 890 } 891