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 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 textView.setImeActionLabel("pinyin", EditorInfo.IME_ACTION_DONE); 485 text.refresh(); 486 verifyImeActionLabel(text, "pinyin"); 487 text.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER.getId()); 488 verify(mockOnEditorActionListener, times(1)).onEditorAction( 489 textView, EditorInfo.IME_ACTION_DONE, null); 490 } 491 492 @Test testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested()493 public void testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested() { 494 final Bundle arg = new Bundle(); 495 final DisplayMetrics displayMetrics = mActivity.getResources().getDisplayMetrics(); 496 final TextView textView = mActivity.findViewById(R.id.text); 497 final String stringToSet = mActivity.getString(R.string.foo_bar_baz); 498 final int expectedWidthInPx = textView.getLayoutParams().width; 499 final int expectedHeightInPx = textView.getLayoutParams().height; 500 final float expectedTextSize = textView.getTextSize(); 501 final float newTextSize = 20f; 502 final float expectedNewTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 503 newTextSize, displayMetrics); 504 makeTextViewVisibleAndSetText(textView, stringToSet); 505 506 final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow() 507 .findAccessibilityNodeInfosByText(stringToSet).get(0); 508 assertTrue("Text view should offer extra data to accessibility ", 509 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY)); 510 511 AccessibilityNodeInfo.ExtraRenderingInfo extraRenderingInfo; 512 assertNull(info.getExtraRenderingInfo()); 513 assertTrue("Refresh failed", info.refreshWithExtraData( 514 EXTRA_DATA_RENDERING_INFO_KEY , arg)); 515 assertNotNull(info.getExtraRenderingInfo()); 516 extraRenderingInfo = info.getExtraRenderingInfo(); 517 assertNotNull(extraRenderingInfo.getLayoutSize()); 518 assertEquals(expectedWidthInPx, extraRenderingInfo.getLayoutSize().getWidth()); 519 assertEquals(expectedHeightInPx, extraRenderingInfo.getLayoutSize().getHeight()); 520 assertEquals(expectedTextSize, extraRenderingInfo.getTextSizeInPx(), 0f); 521 assertEquals(TypedValue.COMPLEX_UNIT_DIP, extraRenderingInfo.getTextSizeUnit()); 522 523 // After changing text size 524 sInstrumentation.runOnMainSync(() -> 525 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, newTextSize)); 526 assertTrue("Refresh failed", info.refreshWithExtraData( 527 EXTRA_DATA_RENDERING_INFO_KEY, arg)); 528 extraRenderingInfo = info.getExtraRenderingInfo(); 529 assertEquals(expectedNewTextSize, extraRenderingInfo.getTextSizeInPx(), 0f); 530 assertEquals(TypedValue.COMPLEX_UNIT_SP, extraRenderingInfo.getTextSizeUnit()); 531 } 532 533 @Test testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested()534 public void testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested() { 535 final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow() 536 .findAccessibilityNodeInfosByViewId( 537 "android.accessibilityservice.cts:id/viewGroup").get(0); 538 539 assertTrue("ViewGroup should offer extra data to accessibility", 540 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY)); 541 assertNull(info.getExtraRenderingInfo()); 542 assertTrue("Refresh failed", info.refreshWithExtraData( 543 EXTRA_DATA_RENDERING_INFO_KEY, new Bundle())); 544 assertNotNull(info.getExtraRenderingInfo()); 545 assertNotNull(info.getExtraRenderingInfo().getLayoutSize()); 546 final Size size = info.getExtraRenderingInfo().getLayoutSize(); 547 assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, size.getWidth()); 548 assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, size.getHeight()); 549 } 550 verifyImeActionLabel(AccessibilityNodeInfo node, String label)551 private void verifyImeActionLabel(AccessibilityNodeInfo node, String label) { 552 final List<AccessibilityNodeInfo.AccessibilityAction> actionList = node.getActionList(); 553 final int indexOfActionImeEnter = 554 actionList.indexOf(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER); 555 assertTrue(indexOfActionImeEnter >= 0); 556 557 final AccessibilityNodeInfo.AccessibilityAction action = 558 actionList.get(indexOfActionImeEnter); 559 assertEquals(action.getLabel().toString(), label); 560 } 561 getTextLocationArguments(int locationLength)562 private Bundle getTextLocationArguments(int locationLength) { 563 Bundle args = new Bundle(); 564 args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, 0); 565 args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, locationLength); 566 return args; 567 } 568 assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info)569 private void assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info) { 570 final Parcelable[] parcelables = info.getExtras() 571 .getParcelableArray(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY); 572 final RectF[] locations = Arrays.copyOf(parcelables, parcelables.length, RectF[].class); 573 assertEquals(info.getText().length(), locations.length); 574 // The text should all be on one line, running left to right 575 for (int i = 0; i < locations.length; i++) { 576 assertEquals(locations[0].top, locations[i].top, 0.01); 577 assertEquals(locations[0].bottom, locations[i].bottom, 0.01); 578 assertTrue(locations[i].right > locations[i].left); 579 if (i > 0) { 580 assertTrue(locations[i].left > locations[i-1].left); 581 } 582 } 583 } 584 onClickCallback()585 private void onClickCallback() { 586 synchronized (mClickableSpanCallbackLock) { 587 mClickableSpanCalled.set(true); 588 mClickableSpanCallbackLock.notifyAll(); 589 } 590 } 591 assertOnClickCalled()592 private void assertOnClickCalled() { 593 synchronized (mClickableSpanCallbackLock) { 594 long endTime = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS; 595 while (!mClickableSpanCalled.get() && (System.currentTimeMillis() < endTime)) { 596 try { 597 mClickableSpanCallbackLock.wait(endTime - System.currentTimeMillis()); 598 } catch (InterruptedException e) {} 599 } 600 } 601 assert(mClickableSpanCalled.get()); 602 } 603 findSingleSpanInViewWithText(int stringId, Class<T> type)604 private <T> T findSingleSpanInViewWithText(int stringId, Class<T> type) { 605 final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow() 606 .findAccessibilityNodeInfosByText(mActivity.getString(stringId)).get(0); 607 CharSequence accessibilityTextWithSpan = text.getText(); 608 // The span should work even with the node recycled 609 text.recycle(); 610 assertTrue(accessibilityTextWithSpan instanceof Spanned); 611 612 T spans[] = ((Spanned) accessibilityTextWithSpan) 613 .getSpans(0, accessibilityTextWithSpan.length(), type); 614 assertEquals(1, spans.length); 615 return spans[0]; 616 } 617 makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text)618 private void makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text) { 619 sInstrumentation.runOnMainSync(() -> { 620 textView.setVisibility(View.VISIBLE); 621 textView.setText(text); 622 }); 623 } 624 } 625