• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.homeScreenOrBust;
18 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
21 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_RENDERING_INFO_KEY;
22 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH;
23 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
24 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY;
25 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 import static com.google.common.truth.Truth.assertWithMessage;
29 
30 import static org.junit.Assert.assertEquals;
31 import static org.junit.Assert.assertFalse;
32 import static org.junit.Assert.assertNotNull;
33 import static org.junit.Assert.assertNull;
34 import static org.junit.Assert.assertTrue;
35 import static org.junit.Assert.fail;
36 import static org.junit.Assume.assumeTrue;
37 import static org.mockito.Mockito.mock;
38 import static org.mockito.Mockito.timeout;
39 import static org.mockito.Mockito.times;
40 import static org.mockito.Mockito.verify;
41 import static org.mockito.Mockito.verifyNoMoreInteractions;
42 
43 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
44 import android.accessibilityservice.AccessibilityServiceInfo;
45 import android.accessibilityservice.cts.activities.AccessibilityTextTraversalActivity;
46 import android.accessibilityservice.cts.activities.AccessibilityTextViewActivity;
47 import android.app.ActivityOptions;
48 import android.app.Instrumentation;
49 import android.app.UiAutomation;
50 import android.graphics.Bitmap;
51 import android.graphics.Rect;
52 import android.graphics.RectF;
53 import android.os.Bundle;
54 import android.os.Message;
55 import android.os.Parcelable;
56 import android.os.SystemClock;
57 import android.platform.test.annotations.Presubmit;
58 import android.platform.test.annotations.RequiresFlagsEnabled;
59 import android.platform.test.flag.junit.CheckFlagsRule;
60 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
61 import android.text.SpannableString;
62 import android.text.Spanned;
63 import android.text.TextUtils;
64 import android.text.style.ClickableSpan;
65 import android.text.style.ImageSpan;
66 import android.text.style.ReplacementSpan;
67 import android.text.style.URLSpan;
68 import android.util.DisplayMetrics;
69 import android.util.Size;
70 import android.util.TypedValue;
71 import android.view.Display;
72 import android.view.View;
73 import android.view.ViewGroup;
74 import android.view.accessibility.AccessibilityManager;
75 import android.view.accessibility.AccessibilityNodeInfo;
76 import android.view.accessibility.AccessibilityNodeProvider;
77 import android.view.accessibility.AccessibilityRequestPreparer;
78 import android.view.accessibility.AccessibilityWindowInfo;
79 import android.view.inputmethod.EditorInfo;
80 import android.widget.EditText;
81 import android.widget.TextView;
82 
83 import androidx.lifecycle.Lifecycle;
84 import androidx.test.core.app.ActivityScenario;
85 import androidx.test.ext.junit.rules.ActivityScenarioRule;
86 import androidx.test.ext.junit.runners.AndroidJUnit4;
87 import androidx.test.filters.FlakyTest;
88 import androidx.test.platform.app.InstrumentationRegistry;
89 
90 import com.android.compatibility.common.util.CddTest;
91 import com.android.compatibility.common.util.TestUtils;
92 
93 import org.junit.AfterClass;
94 import org.junit.Before;
95 import org.junit.BeforeClass;
96 import org.junit.Rule;
97 import org.junit.Test;
98 import org.junit.rules.RuleChain;
99 import org.junit.runner.RunWith;
100 
101 import java.util.List;
102 import java.util.concurrent.atomic.AtomicBoolean;
103 import java.util.concurrent.atomic.AtomicReference;
104 
105 /**
106  * Test cases for actions taken on text views.
107  */
108 @RunWith(AndroidJUnit4.class)
109 @CddTest(requirements = {"3.10/C-1-1,C-1-2"})
110 @Presubmit
111 public class AccessibilityTextActionTest {
112     private static Instrumentation sInstrumentation;
113     private static UiAutomation sUiAutomation;
114     final Object mClickableSpanCallbackLock = new Object();
115     final AtomicBoolean mClickableSpanCalled = new AtomicBoolean(false);
116 
117     @Rule
118     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
119 
120     private AccessibilityTextTraversalActivity mActivity;
121 
122     private ActivityScenarioRule<AccessibilityTextTraversalActivity> mActivityRule =
123             new ActivityScenarioRule<>(AccessibilityTextTraversalActivity.class);
124 
125     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
126             new AccessibilityDumpOnFailureRule();
127 
128     @Rule
129     public final RuleChain mRuleChain = RuleChain
130             .outerRule(mActivityRule)
131             .around(mDumpOnFailureRule);
132 
133     @BeforeClass
oneTimeSetup()134     public static void oneTimeSetup() throws Exception {
135         sInstrumentation = InstrumentationRegistry.getInstrumentation();
136         sUiAutomation = sInstrumentation.getUiAutomation();
137     }
138 
139     @Before
setUp()140     public void setUp() throws Exception {
141         mActivityRule
142                 .getScenario()
143                 .moveToState(Lifecycle.State.RESUMED)
144                 .onActivity(activity -> mActivity = activity);
145         mClickableSpanCalled.set(false);
146     }
147 
148     @AfterClass
postTestTearDown()149     public static void postTestTearDown() {
150         sUiAutomation.destroy();
151     }
152 
153     @Test
testNotEditableTextView_shouldNotExposeOrRespondToSetTextAction()154     public void testNotEditableTextView_shouldNotExposeOrRespondToSetTextAction() {
155         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
156         makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b));
157 
158         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
159                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
160 
161         assertFalse("Standard text view should not support SET_TEXT", text.getActionList()
162                 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT));
163         assertEquals("Standard text view should not support SET_TEXT", 0,
164                 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT);
165         Bundle args = new Bundle();
166         args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
167                 mActivity.getString(R.string.text_input_blah));
168         assertFalse(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args));
169 
170         sInstrumentation.waitForIdleSync();
171         assertTrue("Text view should not update on failed set text",
172                 TextUtils.equals(mActivity.getString(R.string.a_b), textView.getText()));
173     }
174 
175     @Test
testEditableTextView_shouldExposeAndRespondToSetTextAction()176     public void testEditableTextView_shouldExposeAndRespondToSetTextAction() {
177         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
178 
179         sInstrumentation.runOnMainSync(new Runnable() {
180             @Override
181             public void run() {
182                 textView.setVisibility(View.VISIBLE);
183                 textView.setText(mActivity.getString(R.string.a_b), TextView.BufferType.EDITABLE);
184             }
185         });
186 
187         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
188                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
189 
190         assertTrue("Editable text view should support SET_TEXT", text.getActionList()
191                 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT));
192         assertEquals("Editable text 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("Editable text should update on set text",
205                 TextUtils.equals(textToSet, textView.getText()));
206     }
207 
208     @Test
testEditText_shouldExposeAndRespondToSetTextAction()209     public void testEditText_shouldExposeAndRespondToSetTextAction() {
210         final EditText editText = (EditText) mActivity.findViewById(R.id.edit);
211         makeTextViewVisibleAndSetText(editText, mActivity.getString(R.string.a_b));
212 
213         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
214                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
215 
216         assertTrue("EditText should support SET_TEXT", text.getActionList()
217                 .contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT));
218         assertEquals("EditText view should support SET_TEXT",
219                 AccessibilityNodeInfo.ACTION_SET_TEXT,
220                 text.getActions() & AccessibilityNodeInfo.ACTION_SET_TEXT);
221 
222         Bundle args = new Bundle();
223         String textToSet = mActivity.getString(R.string.text_input_blah);
224         args.putCharSequence(
225                 AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, textToSet);
226 
227         assertTrue(text.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args));
228 
229         sInstrumentation.waitForIdleSync();
230         assertTrue("EditText should update on set text",
231                 TextUtils.equals(textToSet, editText.getText()));
232     }
233 
234     @Test
testClickableSpan_shouldWorkFromAccessibilityService()235     public void testClickableSpan_shouldWorkFromAccessibilityService() {
236         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
237         final ClickableSpan clickableSpan = new ClickableSpan() {
238             @Override
239             public void onClick(View widget) {
240                 assertEquals("Clickable span called back on wrong View", textView, widget);
241                 onClickCallback();
242             }
243         };
244         final SpannableString textWithClickableSpan =
245                 new SpannableString(mActivity.getString(R.string.a_b));
246         textWithClickableSpan.setSpan(clickableSpan, 0, 1, 0);
247         makeTextViewVisibleAndSetText(textView, textWithClickableSpan);
248 
249         ClickableSpan clickableSpanFromA11y
250                 = findSingleSpanInViewWithText(R.string.a_b, ClickableSpan.class);
251         clickableSpanFromA11y.onClick(null);
252         assertOnClickCalled();
253     }
254 
255     @Test
testUrlSpan_shouldWorkFromAccessibilityService()256     public void testUrlSpan_shouldWorkFromAccessibilityService() {
257         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
258         final String url = "com.android.some.random.url";
259         final URLSpan urlSpan = new URLSpan(url) {
260             @Override
261             public void onClick(View widget) {
262                 assertEquals("Url span called back on wrong View", textView, widget);
263                 onClickCallback();
264             }
265         };
266         final SpannableString textWithClickableSpan =
267                 new SpannableString(mActivity.getString(R.string.a_b));
268         textWithClickableSpan.setSpan(urlSpan, 0, 1, 0);
269         makeTextViewVisibleAndSetText(textView, textWithClickableSpan);
270 
271         URLSpan urlSpanFromA11y = findSingleSpanInViewWithText(R.string.a_b, URLSpan.class);
272         assertEquals(url, urlSpanFromA11y.getURL());
273         urlSpanFromA11y.onClick(null);
274 
275         assertOnClickCalled();
276     }
277 
278     @Test
testImageSpan_accessibilityServiceShouldSeeContentDescription()279     public void testImageSpan_accessibilityServiceShouldSeeContentDescription() {
280         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
281         final Bitmap bitmap = Bitmap.createBitmap(/* width= */10, /* height= */10,
282                 Bitmap.Config.ARGB_8888);
283         final ImageSpan imageSpan = new ImageSpan(mActivity, bitmap);
284         final String contentDescription = mActivity.getString(R.string.contentDescription);
285         imageSpan.setContentDescription(contentDescription);
286         final SpannableString textWithImageSpan =
287                 new SpannableString(mActivity.getString(R.string.a_b));
288         textWithImageSpan.setSpan(imageSpan, /* start= */0, /* end= */1, /* flags= */0);
289         makeTextViewVisibleAndSetText(textView, textWithImageSpan);
290 
291         ReplacementSpan replacementSpanFromA11y = findSingleSpanInViewWithText(R.string.a_b,
292                 ReplacementSpan.class);
293 
294         assertEquals(contentDescription, replacementSpanFromA11y.getContentDescription());
295     }
296 
297     @Test
testTextLocations_textViewShouldProvideWhenRequested()298     public void testTextLocations_textViewShouldProvideWhenRequested() {
299         testTextViewProvidesLocationsWhenRequested(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
300     }
301 
302     @Test
303     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API)
testTextLocations_textViewShouldProvideWhenRequestedInWindow()304     public void testTextLocations_textViewShouldProvideWhenRequestedInWindow() {
305         testTextViewProvidesLocationsWhenRequested(
306                 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY);
307     }
308 
testTextViewProvidesLocationsWhenRequested(String extraDataKey)309     private void testTextViewProvidesLocationsWhenRequested(String extraDataKey) {
310         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
311         // Use text with a strong s, since that gets replaced with a double s for all caps.
312         // That replacement requires us to properly handle the length of the string changing.
313         String stringToSet = mActivity.getString(R.string.german_text_with_strong_s);
314         makeTextViewVisibleAndSetText(textView, stringToSet);
315         sInstrumentation.runOnMainSync(() -> textView.setAllCaps(true));
316 
317         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
318                 .findAccessibilityNodeInfosByText(stringToSet).get(0);
319         List<String> textAvailableExtraData = text.getAvailableExtraData();
320         assertTrue("Text view should offer text location to accessibility",
321                 textAvailableExtraData.contains(extraDataKey));
322         assertNull("Text locations should not be populated by default",
323                 text.getExtras().getString(extraDataKey));
324 
325         waitForExtraTextData(text, extraDataKey);
326         assertNodeContainsTextLocationInfoOnOneLineLTR(text, extraDataKey);
327     }
328 
329     @Test
330     @FlakyTest
testTextLocations_textOutsideOfViewBounds_locationsShouldBeNull()331     public void testTextLocations_textOutsideOfViewBounds_locationsShouldBeNull() {
332         testTextOutsideOfViewBounds_locationsInWindowsNull(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
333     }
334 
335     @Test
336     @FlakyTest
337     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API)
testTextLocations_textOutsideOfViewBounds_locationsInWindowShouldBeNull()338     public void testTextLocations_textOutsideOfViewBounds_locationsInWindowShouldBeNull() {
339         testTextOutsideOfViewBounds_locationsInWindowsNull(
340                 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY);
341     }
342 
testTextOutsideOfViewBounds_locationsInWindowsNull(String extraDataKey)343     private void testTextOutsideOfViewBounds_locationsInWindowsNull(String extraDataKey) {
344         final EditText editText = mActivity.findViewById(R.id.edit);
345         makeTextViewVisibleAndSetText(editText, mActivity.getString(R.string.android_wiki));
346 
347         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
348                 .findAccessibilityNodeInfosByText(
349                         mActivity.getString(R.string.android_wiki)).get(0);
350         List<String> textAvailableExtraData = text.getAvailableExtraData();
351         assertTrue("Text view should offer text location to accessibility",
352                 textAvailableExtraData.contains(extraDataKey));
353 
354         Bundle extras = waitForExtraTextData(text, extraDataKey);
355         Parcelable[] parcelables = extras.getParcelableArray(
356                 extraDataKey, RectF.class);
357         assertNotNull(parcelables);
358         final RectF[] locationsBeforeScroll = (RectF[]) parcelables;
359         assertEquals(text.getText().length(), locationsBeforeScroll.length);
360         // The first character should be visible immediately.
361         assertFalse(locationsBeforeScroll[0].isEmpty());
362         // Some of the characters should be off the screen, and thus have empty rects. Find the
363         // break point.
364         int firstNullRectIndex = -1;
365         for (int i = 1; i < locationsBeforeScroll.length; i++) {
366             boolean isNull = locationsBeforeScroll[i] == null;
367             if (firstNullRectIndex < 0) {
368                 if (isNull) {
369                     firstNullRectIndex = i;
370                 }
371             } else {
372                 assertTrue(isNull);
373             }
374         }
375 
376         // Scroll down one line.
377         sInstrumentation.runOnMainSync(
378                 () -> {
379                     // Calculate the height of a line from the relative character heights.
380                     int firstLineBottom = (int) locationsBeforeScroll[0].bottom;
381                     int i = 1;
382                     while ((int) locationsBeforeScroll[i].bottom == firstLineBottom) {
383                         i++;
384                     }
385                     final int oneLineDownY =
386                             (int) locationsBeforeScroll[i].bottom - firstLineBottom;
387                     editText.scrollTo(0, oneLineDownY + 1);
388                 });
389 
390         extras = waitForExtraTextData(text, extraDataKey);
391         parcelables = extras
392                 .getParcelableArray(extraDataKey, RectF.class);
393         assertNotNull(parcelables);
394         final RectF[] locationsAfterScroll = (RectF[]) parcelables;
395         // Now the first character should be off the screen.
396         assertNull(locationsAfterScroll[0]);
397         // The first character that was off the screen should now be on it.
398         assertNotNull(locationsAfterScroll[firstNullRectIndex]);
399     }
400 
401     @Test
testTextLocations_withRequestPreparer_shouldHoldOffUntilReady()402     public void testTextLocations_withRequestPreparer_shouldHoldOffUntilReady() {
403         testTextLocations_withRequestPreparer_shouldHoldOffUntilReady(
404                 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
405     }
406 
407     @Test
408     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API)
testTextLocationsInWindow_withRequestPreparer_shouldHoldOffUntilReady()409     public void testTextLocationsInWindow_withRequestPreparer_shouldHoldOffUntilReady() {
410         testTextLocations_withRequestPreparer_shouldHoldOffUntilReady(
411                 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY);
412     }
413 
testTextLocations_withRequestPreparer_shouldHoldOffUntilReady( String extraDataKey)414     private void testTextLocations_withRequestPreparer_shouldHoldOffUntilReady(
415             String extraDataKey) {
416         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
417         makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b));
418 
419         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
420                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
421         final List<String> textAvailableExtraData = text.getAvailableExtraData();
422         final Bundle getTextArgs = getTextLocationArguments(text.getText().length());
423 
424         // Register a request preparer that will capture the message indicating that preparation
425         // is complete
426         final AtomicReference<Message> messageRefForPrepare = new AtomicReference<>(null);
427         // Use mockito's asynchronous signaling
428         Runnable mockRunnableForPrepare = mock(Runnable.class);
429 
430         AccessibilityManager a11yManager =
431                 mActivity.getSystemService(AccessibilityManager.class);
432         assertNotNull(a11yManager);
433         AccessibilityRequestPreparer requestPreparer = new AccessibilityRequestPreparer(
434                 textView, AccessibilityRequestPreparer.REQUEST_TYPE_EXTRA_DATA) {
435             @Override
436             public void onPrepareExtraData(int virtualViewId,
437                     String preparedExtraDataKey, Bundle args, Message preparationFinishedMessage) {
438                 assertEquals(AccessibilityNodeProvider.HOST_VIEW_ID, virtualViewId);
439                 assertEquals(extraDataKey, preparedExtraDataKey);
440                 assertEquals(0, args.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX));
441                 assertEquals(text.getText().length(),
442                         args.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH));
443                 messageRefForPrepare.set(preparationFinishedMessage);
444                 mockRunnableForPrepare.run();
445             }
446         };
447         a11yManager.addAccessibilityRequestPreparer(requestPreparer);
448         verify(mockRunnableForPrepare, times(0)).run();
449 
450         // Make the extra data request in another thread
451         Runnable mockRunnableForData = mock(Runnable.class);
452         new Thread(()-> {
453             waitForExtraTextData(text, extraDataKey);
454             mockRunnableForData.run();
455         }).start();
456 
457         // The extra data request should trigger the request preparer
458         verify(mockRunnableForPrepare, timeout(DEFAULT_TIMEOUT_MS)).run();
459         // Verify that the request for extra data didn't return. This is a bit racy, as we may still
460         // not catch it if it does return prematurely, but it does provide some protection.
461         sInstrumentation.waitForIdleSync();
462         verify(mockRunnableForData, times(0)).run();
463 
464         // Declare preparation for the request complete, and verify that it runs to completion
465         messageRefForPrepare.get().sendToTarget();
466         verify(mockRunnableForData, timeout(DEFAULT_TIMEOUT_MS)).run();
467         assertNodeContainsTextLocationInfoOnOneLineLTR(text, extraDataKey);
468         a11yManager.removeAccessibilityRequestPreparer(requestPreparer);
469     }
470 
471     @Test
472     @FlakyTest
testTextLocations_withUnresponsiveRequestPreparer_shouldTimeout()473     public void testTextLocations_withUnresponsiveRequestPreparer_shouldTimeout() {
474         final TextView textView = (TextView) mActivity.findViewById(R.id.text);
475         makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b));
476 
477         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
478                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
479         final List<String> textAvailableExtraData = text.getAvailableExtraData();
480         final Bundle getTextArgs = getTextLocationArguments(text.getText().length());
481 
482         // Use mockito's asynchronous signaling
483         Runnable mockRunnableForPrepare = mock(Runnable.class);
484 
485         AccessibilityManager a11yManager =
486                 mActivity.getSystemService(AccessibilityManager.class);
487         AccessibilityRequestPreparer requestPreparer = new AccessibilityRequestPreparer(
488                 textView, AccessibilityRequestPreparer.REQUEST_TYPE_EXTRA_DATA) {
489             @Override
490             public void onPrepareExtraData(int virtualViewId,
491                     String extraDataKey, Bundle args, Message preparationFinishedMessage) {
492                 mockRunnableForPrepare.run();
493             }
494         };
495         a11yManager.addAccessibilityRequestPreparer(requestPreparer);
496         verify(mockRunnableForPrepare, times(0)).run();
497 
498         // Make the extra data request in another thread
499         Runnable mockRunnableForData = mock(Runnable.class);
500         new Thread(() -> {
501             /*
502              * Don't worry about the return value, as we're timing out. We're just making
503              * sure that we don't hang the system.
504              */
505             waitForExtraTextData(text, EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
506             mockRunnableForData.run();
507         }).start();
508 
509         // The extra data request should trigger the request preparer
510         verify(mockRunnableForPrepare, timeout(DEFAULT_TIMEOUT_MS)).run();
511 
512         // Declare preparation for the request complete, and verify that it runs to completion
513         verify(mockRunnableForData, timeout(DEFAULT_TIMEOUT_MS)).run();
514         a11yManager.removeAccessibilityRequestPreparer(requestPreparer);
515     }
516 
517     @Test
518     @FlakyTest
testTextLocation_testLocationBoundary_locationShouldBeLimitationLength()519     public void testTextLocation_testLocationBoundary_locationShouldBeLimitationLength() {
520         textTextLocationBoundaryShouldBeLimitedLength(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
521     }
522 
523     @Test
524     @FlakyTest
525     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API)
testTextLocation_testLocationBoundary_locationInWindowShouldBeLimitationLength()526     public void testTextLocation_testLocationBoundary_locationInWindowShouldBeLimitationLength() {
527         textTextLocationBoundaryShouldBeLimitedLength(
528                 EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY);
529     }
530 
textTextLocationBoundaryShouldBeLimitedLength(String extraDataKey)531     private void textTextLocationBoundaryShouldBeLimitedLength(String extraDataKey) {
532         final TextView textView = mActivity.findViewById(R.id.text);
533         makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b));
534 
535         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
536                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
537 
538         Bundle extras = waitForExtraTextData(text, extraDataKey, Integer.MAX_VALUE);
539 
540         final Parcelable[] parcelables = extras.getParcelableArray(extraDataKey, RectF.class);
541         assertNotNull(parcelables);
542         final RectF[] locations = (RectF[]) parcelables;
543         assertEquals(locations.length,
544                 AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH);
545     }
546 
547     @Test
testTextLocations_inFreeform_screenCoordinates()548     public void testTextLocations_inFreeform_screenCoordinates() throws Exception {
549         final int top = 100;
550         final int left = 200;
551         try (ActivityScenario<AccessibilityTextViewActivity> scenario =
552                 launchTextViewActivityInFreeform(left, top)) {
553             scenario.onActivity(
554                     textViewActivity -> {
555                         // Waits for the node to be on-screen.
556                         final AccessibilityNodeInfo info =
557                                 findNodeByText(textViewActivity.getString(R.string.foo_bar_baz));
558                         Bundle extras =
559                                 waitForExtraTextData(info, EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
560                         final Parcelable[] parcelables =
561                                 extras.getParcelableArray(
562                                         EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, RectF.class);
563                         assertThat(parcelables).isNotNull();
564                         final RectF[] charLocations = (RectF[]) parcelables;
565                         assertThat(charLocations).hasLength(info.getText().length());
566 
567                         Rect windowBounds = new Rect();
568                         info.getWindow().getBoundsInScreen(windowBounds);
569                         assertThat(windowBounds.left).isWithin(1).of(left);
570                         assertThat(windowBounds.top).isWithin(1).of(top);
571 
572                         Rect nodeBoundsInScreen = new Rect();
573                         info.getBoundsInScreen(nodeBoundsInScreen);
574 
575                         for (RectF location : charLocations) {
576                             // The character locations are within the window's location
577                             // when both are represented in screen coordinates.
578                             assertWithMessage(
579                                             "windowBounds %s contains character location %s",
580                                             windowBounds, location)
581                                     .that(new RectF(windowBounds).contains(location))
582                                     .isTrue();
583 
584                             // Double-check that the screen coordinates of the character are within
585                             // the screen coordinates of the node.
586                             assertWithMessage(
587                                             "nodeBoundsInScreen %s contains character location %s",
588                                             nodeBoundsInScreen, location)
589                                     .that(new RectF(nodeBoundsInScreen).contains(location))
590                                     .isTrue();
591                         }
592                     });
593         }
594     }
595 
596     @Test
597     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_CHARACTER_IN_WINDOW_API)
testTextLocations_inFreeform_windowCoordinates()598     public void testTextLocations_inFreeform_windowCoordinates() throws Exception {
599         final int top = 100;
600         final int left = 200;
601         try (ActivityScenario<AccessibilityTextViewActivity> scenario =
602                 launchTextViewActivityInFreeform(left, top)) {
603             scenario.onActivity(
604                     textViewActivity -> {
605                         // Waits for the node to be on-screen.
606                         final AccessibilityNodeInfo info =
607                                 findNodeByText(textViewActivity.getString(R.string.foo_bar_baz));
608                         Bundle extras =
609                                 waitForExtraTextData(
610                                         info, EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY);
611                         final Parcelable[] parcelables =
612                                 extras.getParcelableArray(
613                                         EXTRA_DATA_TEXT_CHARACTER_LOCATION_IN_WINDOW_KEY,
614                                         RectF.class);
615                         assertThat(parcelables).isNotNull();
616                         final RectF[] charLocations = (RectF[]) parcelables;
617                         assertThat(charLocations).hasLength(info.getText().length());
618 
619                         // Check the window is in the right part of the screen.
620                         Rect windowBounds = new Rect();
621                         info.getWindow().getBoundsInScreen(windowBounds);
622                         assertThat(windowBounds.left).isWithin(1).of(left);
623                         assertThat(windowBounds.top).isWithin(1).of(top);
624 
625                         // The first primary location should be at the left edge of the window.
626                         assertThat(charLocations[0].left).isLessThan(1);
627 
628                         Rect nodeBoundsInWindow = new Rect();
629                         info.getBoundsInWindow(nodeBoundsInWindow);
630 
631                         for (RectF location : charLocations) {
632                             // Check that the window coordinates of the character are within the
633                             // window coordinates of the node.
634                             assertWithMessage(
635                                             "nodeBoundsInWindow %s contains character location %s",
636                                             nodeBoundsInWindow, location)
637                                     .that(new RectF(nodeBoundsInWindow).contains(location))
638                                     .isTrue();
639                         }
640                     });
641         }
642     }
643 
launchTextViewActivityInFreeform( int left, int top)644     private ActivityScenario<AccessibilityTextViewActivity> launchTextViewActivityInFreeform(
645             int left, int top) {
646         assumeTrue(
647                 sInstrumentation
648                         .getContext()
649                         .getPackageManager()
650                         .hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT));
651         homeScreenOrBust(sInstrumentation.getContext(), sUiAutomation);
652         mActivityRule.getScenario().close();
653 
654         final ActivityOptions options = ActivityOptions.makeBasic();
655         options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
656         options.setLaunchBounds(new Rect(left, top, left + 400, top + 400));
657         options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
658         return ActivityScenario.launch(AccessibilityTextViewActivity.class, options.toBundle());
659     }
660 
661     @Test
testEditableTextView_shouldExposeAndRespondToImeEnterAction()662     public void testEditableTextView_shouldExposeAndRespondToImeEnterAction() throws Throwable {
663         final TextView textView = (TextView) mActivity.findViewById(R.id.editText);
664         makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b));
665         sInstrumentation.runOnMainSync(() -> textView.requestFocus());
666         assertTrue(textView.isFocused());
667 
668         final TextView.OnEditorActionListener mockOnEditorActionListener =
669                 mock(TextView.OnEditorActionListener.class);
670         textView.setOnEditorActionListener(mockOnEditorActionListener);
671         verifyNoMoreInteractions(mockOnEditorActionListener);
672 
673         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
674                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
675         verifyImeActionLabel(text, sInstrumentation.getContext().getString(
676                                 R.string.accessibility_action_ime_enter_label));
677         text.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER.getId());
678         verify(mockOnEditorActionListener, times(1)).onEditorAction(
679                 textView, EditorInfo.IME_ACTION_UNSPECIFIED, null);
680 
681         // Testing custom ime action : IME_ACTION_DONE.
682         sInstrumentation.runOnMainSync(() -> textView.requestFocus());
683         textView.setImeActionLabel("pinyin", EditorInfo.IME_ACTION_DONE);
684 
685         final AccessibilityNodeInfo textNode = sUiAutomation.getRootInActiveWindow()
686                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
687         verifyImeActionLabel(textNode, "pinyin");
688         textNode.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER.getId());
689         verify(mockOnEditorActionListener, times(1)).onEditorAction(
690                 textView, EditorInfo.IME_ACTION_DONE, null);
691     }
692 
693     @Test
testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested()694     public void testExtraRendering_textViewShouldProvideExtraDataTextSizeWhenRequested() {
695         final DisplayMetrics displayMetrics = mActivity.getResources().getDisplayMetrics();
696         final TextView textView = mActivity.findViewById(R.id.text);
697         final String stringToSet = mActivity.getString(R.string.foo_bar_baz);
698         final int expectedWidthInPx = textView.getLayoutParams().width;
699         final int expectedHeightInPx = textView.getLayoutParams().height;
700         final float expectedTextSize = textView.getTextSize();
701         final float newTextSize = 20f;
702         final float expectedNewTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
703                 newTextSize, displayMetrics);
704         makeTextViewVisibleAndSetText(textView, stringToSet);
705 
706         final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow()
707                 .findAccessibilityNodeInfosByText(stringToSet).get(0);
708         assertTrue("Text view should offer extra data to accessibility ",
709                 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY));
710 
711         AccessibilityNodeInfo.ExtraRenderingInfo extraRenderingInfo;
712         assertNull(info.getExtraRenderingInfo());
713         extraRenderingInfo = waitForExtraRenderingInfo(info);
714         assertNotNull(extraRenderingInfo);
715         assertNotNull(extraRenderingInfo.getLayoutSize());
716         assertEquals(expectedWidthInPx, extraRenderingInfo.getLayoutSize().getWidth());
717         assertEquals(expectedHeightInPx, extraRenderingInfo.getLayoutSize().getHeight());
718         assertEquals(expectedTextSize, extraRenderingInfo.getTextSizeInPx(), 0f);
719         assertEquals(TypedValue.COMPLEX_UNIT_DIP, extraRenderingInfo.getTextSizeUnit());
720 
721         // After changing text size
722         sInstrumentation.runOnMainSync(() ->
723                 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, newTextSize));
724         extraRenderingInfo = waitForExtraRenderingInfo(info);
725         assertEquals(expectedNewTextSize, extraRenderingInfo.getTextSizeInPx(), 0f);
726         assertEquals(TypedValue.COMPLEX_UNIT_SP, extraRenderingInfo.getTextSizeUnit());
727     }
728 
729     @Test
testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested()730     public void testExtraRendering_viewGroupShouldNotProvideLayoutParamsWhenNotRequested() {
731         final AccessibilityNodeInfo info = sUiAutomation.getRootInActiveWindow()
732                 .findAccessibilityNodeInfosByViewId(
733                         "android.accessibilityservice.cts:id/viewGroup").get(0);
734 
735         assertTrue("ViewGroup should offer extra data to accessibility",
736                 info.getAvailableExtraData().contains(EXTRA_DATA_RENDERING_INFO_KEY));
737         assertNull(info.getExtraRenderingInfo());
738         AccessibilityNodeInfo.ExtraRenderingInfo renderingInfo = waitForExtraRenderingInfo(info);
739         assertNotNull(renderingInfo);
740         assertNotNull(renderingInfo.getLayoutSize());
741         final Size size = renderingInfo.getLayoutSize();
742         assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, size.getWidth());
743         assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, size.getHeight());
744     }
745 
verifyImeActionLabel(AccessibilityNodeInfo node, String label)746     private void verifyImeActionLabel(AccessibilityNodeInfo node, String label) {
747         final List<AccessibilityNodeInfo.AccessibilityAction> actionList = node.getActionList();
748         final int indexOfActionImeEnter =
749                 actionList.indexOf(AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER);
750         assertTrue(indexOfActionImeEnter >= 0);
751 
752         final AccessibilityNodeInfo.AccessibilityAction action =
753                 actionList.get(indexOfActionImeEnter);
754         assertEquals(action.getLabel().toString(), label);
755     }
756 
getTextLocationArguments(int locationLength)757     private Bundle getTextLocationArguments(int locationLength) {
758         Bundle args = new Bundle();
759         args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, 0);
760         args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, locationLength);
761         return args;
762     }
763 
assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info, String extraDataKey)764     private void assertNodeContainsTextLocationInfoOnOneLineLTR(AccessibilityNodeInfo info,
765             String extraDataKey) {
766         Bundle extras = waitForExtraTextData(info, extraDataKey);
767         final Parcelable[] parcelables = extras.getParcelableArray(extraDataKey, RectF.class);
768         assertNotNull(parcelables);
769         final RectF[] locations = (RectF[]) parcelables;
770         assertEquals(info.getText().length(), locations.length);
771         // The text should all be on one line, running left to right
772         for (int i = 0; i < locations.length; i++) {
773             if (i != 0 && locations[i] == null) {
774                 // If we run into an off-screen character after at least one on-screen character
775                 // then stop checking the rest of the character locations.
776                 break;
777             }
778             assertEquals(locations[0].top, locations[i].top, 0.01);
779             assertEquals(locations[0].bottom, locations[i].bottom, 0.01);
780             assertTrue(locations[i].right > locations[i].left);
781             if (i > 0) {
782                 assertTrue(locations[i].left > locations[i-1].left);
783             }
784         }
785     }
786 
onClickCallback()787     private void onClickCallback() {
788         synchronized (mClickableSpanCallbackLock) {
789             mClickableSpanCalled.set(true);
790             mClickableSpanCallbackLock.notifyAll();
791         }
792     }
793 
assertOnClickCalled()794     private void assertOnClickCalled() {
795         synchronized (mClickableSpanCallbackLock) {
796             long endTime = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS;
797             while (!mClickableSpanCalled.get() && (System.currentTimeMillis() < endTime)) {
798                 try {
799                     mClickableSpanCallbackLock.wait(endTime - System.currentTimeMillis());
800                 } catch (InterruptedException e) {}
801             }
802         }
803         assert(mClickableSpanCalled.get());
804     }
805 
findSingleSpanInViewWithText(int stringId, Class<T> type)806     private <T> T findSingleSpanInViewWithText(int stringId, Class<T> type) {
807         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
808                 .findAccessibilityNodeInfosByText(mActivity.getString(stringId)).get(0);
809         CharSequence accessibilityTextWithSpan = text.getText();
810         // The span should work even with the node recycled
811         text.recycle();
812         assertTrue(accessibilityTextWithSpan instanceof Spanned);
813 
814         T spans[] = ((Spanned) accessibilityTextWithSpan)
815                 .getSpans(0, accessibilityTextWithSpan.length(), type);
816         assertEquals(1, spans.length);
817         return spans[0];
818     }
819 
makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text)820     private void makeTextViewVisibleAndSetText(final TextView textView, final CharSequence text) {
821         sInstrumentation.runOnMainSync(() -> {
822             textView.setVisibility(View.VISIBLE);
823             textView.setText(text);
824         });
825         sInstrumentation.waitForIdleSync();
826     }
827 
waitForExtraTextData(AccessibilityNodeInfo info, String key)828     private Bundle waitForExtraTextData(AccessibilityNodeInfo info, String key) {
829         return waitForExtraTextData(info, key, info.getText().length());
830     }
831 
waitForExtraTextData(AccessibilityNodeInfo info, String key, int length)832     private Bundle waitForExtraTextData(AccessibilityNodeInfo info, String key, int length) {
833         final Bundle getTextArgs = getTextLocationArguments(length);
834         // Node refresh must succeed and the resulting extras must contain the requested key.
835         try {
836             TestUtils.waitUntil("Timed out waiting for extra data", () -> {
837                 info.refreshWithExtraData(key, getTextArgs);
838                 return info.getExtras().containsKey(key);
839             });
840         } catch (Exception e) {
841             fail(e.getMessage());
842         }
843 
844         return info.getExtras();
845     }
846 
waitForExtraRenderingInfo( AccessibilityNodeInfo info)847     private AccessibilityNodeInfo.ExtraRenderingInfo waitForExtraRenderingInfo(
848             AccessibilityNodeInfo info) {
849         // Node refresh must succeed and extraRenderingInfo must not be null.
850         try {
851             TestUtils.waitUntil("Timed out waiting for extra rendering data", () -> {
852                 info.refreshWithExtraData(
853                         EXTRA_DATA_RENDERING_INFO_KEY, new Bundle());
854                 return info.getExtraRenderingInfo() != null;
855             });
856         } catch (Exception e) {
857             fail(e.getMessage());
858         }
859 
860         return info.getExtraRenderingInfo();
861     }
862 
findNodeByText(String text)863     private AccessibilityNodeInfo findNodeByText(String text) {
864         AccessibilityServiceInfo serviceInfo = sUiAutomation.getServiceInfo();
865         serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
866         sUiAutomation.setServiceInfo(serviceInfo);
867 
868         for (int attempts = 0; attempts < 5; attempts++) {
869             // Find the AccessibilityNodeInfo within a window with the text.
870             List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
871             int numWindows = windows.size();
872 
873             for (int i = 0; i < numWindows; i++) {
874                 AccessibilityWindowInfo window = windows.get(i);
875                 AccessibilityNodeInfo root = window.getRoot();
876                 if (root == null) {
877                     continue;
878                 }
879                 List<AccessibilityNodeInfo> infos = root.findAccessibilityNodeInfosByText(text);
880                 if (!infos.isEmpty()) {
881                     return infos.getFirst();
882                 }
883             }
884             // Wait for the system to settle.
885             SystemClock.sleep(1000);
886         }
887         fail("Unable to find AccessibilityNodeInfo with text " + text);
888         return null;
889     }
890 }
891