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