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