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