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