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