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