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