• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.accessibilityservice.cts;
18 
19 import static android.Manifest.permission.POST_NOTIFICATIONS;
20 import static android.accessibility.cts.common.InstrumentedAccessibilityService.TIMEOUT_SERVICE_ENABLE;
21 import static android.accessibility.cts.common.InstrumentedAccessibilityService.enableService;
22 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
23 import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN;
24 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventType;
25 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithAction;
26 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithResource;
27 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.findWindowByTitle;
28 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.getActivityTitle;
29 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
30 import static android.accessibilityservice.cts.utils.AsyncUtils.await;
31 import static android.accessibilityservice.cts.utils.CtsTestUtils.DEFAULT_GLOBAL_TIMEOUT_MS;
32 import static android.accessibilityservice.cts.utils.CtsTestUtils.DEFAULT_IDLE_TIMEOUT_MS;
33 import static android.accessibilityservice.cts.utils.CtsTestUtils.isAutomotive;
34 import static android.accessibilityservice.cts.utils.GestureUtils.click;
35 import static android.accessibilityservice.cts.utils.GestureUtils.dispatchGesture;
36 import static android.accessibilityservice.cts.utils.RunOnMainUtils.getOnMain;
37 import static android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES;
38 import static android.view.MotionEvent.ACTION_DOWN;
39 import static android.view.MotionEvent.ACTION_UP;
40 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
41 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
42 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
43 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT;
44 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_HIDE_TOOLTIP;
45 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_IN_DIRECTION;
46 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_TOOLTIP;
47 import static android.view.accessibility.Flags.FLAG_PREVENT_A11Y_NONTOOL_FROM_INJECTING_INTO_SENSITIVE_VIEWS;
48 
49 import static androidx.test.espresso.Espresso.onView;
50 import static androidx.test.espresso.action.ViewActions.scrollTo;
51 import static androidx.test.espresso.matcher.ViewMatchers.withId;
52 
53 import static com.google.common.truth.Truth.assertThat;
54 
55 import static org.junit.Assert.assertEquals;
56 import static org.junit.Assert.assertFalse;
57 import static org.junit.Assert.assertNotNull;
58 import static org.junit.Assert.assertThrows;
59 import static org.junit.Assert.assertTrue;
60 import static org.junit.Assert.fail;
61 import static org.junit.Assume.assumeFalse;
62 import static org.junit.Assume.assumeTrue;
63 import static org.mockito.ArgumentMatchers.any;
64 import static org.mockito.ArgumentMatchers.argThat;
65 import static org.mockito.ArgumentMatchers.eq;
66 import static org.mockito.Mockito.inOrder;
67 import static org.mockito.Mockito.mock;
68 import static org.mockito.Mockito.timeout;
69 import static org.mockito.Mockito.verify;
70 
71 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
72 import android.accessibility.cts.common.InstrumentedAccessibilityService;
73 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
74 import android.accessibility.cts.common.ShellCommandBuilder;
75 import android.accessibilityservice.AccessibilityServiceInfo;
76 import android.accessibilityservice.GestureDescription;
77 import android.accessibilityservice.GestureDescription.StrokeDescription;
78 import android.accessibilityservice.MagnificationConfig;
79 import android.accessibilityservice.cts.activities.AccessibilityEndToEndActivity;
80 import android.accessibilityservice.cts.utils.EventCapturingMotionEventListener;
81 import android.accessibilityservice.cts.utils.ProviderCustomView;
82 import android.app.Activity;
83 import android.app.AlertDialog;
84 import android.app.Instrumentation;
85 import android.app.Notification;
86 import android.app.NotificationChannel;
87 import android.app.NotificationManager;
88 import android.app.PendingIntent;
89 import android.app.Service;
90 import android.app.UiAutomation;
91 import android.appwidget.AppWidgetHost;
92 import android.appwidget.AppWidgetManager;
93 import android.appwidget.AppWidgetProviderInfo;
94 import android.content.ComponentName;
95 import android.content.Context;
96 import android.content.Intent;
97 import android.content.pm.PackageManager;
98 import android.content.res.Configuration;
99 import android.content.res.Resources;
100 import android.graphics.PointF;
101 import android.graphics.Rect;
102 import android.graphics.Region;
103 import android.os.Bundle;
104 import android.os.Parcel;
105 import android.os.Process;
106 import android.os.SystemClock;
107 import android.platform.test.annotations.AppModeFull;
108 import android.platform.test.annotations.AsbSecurityTest;
109 import android.platform.test.annotations.Presubmit;
110 import android.platform.test.annotations.RequiresFlagsDisabled;
111 import android.platform.test.annotations.RequiresFlagsEnabled;
112 import android.platform.test.flag.junit.CheckFlagsRule;
113 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
114 import android.provider.Settings;
115 import android.text.TextUtils;
116 import android.util.Log;
117 import android.view.InputDevice;
118 import android.view.MotionEvent;
119 import android.view.TouchDelegate;
120 import android.view.View;
121 import android.view.Window;
122 import android.view.WindowManager;
123 import android.view.accessibility.AccessibilityEvent;
124 import android.view.accessibility.AccessibilityManager;
125 import android.view.accessibility.AccessibilityNodeInfo;
126 import android.view.accessibility.AccessibilityNodeProvider;
127 import android.view.accessibility.AccessibilityWindowInfo;
128 import android.view.accessibility.Flags;
129 import android.widget.Button;
130 import android.widget.EditText;
131 import android.widget.LinearLayout;
132 import android.widget.ListView;
133 import android.widget.TextView;
134 
135 import androidx.annotation.NonNull;
136 import androidx.annotation.Nullable;
137 import androidx.lifecycle.Lifecycle;
138 import androidx.test.ext.junit.rules.ActivityScenarioRule;
139 import androidx.test.ext.junit.runners.AndroidJUnit4;
140 import androidx.test.filters.MediumTest;
141 import androidx.test.platform.app.InstrumentationRegistry;
142 
143 import com.android.compatibility.common.util.ApiTest;
144 import com.android.compatibility.common.util.CddTest;
145 import com.android.compatibility.common.util.CtsMouseUtil;
146 import com.android.compatibility.common.util.SystemUtil;
147 import com.android.compatibility.common.util.TestUtils;
148 import com.android.sts.common.util.StsExtraBusinessLogicTestCase;
149 
150 import org.junit.After;
151 import org.junit.AfterClass;
152 import org.junit.Before;
153 import org.junit.BeforeClass;
154 import org.junit.Rule;
155 import org.junit.Test;
156 import org.junit.rules.RuleChain;
157 import org.junit.runner.RunWith;
158 
159 import java.time.Duration;
160 import java.util.ArrayDeque;
161 import java.util.ArrayList;
162 import java.util.Arrays;
163 import java.util.Deque;
164 import java.util.Iterator;
165 import java.util.List;
166 import java.util.concurrent.TimeoutException;
167 import java.util.concurrent.atomic.AtomicBoolean;
168 import java.util.concurrent.atomic.AtomicInteger;
169 import java.util.concurrent.atomic.AtomicReference;
170 
171 /**
172  * This class performs end-to-end testing of the accessibility feature by
173  * creating an {@link Activity} and poking around so {@link AccessibilityEvent}s
174  * are generated and their correct dispatch verified.
175  */
176 @RunWith(AndroidJUnit4.class)
177 @CddTest(requirements = {"3.10/C-1-1,C-1-2"})
178 @Presubmit
179 public class AccessibilityEndToEndTest extends StsExtraBusinessLogicTestCase {
180 
181     private static final String LOG_TAG = "AccessibilityEndToEndTest";
182 
183     private static final String GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND =
184             "appwidget grantbind --package android.accessibilityservice.cts --user ";
185 
186     private static final String REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND =
187             "appwidget revokebind --package android.accessibilityservice.cts --user ";
188 
189     private static final String APP_WIDGET_PROVIDER_PACKAGE = "foo.bar.baz";
190 
191     private static final int TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS = 1000;
192 
193     private static Instrumentation sInstrumentation;
194     private static UiAutomation sUiAutomation;
195 
196     private AccessibilityEndToEndActivity mActivity;
197     private ActivityScenarioRule<AccessibilityEndToEndActivity> mActivityRule =
198             new ActivityScenarioRule<>(AccessibilityEndToEndActivity.class);
199 
200     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
201             new AccessibilityDumpOnFailureRule();
202 
203     private CheckFlagsRule mCheckFlagsRule =
204             DeviceFlagsValueProvider.createCheckFlagsRule(sUiAutomation);
205 
206     private final InstrumentedAccessibilityServiceTestRule<
207             StubMotionInterceptingAccessibilityService>
208             mMotionInterceptingServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
209             StubMotionInterceptingAccessibilityService.class, false);
210 
211     @Rule
212     public final RuleChain mRuleChain = RuleChain
213             .outerRule(mActivityRule)
214             .around(mMotionInterceptingServiceRule)
215             .around(mDumpOnFailureRule)
216             .around(mCheckFlagsRule);
217 
218     @BeforeClass
oneTimeSetup()219     public static void oneTimeSetup() throws Exception {
220         sInstrumentation = InstrumentationRegistry.getInstrumentation();
221         sUiAutomation = sInstrumentation.getUiAutomation(FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
222 
223         AccessibilityServiceInfo serviceInfo = sUiAutomation.getServiceInfo();
224         // Make sure we could query windows.
225         serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
226         sUiAutomation.setServiceInfo(serviceInfo);
227     }
228 
229     @AfterClass
postTestTearDown()230     public static void postTestTearDown() {
231         sUiAutomation.destroy();
232     }
233 
234     @Before
setUp()235     public void setUp() throws Exception {
236         sUiAutomation.adoptShellPermissionIdentity(POST_NOTIFICATIONS);
237         mActivityRule
238                 .getScenario()
239                 .moveToState(Lifecycle.State.RESUMED)
240                 .onActivity(activity -> mActivity = activity);
241     }
242 
243     @After
tearDown()244     public void tearDown() throws Exception {
245         sInstrumentation.waitForIdleSync();
246         sUiAutomation.dropShellPermissionIdentity();
247     }
248 
249     @MediumTest
250     @Test
251     @ApiTest(apis = {"android.view.View#setSelected",
252             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewSelectedAccessibilityEvent()253     public void testTypeViewSelectedAccessibilityEvent() throws Throwable {
254         try {
255             // Need to be non-touch mode so that calling setSelection will make the item selected
256             sInstrumentation.setInTouchMode(false);
257             sInstrumentation.waitForIdleSync();
258             // create and populate the expected event
259             final AccessibilityEvent expected = AccessibilityEvent.obtain();
260             expected.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
261             expected.setClassName(ListView.class.getName());
262             expected.setPackageName(mActivity.getPackageName());
263             expected.setDisplayId(mActivity.getDisplayId());
264             expected.getText().add(mActivity.getString(R.string.second_list_item));
265             expected.setItemCount(2);
266             expected.setCurrentItemIndex(1);
267             expected.setEnabled(true);
268             expected.setScrollable(false);
269             expected.setFromIndex(0);
270             expected.setToIndex(1);
271 
272             // check the received event
273             AccessibilityEvent awaitedEvent =
274                     sUiAutomation.executeAndWaitForEvent(
275                             () -> {
276                                 // trigger the event
277                                 mActivityRule
278                                         .getScenario()
279                                         .onActivity(
280                                                 activity -> {
281                                                     final ListView listView =
282                                                             activity.findViewById(R.id.listview);
283                                                     listView.setSelection(1);
284                                                 });
285                             },
286                             event -> equalsAccessibilityEvent(event, expected),
287                             DEFAULT_TIMEOUT_MS);
288             assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
289         } finally {
290             sInstrumentation.resetInTouchMode();
291             sInstrumentation.waitForIdleSync();
292         }
293     }
294 
295     @MediumTest
296     @Test
297     @ApiTest(apis = {"android.view.View#performClick",
298             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewClickedAccessibilityEvent()299     public void testTypeViewClickedAccessibilityEvent() throws Throwable {
300         // create and populate the expected event
301         final AccessibilityEvent expected = AccessibilityEvent.obtain();
302         expected.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED);
303         expected.setClassName(Button.class.getName());
304         expected.setPackageName(mActivity.getPackageName());
305         expected.setDisplayId(mActivity.getDisplayId());
306         expected.getText().add(mActivity.getString(R.string.button_title));
307         expected.setEnabled(true);
308 
309         final Button button = (Button) mActivity.findViewById(R.id.button);
310 
311         AccessibilityEvent awaitedEvent =
312             sUiAutomation.executeAndWaitForEvent(
313                 new Runnable() {
314             @Override
315             public void run() {
316                 // trigger the event
317                 mActivity.runOnUiThread(new Runnable() {
318                     @Override
319                     public void run() {
320                         button.performClick();
321                     }
322                 });
323             }},
324             new UiAutomation.AccessibilityEventFilter() {
325                 // check the received event
326                 @Override
327                 public boolean accept(AccessibilityEvent event) {
328                         return equalsAccessibilityEvent(event, expected);
329                 }
330             },
331                     DEFAULT_TIMEOUT_MS);
332         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
333     }
334 
335     @MediumTest
336     @Test
337     @ApiTest(apis = {"android.view.View#performLongClick",
338             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewLongClickedAccessibilityEvent()339     public void testTypeViewLongClickedAccessibilityEvent() throws Throwable {
340         // create and populate the expected event
341         final AccessibilityEvent expected = AccessibilityEvent.obtain();
342         expected.setEventType(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
343         expected.setClassName(Button.class.getName());
344         expected.setPackageName(mActivity.getPackageName());
345         expected.setDisplayId(mActivity.getDisplayId());
346         expected.getText().add(mActivity.getString(R.string.button_title));
347         expected.setEnabled(true);
348 
349         final Button button = (Button) mActivity.findViewById(R.id.button);
350 
351         AccessibilityEvent awaitedEvent =
352             sUiAutomation.executeAndWaitForEvent(
353                 new Runnable() {
354             @Override
355             public void run() {
356                 // trigger the event
357                 mActivity.runOnUiThread(new Runnable() {
358                     @Override
359                     public void run() {
360                         button.performLongClick();
361                     }
362                 });
363             }},
364             new UiAutomation.AccessibilityEventFilter() {
365                 // check the received event
366                 @Override
367                 public boolean accept(AccessibilityEvent event) {
368                         return equalsAccessibilityEvent(event, expected);
369                 }
370             },
371                     DEFAULT_TIMEOUT_MS);
372         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
373     }
374 
375     @MediumTest
376     @Test
377     @ApiTest(apis = {"android.view.View#requestFocus",
378             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewFocusedAccessibilityEvent()379     public void testTypeViewFocusedAccessibilityEvent() throws Throwable {
380         mActivityRule
381                 .getScenario()
382                 .moveToState(Lifecycle.State.RESUMED)
383                 .onActivity(activity -> mActivity = activity);
384         // create and populate the expected event
385         final AccessibilityEvent expected = AccessibilityEvent.obtain();
386         expected.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
387         expected.setClassName(Button.class.getName());
388         expected.setPackageName(mActivity.getPackageName());
389         expected.setDisplayId(mActivity.getDisplayId());
390         expected.getText().add(mActivity.getString(R.string.button_title));
391         expected.setItemCount(6);
392         expected.setCurrentItemIndex(4);
393         expected.setEnabled(true);
394 
395         AccessibilityEvent awaitedEvent =
396                 sUiAutomation.executeAndWaitForEvent(
397                         () ->
398                                 mActivityRule
399                                         .getScenario()
400                                         .onActivity(
401                                                 activity -> {
402                                                     final Button button =
403                                                             activity.findViewById(
404                                                                     R.id.buttonWithTooltip);
405                                                     button.setFocusable(true);
406                                                     button.setFocusableInTouchMode(true);
407                                                     button.requestFocus();
408                                                 }),
409                         (event) -> equalsAccessibilityEvent(event, expected),
410                         DEFAULT_TIMEOUT_MS);
411         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
412     }
413 
414     @MediumTest
415     @Test
416     @ApiTest(apis = {"android.text.Editable#replace",
417             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewTextChangedAccessibilityEvent()418     public void testTypeViewTextChangedAccessibilityEvent() throws Throwable {
419         final EditText editText = mActivity.findViewById(R.id.edittext);
420 
421         AccessibilityEvent awaitedFocusEvent =
422                 sUiAutomation.executeAndWaitForEvent(
423                         new Runnable() {
424                             @Override
425                             public void run() {
426                                 // trigger the event
427                                 mActivity.runOnUiThread(
428                                         new Runnable() {
429                                             @Override
430                                             public void run() {
431                                                 editText.requestFocus();
432                                             }
433                                         });
434                             }
435                         },
436                         new UiAutomation.AccessibilityEventFilter() {
437                             // check the received event
438                             @Override
439                             public boolean accept(AccessibilityEvent event) {
440                                 return event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED;
441                             }
442                         },
443                         DEFAULT_TIMEOUT_MS);
444         assertNotNull("Did not receive expected focuss event.", awaitedFocusEvent);
445 
446         final String beforeText = mActivity.getString(R.string.text_input_blah);
447         final String newText = mActivity.getString(R.string.text_input_blah_blah);
448         final String afterText = beforeText.substring(0, 3) + newText;
449 
450         // create and populate the expected event
451         final AccessibilityEvent expected = AccessibilityEvent.obtain();
452         expected.setEventType(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
453         expected.setClassName(EditText.class.getName());
454         expected.setPackageName(mActivity.getPackageName());
455         expected.setDisplayId(mActivity.getDisplayId());
456         expected.getText().add(afterText);
457         expected.setBeforeText(beforeText);
458         expected.setFromIndex(3);
459         expected.setAddedCount(9);
460         expected.setRemovedCount(1);
461         expected.setEnabled(true);
462 
463         AccessibilityEvent awaitedTextChangeEvent =
464                 sUiAutomation.executeAndWaitForEvent(
465                         new Runnable() {
466                             @Override
467                             public void run() {
468                                 // trigger the event
469                                 mActivity.runOnUiThread(
470                                         new Runnable() {
471                                             @Override
472                                             public void run() {
473                                                 editText.getEditableText().replace(3, 4, newText);
474                                             }
475                                         });
476                             }
477                         },
478                         new UiAutomation.AccessibilityEventFilter() {
479                             // check the received event
480                             @Override
481                             public boolean accept(AccessibilityEvent event) {
482                                 return equalsAccessibilityEvent(event, expected);
483                             }
484                         },
485                         DEFAULT_TIMEOUT_MS);
486         assertNotNull("Did not receive expected event: " + expected, awaitedTextChangeEvent);
487     }
488 
489     @MediumTest
490     @Test
491     @ApiTest(apis = {"android.view.ViewManager#addView",
492             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeWindowStateChangedAccessibilityEvent()493     public void testTypeWindowStateChangedAccessibilityEvent() throws Throwable {
494         // create and populate the expected event
495         final AccessibilityEvent expected = AccessibilityEvent.obtain();
496         expected.setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
497         expected.setClassName(AlertDialog.class.getName());
498         expected.setPackageName(mActivity.getPackageName());
499         expected.setDisplayId(mActivity.getDisplayId());
500         expected.getText().add(mActivity.getString(R.string.alert_title));
501         expected.getText().add(mActivity.getString(R.string.alert_message));
502         expected.setEnabled(true);
503 
504         final AtomicReference<AlertDialog> dialog = new AtomicReference<>();
505         try {
506             // check the received event
507             final AccessibilityEvent awaitedEvent =
508                     sUiAutomation.executeAndWaitForEvent(
509                             () -> {
510                                 // trigger the event
511                                 mActivityRule
512                                         .getScenario()
513                                         .onActivity(
514                                                 activity -> {
515                                                     dialog.set(
516                                                             new AlertDialog.Builder(activity)
517                                                                     .setTitle(R.string.alert_title)
518                                                                     .setMessage(
519                                                                             R.string.alert_message)
520                                                                     .create());
521                                                     dialog.get().show();
522                                                 });
523                             },
524                             event -> equalsAccessibilityEvent(event, expected),
525                             DEFAULT_TIMEOUT_MS);
526             assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
527         } finally {
528             if (dialog.get() != null) {
529                 // dismiss the dialog window to prevent WindowLeaked
530                 dialog.get().dismiss();
531             }
532         }
533     }
534 
535     @MediumTest
536     @Test
537     @ApiTest(apis = {"android.app.Activity#finish",
538             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeWindowsChangedAccessibilityEvent()539     public void testTypeWindowsChangedAccessibilityEvent() throws Throwable {
540         // create and populate the expected event
541         final AccessibilityEvent expected = AccessibilityEvent.obtain();
542         expected.setEventType(AccessibilityEvent.TYPE_WINDOWS_CHANGED);
543         expected.setDisplayId(mActivity.getDisplayId());
544 
545         // check the received event
546         AccessibilityEvent awaitedEvent =
547             sUiAutomation.executeAndWaitForEvent(
548                     () -> mActivity.runOnUiThread(() -> mActivity.finish()),
549                     event -> event.getWindowChanges() == AccessibilityEvent.WINDOWS_CHANGE_REMOVED
550                             && equalsAccessibilityEvent(event, expected),
551                     DEFAULT_TIMEOUT_MS);
552         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
553     }
554 
555     @MediumTest
556     @AppModeFull
557     @SuppressWarnings("deprecation")
558     @Test
559     @ApiTest(apis = {"android.app.NotificationManager#notify",
560             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeNotificationStateChangedAccessibilityEvent()561     public void testTypeNotificationStateChangedAccessibilityEvent() throws Throwable {
562         // No notification UI on televisions.
563         if ((mActivity.getResources().getConfiguration().uiMode
564                 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) {
565             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
566                     " - No notification UI on televisions.");
567             return;
568         }
569         PackageManager pm = sInstrumentation.getTargetContext().getPackageManager();
570         if (pm.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
571             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
572                     " - Watches have different notification system.");
573             return;
574         }
575         assumeFalse("Skipping - Automotive handle notifications differently.",
576                 isAutomotive(sInstrumentation.getTargetContext()));
577 
578         String message = mActivity.getString(R.string.notification_message);
579 
580         final NotificationManager notificationManager =
581                 (NotificationManager) mActivity.getSystemService(Service.NOTIFICATION_SERVICE);
582         final NotificationChannel channel =
583                 new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT);
584         try {
585             // create the notification to send
586             channel.enableVibration(true);
587             channel.enableLights(true);
588             channel.setBypassDnd(true);
589             notificationManager.createNotificationChannel(channel);
590             final int notificationId = 1;
591             final Notification notification =
592                     new Notification.Builder(mActivity, channel.getId())
593                             .setSmallIcon(android.R.drawable.stat_notify_call_mute)
594                             .setContentIntent(PendingIntent.getActivity(mActivity, 0,
595                                     new Intent(),
596             PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE))
597                             .setTicker(message)
598                             .setContentTitle("")
599                             .setContentText("")
600                             .setPriority(Notification.PRIORITY_MAX)
601                             // Mark the notification as "interruptive" by specifying a vibration
602                             // pattern. This ensures it's announced properly on watch-type devices.
603                             .setVibrate(new long[]{})
604                             .build();
605 
606             // create and populate the expected event
607             final AccessibilityEvent expected = AccessibilityEvent.obtain();
608             expected.setEventType(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
609             expected.setClassName(Notification.class.getName());
610             expected.setPackageName(mActivity.getPackageName());
611             expected.getText().add(message);
612             expected.setParcelableData(notification);
613 
614             AccessibilityEvent awaitedEvent =
615                     sUiAutomation.executeAndWaitForEvent(
616                             new Runnable() {
617                                 @Override
618                                 public void run() {
619                                     // trigger the event
620                                     mActivity.runOnUiThread(new Runnable() {
621                                         @Override
622                                         public void run() {
623                                             // trigger the event
624                                             notificationManager
625                                                     .notify(notificationId, notification);
626                                             mActivity.finish();
627                                         }
628                                     });
629                                 }
630                             },
631                             new UiAutomation.AccessibilityEventFilter() {
632                                 // check the received event
633                                 @Override
634                                 public boolean accept(AccessibilityEvent event) {
635                                     return equalsAccessibilityEvent(event, expected);
636                                 }
637                             },
638                             DEFAULT_TIMEOUT_MS);
639             assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
640         } finally {
641             notificationManager.deleteNotificationChannel(channel.getId());
642         }
643     }
644 
645     @MediumTest
646     @Test
647     @ApiTest(apis = {"android.view.accessibility.AccessibilityManager#interrupt"})
testInterrupt_notifiesService()648     public void testInterrupt_notifiesService() {
649         InstrumentedAccessibilityService service =
650                 enableService(InstrumentedAccessibilityService.class);
651 
652         try {
653             assertFalse(service.wasOnInterruptCalled());
654 
655             mActivity.runOnUiThread(() -> {
656                 AccessibilityManager accessibilityManager = (AccessibilityManager) mActivity
657                         .getSystemService(Service.ACCESSIBILITY_SERVICE);
658                 accessibilityManager.interrupt();
659             });
660 
661             Object waitObject = service.getInterruptWaitObject();
662             synchronized (waitObject) {
663                 if (!service.wasOnInterruptCalled()) {
664                     try {
665                         waitObject.wait(DEFAULT_TIMEOUT_MS);
666                     } catch (InterruptedException e) {
667                         // Do nothing
668                     }
669                 }
670             }
671             assertTrue(service.wasOnInterruptCalled());
672         } finally {
673             service.disableSelfAndRemove();
674         }
675     }
676 
677     @MediumTest
678     @Test
679     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getPackageName"})
testPackageNameCannotBeFaked()680     public void testPackageNameCannotBeFaked() {
681         mActivityRule
682                 .getScenario()
683                 .onActivity(
684                         activity -> {
685                             // Set the activity to report fake package for events and nodes
686                             activity.setReportedPackageName("foo.bar.baz");
687 
688                             // Make sure node package cannot be faked
689                             AccessibilityNodeInfo root = sUiAutomation.getRootInActiveWindow();
690                             assertPackageName(root, activity.getPackageName());
691                         });
692 
693         // Make sure event package cannot be faked
694         try {
695             sUiAutomation.executeAndWaitForEvent(
696                     () ->
697                             mActivityRule
698                                     .getScenario()
699                                     .onActivity(
700                                             activity -> {
701                                                 final Button button =
702                                                         activity.findViewById(R.id.button);
703                                                 button.setFocusable(true);
704                                                 button.setFocusableInTouchMode(true);
705                                                 button.requestFocus();
706                                                 mActivity = activity;
707                                             }),
708                     (AccessibilityEvent event) ->
709                             event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
710                                     && event.getPackageName().equals(mActivity.getPackageName()),
711                     DEFAULT_TIMEOUT_MS);
712         } catch (TimeoutException e) {
713             fail("Events from fake package should be fixed to use the correct package");
714         }
715     }
716 
717     @AppModeFull
718     @MediumTest
719     @Test
720     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getPackageName"})
testPackageNameCannotBeFakedAppWidget()721     public void testPackageNameCannotBeFakedAppWidget() throws Exception {
722         if (!hasAppWidgets()) {
723             return;
724         }
725 
726         try {
727             sInstrumentation.setInTouchMode(false);
728             sInstrumentation.waitForIdleSync();
729 
730             sInstrumentation.runOnMainSync(
731                     () -> {
732                         // Set the activity to report fake package for events and nodes
733                         mActivity.setReportedPackageName(APP_WIDGET_PROVIDER_PACKAGE);
734 
735                         // Make sure we cannot report nodes as if from the widget package
736                         AccessibilityNodeInfo root = sUiAutomation.getRootInActiveWindow();
737                         assertPackageName(root, mActivity.getPackageName());
738                     });
739 
740             // Make sure we cannot send events as if from the widget package
741             try {
742                 sUiAutomation.executeAndWaitForEvent(
743                         () ->
744                                 sInstrumentation.runOnMainSync(
745                                         () -> mActivity.findViewById(R.id.button).requestFocus()),
746                         (AccessibilityEvent event) ->
747                                 event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
748                                         && event.getPackageName()
749                                                 .equals(mActivity.getPackageName()),
750                         DEFAULT_TIMEOUT_MS);
751             } catch (TimeoutException e) {
752                 fail("Should not be able to send events from a widget package if no widget hosted");
753             }
754 
755             // Create a host and start listening.
756             final AppWidgetHost host = new AppWidgetHost(sInstrumentation.getTargetContext(), 0);
757             host.deleteHost();
758             host.startListening();
759 
760             // Well, app do not have this permission unless explicitly granted
761             // by the user. Now we will pretend for the user and grant it.
762             grantBindAppWidgetPermission();
763 
764             // Allocate an app widget id to bind.
765             final int appWidgetId = host.allocateAppWidgetId();
766             try {
767                 // Grab a provider we defined to be bound.
768                 final AppWidgetProviderInfo provider = getAppWidgetProviderInfo();
769 
770                 // Bind the widget.
771                 final boolean widgetBound =
772                         getAppWidgetManager()
773                                 .bindAppWidgetIdIfAllowed(
774                                         appWidgetId,
775                                         provider.getProfile(),
776                                         provider.provider,
777                                         null);
778                 assertTrue(widgetBound);
779 
780                 // Make sure the app can use the package of a widget it hosts
781                 sInstrumentation.runOnMainSync(
782                         () -> {
783                             // Make sure we can report nodes as if from the widget package
784                             AccessibilityNodeInfo root = sUiAutomation.getRootInActiveWindow();
785                             assertPackageName(root, APP_WIDGET_PROVIDER_PACKAGE);
786                         });
787 
788                 // Make sure we can send events as if from the widget package
789                 try {
790                     sUiAutomation.executeAndWaitForEvent(
791                             () ->
792                                     sInstrumentation.runOnMainSync(
793                                             () ->
794                                                     mActivity
795                                                             .findViewById(R.id.button)
796                                                             .performClick()),
797                             (AccessibilityEvent event) ->
798                                     event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED
799                                             && event.getPackageName()
800                                                     .equals(APP_WIDGET_PROVIDER_PACKAGE),
801                             DEFAULT_TIMEOUT_MS);
802                 } catch (TimeoutException e) {
803                     fail("Should be able to send events from a widget package if widget hosted");
804                 }
805             } finally {
806                 // Clean up.
807                 host.deleteAppWidgetId(appWidgetId);
808                 host.deleteHost();
809                 revokeBindAppWidgetPermission();
810             }
811         } finally {
812             sInstrumentation.resetInTouchMode();
813             sInstrumentation.waitForIdleSync();
814         }
815     }
816 
817     @MediumTest
818     @Test
819     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#isHeading"})
testViewHeadingReportedToAccessibility()820     public void testViewHeadingReportedToAccessibility() throws Exception {
821         final EditText editText = (EditText) getOnMain(sInstrumentation,
822                 () -> mActivity.findViewById(R.id.edittext));
823         // Make sure the edittext was populated properly from xml
824         final boolean editTextIsHeading = getOnMain(sInstrumentation,
825                 editText::isAccessibilityHeading);
826         assertTrue("isAccessibilityHeading not populated properly from xml", editTextIsHeading);
827 
828         final AccessibilityNodeInfo editTextNode = sUiAutomation.getRootInActiveWindow()
829                 .findAccessibilityNodeInfosByViewId(
830                         "android.accessibilityservice.cts:id/edittext")
831                 .get(0);
832         assertTrue("isAccessibilityHeading not reported to accessibility",
833                 editTextNode.isHeading());
834 
835         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() ->
836                         editText.setAccessibilityHeading(false)),
837                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
838                 DEFAULT_TIMEOUT_MS);
839         editTextNode.refresh();
840         assertFalse("isAccessibilityHeading not reported to accessibility after update",
841                 editTextNode.isHeading());
842     }
843 
844     @MediumTest
845     @Test
846     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTooltipText"})
testTooltipTextReportedToAccessibility()847     public void testTooltipTextReportedToAccessibility() {
848         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
849                 .findAccessibilityNodeInfosByViewId(
850                         "android.accessibilityservice.cts:id/buttonWithTooltip")
851                 .get(0);
852         assertEquals("Tooltip text not reported to accessibility",
853                 sInstrumentation.getContext().getString(R.string.button_tooltip),
854                 buttonNode.getTooltipText());
855     }
856 
857     @MediumTest
858     @Test
testAccessibilityActionRetained()859     public void testAccessibilityActionRetained() throws Exception {
860         final AccessibilityNodeInfo sentInfo = new AccessibilityNodeInfo(new View(mActivity));
861         sentInfo.addAction(ACTION_SCROLL_IN_DIRECTION);
862         final Parcel parcel = Parcel.obtain();
863         sentInfo.writeToParcelNoRecycle(parcel, 0);
864         parcel.setDataPosition(0);
865         AccessibilityNodeInfo receivedInfo = AccessibilityNodeInfo.CREATOR.createFromParcel(parcel);
866 
867         assertThat(receivedInfo.getActionList()).contains(ACTION_SCROLL_IN_DIRECTION);
868 
869         parcel.recycle();
870     }
871 
872     @MediumTest
873     @Test
874     @ApiTest(apis = {
875             "android.view.accessibility.AccessibilityNodeInfo#ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT"})
testActionArgumentScrollAmountFloat()876     public void testActionArgumentScrollAmountFloat() throws Exception {
877         class MyView extends TextView {
878             MyView(Context context) {
879                 super(context);
880             }
881 
882             @Override
883             public boolean performAccessibilityAction(int action, Bundle args) {
884                 final float scrollAmount = args.getFloat(ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT, -1F);
885                 return scrollAmount < 0 ? false : true;
886             }
887         }
888 
889         Bundle bundle = new Bundle();
890         bundle.putFloat(ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT, -1);
891         String text = "action_argument_scroll_amount";
892 
893         sUiAutomation.executeAndWaitForEvent(
894                 () ->
895                         sInstrumentation.runOnMainSync(
896                                 () -> {
897                                     final MyView myView = new MyView(mActivity);
898                                     myView.setText(text);
899                                     ((LinearLayout) mActivity.findViewById(R.id.containerView))
900                                             .addView(myView);
901                                 }),
902                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
903                 DEFAULT_TIMEOUT_MS);
904         AccessibilityNodeInfo myViewNode =
905                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByText(
906                         text).getFirst();
907 
908         assertThat(myViewNode.performAction(
909                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId(),
910                 bundle)).isFalse();
911 
912         bundle.putFloat(ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT, 1);
913         assertThat(myViewNode.performAction(
914                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId(),
915                 bundle)).isTrue();
916     }
917 
918     @MediumTest
919     @Test
testCollectionInfoFields()920     public void testCollectionInfoFields() {
921         // Collection with 4 items, 1 unimportant.
922         AccessibilityNodeInfo.CollectionInfo ci =
923                 new AccessibilityNodeInfo.CollectionInfo.Builder()
924                         .setRowCount(4)
925                         .setColumnCount(1)
926                         .setHierarchical(false)
927                         .setSelectionMode(0)
928                         .setItemCount(4)
929                         .setImportantForAccessibilityItemCount(3)
930                         .build();
931 
932         final View listView = mActivity.findViewById(R.id.listview);
933 
934         listView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
935             @Override
936             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
937                 super.onInitializeAccessibilityNodeInfo(host, info);
938                 info.setCollectionInfo(ci);
939             }
940         });
941 
942         AccessibilityNodeInfo foundInfo =
943                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
944                         mActivity.getResources().getResourceName(R.id.listview)).get(0);
945         AccessibilityNodeInfo.CollectionInfo foundCi = foundInfo.getCollectionInfo();
946 
947         assertThat(foundCi.getRowCount()).isEqualTo(ci.getRowCount());
948         assertThat(foundCi.getColumnCount()).isEqualTo(ci.getColumnCount());
949         assertThat(foundCi.isHierarchical()).isEqualTo(ci.isHierarchical());
950         assertThat(foundCi.getSelectionMode()).isEqualTo(ci.getSelectionMode());
951         assertThat(foundCi.getItemCount()).isEqualTo(ci.getItemCount());
952         assertThat(foundCi.getImportantForAccessibilityItemCount()).isEqualTo(
953                 ci.getImportantForAccessibilityItemCount());
954     }
955 
956     @MediumTest
957     @Test
958     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getActionList"})
testTooltipTextActionsReportedToAccessibility()959     public void testTooltipTextActionsReportedToAccessibility() throws Exception {
960         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
961                 .findAccessibilityNodeInfosByViewId(
962                         "android.accessibilityservice.cts:id/buttonWithTooltip")
963                 .get(0);
964         assertFalse(hasTooltipShowing(R.id.buttonWithTooltip));
965         assertThat(buttonNode.getActionList()).contains(ACTION_SHOW_TOOLTIP);
966         assertThat(buttonNode.getActionList()).doesNotContain(ACTION_HIDE_TOOLTIP);
967         sUiAutomation.executeAndWaitForEvent(
968                 () -> buttonNode.performAction(ACTION_SHOW_TOOLTIP.getId()),
969                 filterForEventTypeWithAction(
970                         AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
971                         ACTION_SHOW_TOOLTIP.getId()),
972                 DEFAULT_TIMEOUT_MS);
973         sUiAutomation.waitForIdle(DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_GLOBAL_TIMEOUT_MS);
974 
975         // The button should now be showing the tooltip, so it should have the option to hide it.
976         buttonNode.refresh();
977         assertThat(buttonNode.getActionList()).contains(ACTION_HIDE_TOOLTIP);
978         assertThat(buttonNode.getActionList()).doesNotContain(ACTION_SHOW_TOOLTIP);
979         assertTrue(hasTooltipShowing(R.id.buttonWithTooltip));
980     }
981 
982     @MediumTest
983     @Test
984     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTraversalBefore"})
testTraversalBeforeReportedToAccessibility()985     public void testTraversalBeforeReportedToAccessibility() throws Exception {
986         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
987                 .findAccessibilityNodeInfosByViewId(
988                         "android.accessibilityservice.cts:id/buttonWithTooltip")
989                 .get(0);
990         final AccessibilityNodeInfo beforeNode = buttonNode.getTraversalBefore();
991         assertThat(beforeNode).isNotNull();
992         assertThat(beforeNode.getViewIdResourceName()).isEqualTo(
993                 "android.accessibilityservice.cts:id/edittext");
994 
995         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(
996                 () -> mActivity.findViewById(R.id.buttonWithTooltip)
997                         .setAccessibilityTraversalBefore(View.NO_ID)),
998                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
999                 DEFAULT_TIMEOUT_MS);
1000 
1001         buttonNode.refresh();
1002         assertThat(buttonNode.getTraversalBefore()).isNull();
1003     }
1004 
1005     @MediumTest
1006     @Test
1007     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTraversalAfter"})
testTraversalAfterReportedToAccessibility()1008     public void testTraversalAfterReportedToAccessibility() throws Exception {
1009         final AccessibilityNodeInfo editNode = sUiAutomation.getRootInActiveWindow()
1010                 .findAccessibilityNodeInfosByViewId(
1011                         "android.accessibilityservice.cts:id/edittext")
1012                 .get(0);
1013         final AccessibilityNodeInfo afterNode = editNode.getTraversalAfter();
1014         assertThat(afterNode).isNotNull();
1015         assertThat(afterNode.getViewIdResourceName()).isEqualTo(
1016                 "android.accessibilityservice.cts:id/buttonWithTooltip");
1017 
1018         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(
1019                 () -> mActivity.findViewById(R.id.edittext)
1020                         .setAccessibilityTraversalAfter(View.NO_ID)),
1021                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
1022                 DEFAULT_TIMEOUT_MS);
1023 
1024         editNode.refresh();
1025         assertThat(editNode.getTraversalAfter()).isNull();
1026     }
1027 
1028     @MediumTest
1029     @Test
1030     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getLabelFor"})
testLabelForReportedToAccessibility()1031     public void testLabelForReportedToAccessibility() throws Exception {
1032         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() -> mActivity
1033                 .findViewById(R.id.edittext).setLabelFor(R.id.buttonWithTooltip)),
1034                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
1035                 DEFAULT_TIMEOUT_MS);
1036         // TODO: b/78022650: This code should move above the executeAndWait event. It's here because
1037         // the a11y cache doesn't get notified when labelFor changes, so the node with the
1038         // labledBy isn't updated.
1039         final AccessibilityNodeInfo editNode = sUiAutomation.getRootInActiveWindow()
1040                 .findAccessibilityNodeInfosByViewId(
1041                         "android.accessibilityservice.cts:id/edittext")
1042                 .get(0);
1043         editNode.refresh();
1044         final AccessibilityNodeInfo labelForNode = editNode.getLabelFor();
1045         assertThat(labelForNode).isNotNull();
1046         // Labeled node should indicate that it is labeled by the other one
1047         assertThat(labelForNode.getLabeledBy()).isEqualTo(editNode);
1048     }
1049 
1050     @MediumTest
1051     @Test
1052     @ApiTest(apis = {"android.view.View#setContextClickable"})
testIsImportantForAccessibility_isContextClickable_isImportant()1053     public void testIsImportantForAccessibility_isContextClickable_isImportant() throws
1054             TimeoutException {
1055         sInstrumentation.runOnMainSync(() -> mActivity.findViewById(R.id.autoImportantLinearLayout)
1056                 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO));
1057 
1058         final String autoImportantLinearLayoutName = mActivity.getResources().getResourceName(
1059                 R.id.autoImportantLinearLayout);
1060         final AccessibilityNodeInfo autoImportantLinearLayoutNode =
1061                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1062                         autoImportantLinearLayoutName).get(0);
1063 
1064         assertThat(autoImportantLinearLayoutNode.isContextClickable()).isFalse();
1065         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isFalse();
1066 
1067         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() ->
1068                         mActivity.findViewById(R.id.autoImportantLinearLayout)
1069                                 .setContextClickable(true)),
1070                 // Setting clickable sends an event of subtype CONTENT_CHANGE_TYPE_UNDEFINED.
1071                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
1072                 DEFAULT_TIMEOUT_MS);
1073 
1074         autoImportantLinearLayoutNode.refresh();
1075         assertThat(autoImportantLinearLayoutNode.isContextClickable()).isTrue();
1076         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isTrue();
1077     }
1078 
1079     @MediumTest
1080     @Test
1081     @ApiTest(apis = {"android.view.View#setAccessibilityHeading"})
testIsImportantForAccessibility_isHeading_isImportant()1082     public void testIsImportantForAccessibility_isHeading_isImportant() throws
1083             TimeoutException {
1084         sInstrumentation.runOnMainSync(() -> mActivity.findViewById(R.id.autoImportantLinearLayout)
1085                 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO));
1086 
1087         final String autoImportantLinearLayoutName = mActivity.getResources().getResourceName(
1088                 R.id.autoImportantLinearLayout);
1089         final AccessibilityNodeInfo autoImportantLinearLayoutNode =
1090                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1091                         autoImportantLinearLayoutName).get(0);
1092 
1093         assertThat(autoImportantLinearLayoutNode.isHeading()).isFalse();
1094         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isFalse();
1095 
1096         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() ->
1097                         mActivity.findViewById(R.id.autoImportantLinearLayout)
1098                                 .setAccessibilityHeading(true)),
1099                 // Setting a heading sends an event of subtype CONTENT_CHANGE_TYPE_UNDEFINED.
1100                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
1101                 DEFAULT_TIMEOUT_MS);
1102 
1103         autoImportantLinearLayoutNode.refresh();
1104         assertThat(autoImportantLinearLayoutNode.isHeading()).isTrue();
1105         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isTrue();
1106     }
1107 
1108     @MediumTest
1109 
1110     @Test
1111     @ApiTest(apis = {"android.view.View#setScreenReaderFocusable"})
testIsImportantForAccessibility_isScreenReaderFocusable_isImportant()1112     public void testIsImportantForAccessibility_isScreenReaderFocusable_isImportant() throws
1113             TimeoutException {
1114         sInstrumentation.runOnMainSync(() -> mActivity.findViewById(R.id.autoImportantLinearLayout)
1115                 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO));
1116 
1117         final String autoImportantLinearLayoutName = mActivity.getResources().getResourceName(
1118                 R.id.autoImportantLinearLayout);
1119         final AccessibilityNodeInfo autoImportantLinearLayoutNode =
1120                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1121                         autoImportantLinearLayoutName).get(0);
1122 
1123         assertThat(autoImportantLinearLayoutNode.isScreenReaderFocusable()).isFalse();
1124         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isFalse();
1125 
1126         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() ->
1127                         mActivity.findViewById(R.id.autoImportantLinearLayout)
1128                                 .setScreenReaderFocusable(true)),
1129                 // Setting focusable sends an event of subtype CONTENT_CHANGE_TYPE_UNDEFINED.
1130                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
1131                 DEFAULT_TIMEOUT_MS);
1132 
1133         autoImportantLinearLayoutNode.refresh();
1134         assertThat(autoImportantLinearLayoutNode.isScreenReaderFocusable()).isTrue();
1135         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isTrue();
1136     }
1137 
1138     @MediumTest
1139     @Test
1140     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo"
1141             + "#isImportantForAccessibility"})
testDelegate_ImportantForAccessibility()1142     public void testDelegate_ImportantForAccessibility() throws Exception {
1143         final View delegateView = mActivity.findViewById(R.id.autoImportantLinearLayout);
1144         sInstrumentation.runOnMainSync(() ->
1145                 delegateView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO));
1146 
1147         final AccessibilityNodeInfo autoImportantLinearLayoutNode =
1148                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1149                         mActivity.getResources().getResourceName(
1150                                 R.id.autoImportantLinearLayout)).get(0);
1151 
1152         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isFalse();
1153 
1154         sInstrumentation.runOnMainSync(() -> delegateView.setAccessibilityDelegate(
1155                 new View.AccessibilityDelegate()));
1156 
1157         autoImportantLinearLayoutNode.refresh();
1158         assertThat(autoImportantLinearLayoutNode.isImportantForAccessibility()).isTrue();
1159     }
1160 
1161     @MediumTest
1162     @Test
1163     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo"
1164             + "#isImportantForAccessibility"})
testProviderView_ImportantForAccessibility()1165     public void testProviderView_ImportantForAccessibility() {
1166         final ProviderCustomView customProviderView = mActivity.findViewById(
1167                 R.id.autoImportantProviderView);
1168         sInstrumentation.runOnMainSync(() ->
1169                 customProviderView.setImportantForAccessibility(
1170                         View.IMPORTANT_FOR_ACCESSIBILITY_AUTO));
1171         // Verify first that the node is not important if there is no provider.
1172         customProviderView.setReturnProvider(false);
1173         final AccessibilityNodeInfo autoImportantProviderViewNode =
1174                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1175                         mActivity.getResources().getResourceName(
1176                                 R.id.autoImportantProviderView)).get(0);
1177         assertThat(autoImportantProviderViewNode.isImportantForAccessibility()).isFalse();
1178 
1179         customProviderView.setReturnProvider(true);
1180 
1181         autoImportantProviderViewNode.refresh();
1182 
1183         assertThat(autoImportantProviderViewNode.isImportantForAccessibility()).isTrue();
1184     }
1185 
1186     @MediumTest
1187     @Test
1188     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#performAction"})
testA11yActionTriggerMotionEventActionOutside()1189     public void testA11yActionTriggerMotionEventActionOutside() throws Exception {
1190         final View.OnTouchListener listener = mock(View.OnTouchListener.class);
1191         final AccessibilityNodeInfo button = sUiAutomation.getRootInActiveWindow()
1192                 .findAccessibilityNodeInfosByViewId(
1193                         "android.accessibilityservice.cts:id/button")
1194                 .get(0);
1195         final String title = sInstrumentation.getContext().getString(R.string.alert_title);
1196 
1197         // Add a dialog that is watching outside touch
1198         sUiAutomation.executeAndWaitForEvent(
1199                 () -> sInstrumentation.runOnMainSync(() -> {
1200                             final AlertDialog dialog = new AlertDialog.Builder(mActivity)
1201                                     .setTitle(R.string.alert_title)
1202                                     .setMessage(R.string.alert_message)
1203                                     .create();
1204                             final Window window = dialog.getWindow();
1205                             window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
1206                                     | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
1207                             window.getDecorView().setOnTouchListener(listener);
1208                             window.setTitle(title);
1209                             dialog.show();
1210                     }),
1211                 (event) -> {
1212                     // Ensure the dialog is shown over the activity
1213                     final AccessibilityWindowInfo dialog = findWindowByTitle(
1214                             sUiAutomation, title);
1215                     final AccessibilityWindowInfo activity = findWindowByTitle(
1216                             sUiAutomation, getActivityTitle(sInstrumentation, mActivity));
1217                     return (dialog != null && activity != null)
1218                             && (dialog.getLayer() > activity.getLayer());
1219                 }, DEFAULT_TIMEOUT_MS);
1220 
1221         // Perform an action and wait for an event
1222         sUiAutomation.executeAndWaitForEvent(
1223                 () -> button.performAction(AccessibilityNodeInfo.ACTION_CLICK),
1224                 filterForEventTypeWithAction(
1225                         AccessibilityEvent.TYPE_VIEW_CLICKED, AccessibilityNodeInfo.ACTION_CLICK),
1226                 DEFAULT_TIMEOUT_MS);
1227 
1228         // Make sure the MotionEvent.ACTION_OUTSIDE is received.
1229         verify(listener, timeout(DEFAULT_TIMEOUT_MS).atLeastOnce()).onTouch(any(View.class),
1230                 argThat(event -> event.getActionMasked() == MotionEvent.ACTION_OUTSIDE));
1231     }
1232 
1233     @MediumTest
1234     @Test
1235     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTouchDelegateInfo"})
testTouchDelegateInfoReportedToAccessibility()1236     public void testTouchDelegateInfoReportedToAccessibility() {
1237         final Button button = getOnMain(sInstrumentation, () -> mActivity.findViewById(
1238                 R.id.button));
1239         final View parent = (View) button.getParent();
1240         final Rect rect = new Rect();
1241         button.getHitRect(rect);
1242         parent.setTouchDelegate(new TouchDelegate(rect, button));
1243 
1244         final AccessibilityNodeInfo nodeInfo = sUiAutomation.getRootInActiveWindow()
1245                 .findAccessibilityNodeInfosByViewId(
1246                         "android.accessibilityservice.cts:id/buttonLayout")
1247                 .get(0);
1248         AccessibilityNodeInfo.TouchDelegateInfo targetMapInfo =
1249                 nodeInfo.getTouchDelegateInfo();
1250         assertNotNull("Did not receive TouchDelegate target map", targetMapInfo);
1251         assertEquals("Incorrect target map size", 1, targetMapInfo.getRegionCount());
1252         assertEquals("Incorrect target map region", new Region(rect),
1253                 targetMapInfo.getRegionAt(0));
1254         final AccessibilityNodeInfo node = targetMapInfo.getTargetForRegion(
1255                 targetMapInfo.getRegionAt(0));
1256         assertEquals("Incorrect target map view",
1257                 "android.accessibilityservice.cts:id/button",
1258                 node.getViewIdResourceName());
1259         node.recycle();
1260     }
1261 
1262     @MediumTest
1263     @Test
1264     @ApiTest(apis = {"android.view.View#onHoverEvent",
1265             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTouchDelegateWithEbtBetweenView_ReHoverDelegate_FocusTargetAgain()1266     public void testTouchDelegateWithEbtBetweenView_ReHoverDelegate_FocusTargetAgain()
1267             throws Throwable {
1268         mActivity.waitForEnterAnimationComplete();
1269 
1270         // Layout. The LinearLayout has a touch delegate that covers the button's area extended to
1271         // the right button (x's in the diagram)
1272         //      ++++++++++++++++++++++++++++++++++++++++++++++++++ LinearLayout
1273         //      +   |--------------------| |----------------------- | +
1274         //      +   |xxxxxxxxxxxxxxxxxxxx|x|xxxx                    | +
1275         //      +   |x                   | |   x  buttonWithTooltip  | +
1276         //      +   |x       button      | | A x                     | +
1277         //      +   |xxxxxxxxxxxxxxxxxxxx|x|xxxx                     | +
1278         //      +   |--------------------| |----------------------- | +
1279         //      +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1280         final Resources resources = sInstrumentation.getTargetContext().getResources();
1281         final String buttonResourceName = resources.getResourceName(R.id.button);
1282         final Button button = mActivity.findViewById(R.id.button);
1283         final int[] buttonLocation = new int[2];
1284         button.getLocationOnScreen(buttonLocation);
1285         final int buttonX = button.getWidth() / 2;
1286         final int buttonY = button.getHeight() / 2;
1287         final int hoverY = buttonLocation[1] + buttonY;
1288         final Button buttonWithTooltip = mActivity.findViewById(R.id.buttonWithTooltip);
1289         final int[] buttonWithTooltipLocation = new int[2];
1290         buttonWithTooltip.getLocationOnScreen(buttonWithTooltipLocation);
1291         final int touchableSize = resources.getDimensionPixelSize(
1292                 R.dimen.button_touchable_width_increment_amount);
1293         final int hoverRight = buttonWithTooltipLocation[0] + touchableSize / 2;
1294         final int hoverLeft = buttonLocation[0] + button.getWidth() + touchableSize / 2;
1295         final int hoverMiddle = (hoverLeft + hoverRight) / 2;
1296         final View.OnHoverListener listener = CtsMouseUtil.installHoverListener(button, false);
1297         enableTouchExploration(true);
1298 
1299         try {
1300             // common downTime for touch explorer injected events
1301             final long downTime = SystemClock.uptimeMillis();
1302             // hover through delegate, parent, 2nd view, parent and delegate again
1303             // MOVE event at point A. We should delegate to button
1304             sUiAutomation.executeAndWaitForEvent(
1305                     () -> injectHoverEvent(downTime, false, hoverLeft, hoverY),
1306                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1307                             buttonResourceName), DEFAULT_TIMEOUT_MS);
1308             assertTrue(button.isHovered());
1309             sUiAutomation.executeAndWaitForEvent(
1310                     () -> {
1311                         injectHoverEvent(downTime, true, hoverMiddle, hoverY);
1312                         injectHoverEvent(downTime, true, hoverRight, hoverY);
1313                         injectHoverEvent(downTime, true, hoverMiddle, hoverY);
1314                         injectHoverEvent(downTime, true, hoverLeft, hoverY);
1315                     },
1316                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1317                             buttonResourceName), DEFAULT_TIMEOUT_MS);
1318             // delegate target has a11y focus again
1319             assertTrue(button.isHovered());
1320 
1321             CtsMouseUtil.clearHoverListener(button);
1322             View.OnHoverListener verifier = inOrder(listener).verify(listener);
1323             verifier.onHover(eq(button),
1324                     matchHover(MotionEvent.ACTION_HOVER_ENTER, buttonX, buttonY));
1325             verifier.onHover(eq(button),
1326                     matchHover(MotionEvent.ACTION_HOVER_MOVE, buttonX, buttonY));
1327             verifier.onHover(eq(button),
1328                     matchHover(MotionEvent.ACTION_HOVER_MOVE, hoverMiddle, buttonY));
1329             verifier.onHover(eq(button),
1330                     matchHover(MotionEvent.ACTION_HOVER_EXIT, buttonX, buttonY));
1331             verifier.onHover(eq(button),
1332                     matchHover(MotionEvent.ACTION_HOVER_ENTER, buttonX, buttonY));
1333             verifier.onHover(eq(button),
1334                     matchHover(MotionEvent.ACTION_HOVER_MOVE, buttonX, buttonY));
1335         } catch (TimeoutException e) {
1336             fail("Accessibility events should be received as expected " + e.getMessage());
1337         } finally {
1338             injectHoverExit(SystemClock.uptimeMillis(), hoverLeft, hoverY);
1339             enableTouchExploration(false);
1340         }
1341     }
1342 
1343     @Test
1344     @RequiresFlagsEnabled(Flags.FLAG_REMOVE_CHILD_HOVER_CHECK_FOR_TOUCH_EXPLORATION)
testTouchDelegate_ancestorHasTouchDelegate_sendsEventToDelegate()1345     public void testTouchDelegate_ancestorHasTouchDelegate_sendsEventToDelegate()
1346             throws Exception {
1347         mActivity.waitForEnterAnimationComplete();
1348 
1349         // Layout. buttonTargetGrandparent has a touch delegate that covers the buttonTarget and
1350         // some area to the right of buttonTarget. buttonTargetParent has the same bounds as
1351         // buttonTargetGrandparent
1352         //      ++++++++++++++++++++++++++++++++++++++++++++++++++ buttonTargetGrandparent
1353         //      + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +
1354         //      + x   buttonTargetParent                        x +
1355         //      + x  _______________                            x +
1356         //      + x | buttonTarget  |                           x +
1357         //      + x |_______________|                           x +
1358         //      + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +
1359         //      +++++++++++++++++++++++++++++++++++++++++++++++++++
1360 
1361         final Resources resources = sInstrumentation.getTargetContext().getResources();
1362         final String buttonResourceName = resources.getResourceName(R.id.buttonTarget);
1363         final Button buttonTarget = mActivity.findViewById(R.id.buttonTarget);
1364         onView(withId(R.id.buttonTarget)).perform(scrollTo());
1365         sUiAutomation.waitForIdle(
1366                 /* idleTimeoutMillis= */ 100, /* globalTimeoutMillis= */ DEFAULT_TIMEOUT_MS);
1367 
1368         final int[] buttonLocation = new int[2];
1369         buttonTarget.getLocationOnScreen(buttonLocation);
1370         final int buttonY = buttonTarget.getHeight() / 2;
1371         final int hoverY = buttonLocation[1] + buttonY;
1372         final int touchableSize = resources.getDimensionPixelSize(
1373                 R.dimen.button_touchable_width_increment_amount);
1374         final int hoverLeft = buttonLocation[0] + buttonTarget.getWidth() + touchableSize / 2;
1375         enableTouchExploration(true);
1376 
1377         try {
1378             final long downTime = SystemClock.uptimeMillis();
1379             sUiAutomation.executeAndWaitForEvent(
1380                     () -> injectHoverEvent(downTime, false, hoverLeft, hoverY),
1381                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1382                             buttonResourceName), DEFAULT_TIMEOUT_MS);
1383         } catch (TimeoutException e) {
1384             fail("TYPE_VIEW_HOVER_ENTER from buttonTarget should be received as expected "
1385                     + e.getMessage());
1386         } finally {
1387             injectHoverExit(SystemClock.uptimeMillis(), hoverLeft, hoverY);
1388             enableTouchExploration(false);
1389         }
1390     }
1391 
1392     @Test
1393     @RequiresFlagsDisabled(Flags.FLAG_REMOVE_CHILD_HOVER_CHECK_FOR_TOUCH_EXPLORATION)
testTouchDelegate_ancestorHasTouchDelegate_doesNotSendEventToDelegate()1394     public void testTouchDelegate_ancestorHasTouchDelegate_doesNotSendEventToDelegate()
1395             throws Exception {
1396         mActivity.waitForEnterAnimationComplete();
1397 
1398         final Resources resources = sInstrumentation.getTargetContext().getResources();
1399         final String buttonResourceName = resources.getResourceName(R.id.buttonTarget);
1400         final Button buttonTarget = mActivity.findViewById(R.id.buttonTarget);
1401         onView(withId(R.id.buttonTarget)).perform(scrollTo());
1402         sUiAutomation.waitForIdle(
1403                 /* idleTimeoutMillis= */ 100, /* globalTimeoutMillis= */ DEFAULT_TIMEOUT_MS);
1404 
1405         final int[] buttonLocation = new int[2];
1406         buttonTarget.getLocationOnScreen(buttonLocation);
1407         final int buttonY = buttonTarget.getHeight() / 2;
1408         final int hoverY = buttonLocation[1] + buttonY;
1409         final int touchableSize = resources.getDimensionPixelSize(
1410                 R.dimen.button_touchable_width_increment_amount);
1411         final int hoverLeft = buttonLocation[0] + buttonTarget.getWidth() + touchableSize / 2;
1412         enableTouchExploration(true);
1413 
1414         try {
1415             final long downTime = SystemClock.uptimeMillis();
1416             assertThrows("Received TYPE_HOVER_ENTER from target view.",
1417                     TimeoutException.class,
1418                     () ->   sUiAutomation.executeAndWaitForEvent(
1419                             () -> injectHoverEvent(downTime, false, hoverLeft, hoverY),
1420                             filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1421                                     buttonResourceName), DEFAULT_TIMEOUT_MS));
1422         } finally {
1423             injectHoverExit(SystemClock.uptimeMillis(), hoverLeft, hoverY);
1424             enableTouchExploration(false);
1425         }
1426     }
1427 
1428     @Test
1429     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_nodeMatchesViewProperty()1430     public void testAccessibilityDataSensitive_nodeMatchesViewProperty() {
1431         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(true);
1432         try {
1433             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1434 
1435             final AccessibilityNodeInfo nonAdsNode = root.findAccessibilityNodeInfosByViewId(
1436                     mActivity.getResources().getResourceName(R.id.containerView)).get(0);
1437             final AccessibilityNodeInfo adsNode = root.findAccessibilityNodeInfosByViewId(
1438                     mActivity.getResources().getResourceName(R.id.adsView)).get(0);
1439 
1440             assertThat(nonAdsNode.isAccessibilityDataSensitive()).isFalse();
1441             assertThat(adsNode.isAccessibilityDataSensitive()).isTrue();
1442         } finally {
1443             service.disableSelfAndRemove();
1444         }
1445     }
1446 
1447     @Test
1448     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_visibleToAccessibilityTool()1449     public void testAccessibilityDataSensitive_visibleToAccessibilityTool() throws Throwable {
1450         // Relevant view structure:
1451         //   containerView (LinearLayout, accessibilityDataSensitive=auto)
1452         //     adsView (LinearLayout, accessibilityDataSensitive=true)
1453         //       innerContainerView (LinearLayout, accessibilityDataSensitive=auto)
1454         //         innerView (Button, accessibilityDataSensitive=auto)
1455         // Only adsView sets accessibilityDataSensitive=true in the layout XML.
1456         // Inner views should inherit true from their (grand)parent view.
1457         final StubEventCapturingAccessibilityService service = getServiceForA11yToolTests(true);
1458         try {
1459             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1460 
1461             final String containerViewName = mActivity.getResources().getResourceName(
1462                     R.id.containerView);
1463 
1464             final String adsViewName = mActivity.getResources().getResourceName(R.id.adsView);
1465             final String adsViewText = mActivity.findViewById(
1466                     R.id.adsView).getContentDescription().toString();
1467 
1468             final String innerContainerViewName = mActivity.getResources().getResourceName(
1469                     R.id.innerContainerView);
1470             final String innerContainerViewText =
1471                     mActivity.findViewById(
1472                             R.id.innerContainerView).getContentDescription().toString();
1473 
1474             final String innerViewName = mActivity.getResources().getResourceName(R.id.innerView);
1475             final String innerViewText = mActivity.findViewById(
1476                     R.id.innerView).getContentDescription().toString();
1477 
1478             // Search for the Views' nodes using various techniques:
1479 
1480             // ByViewId
1481             assertThat(root.findAccessibilityNodeInfosByViewId(adsViewName)).hasSize(1);
1482             assertThat(root.findAccessibilityNodeInfosByViewId(innerContainerViewName)).hasSize(1);
1483             assertThat(root.findAccessibilityNodeInfosByViewId(innerViewName)).hasSize(1);
1484             // ByText
1485             assertThat(root.findAccessibilityNodeInfosByText(adsViewText)).hasSize(1);
1486             assertThat(root.findAccessibilityNodeInfosByText(innerContainerViewText)).hasSize(1);
1487             assertThat(root.findAccessibilityNodeInfosByText(innerViewText)).hasSize(1);
1488             // Event propagation and findFocus
1489             service.setEventFilter(
1490                     filterForEventTypeWithResource(TYPE_VIEW_ACCESSIBILITY_FOCUSED, adsViewName));
1491             assertThat(root.findAccessibilityNodeInfosByViewId(adsViewName).get(0)
1492                     .performAction(ACTION_ACCESSIBILITY_FOCUS)).isTrue();
1493             service.waitOnEvent(DEFAULT_TIMEOUT_MS,
1494                     "Expected TYPE_VIEW_ACCESSIBILITY_FOCUSED event");
1495             assertThat(service.findFocus(
1496                     AccessibilityNodeInfo.FOCUS_ACCESSIBILITY).getContentDescription()).isEqualTo(
1497                     adsViewText);
1498             // Parent view's getChild()
1499             final AccessibilityNodeInfo parent = root.findAccessibilityNodeInfosByViewId(
1500                     containerViewName).get(0);
1501             assertThat(parent.getChildCount()).isEqualTo(1);
1502             assertThat(parent.getChild(0)).isNotNull();
1503         } finally {
1504             service.disableSelfAndRemove();
1505         }
1506     }
1507 
1508     @Test
1509     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_canObserveHoverEvent()1510     public void testAccessibilityDataSensitive_canObserveHoverEvent() {
1511         final StubEventCapturingAccessibilityService service = getServiceForA11yToolTests(true);
1512         final long time = SystemClock.uptimeMillis();
1513         final View view = mActivity.findViewById(R.id.innerView);
1514         final int[] viewLocation = new int[2];
1515         view.getLocationOnScreen(viewLocation);
1516         final int x = viewLocation[0] + view.getWidth() / 2;
1517         final int y = viewLocation[1] + view.getHeight() / 2;
1518         try {
1519             service.setEventFilter(
1520                     filterForEventTypeWithResource(
1521                             AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1522                             sInstrumentation.getTargetContext().getResources()
1523                                     .getResourceName(R.id.innerView)));
1524             injectHoverEvent(time, true, x, y);
1525             service.waitOnEvent(DEFAULT_TIMEOUT_MS, "Expected TYPE_VIEW_HOVER_ENTER event");
1526         } finally {
1527             injectHoverExit(SystemClock.uptimeMillis(), x, y);
1528             service.disableSelfAndRemove();
1529         }
1530     }
1531 
1532     @Test
1533     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_checkAdsProperty_topDown()1534     public void testAccessibilityDataSensitive_checkAdsProperty_topDown() {
1535         // Accessing the View#isAccessibilityDataSensitive() property causes both the View & its
1536         // parent hierarchy to cache their values.
1537         // Assert that the property is as expected when starting from the top-most view.
1538         assertThat(mActivity.findViewById(R.id.containerView).isAccessibilityDataSensitive())
1539                 .isFalse();
1540         assertThat(mActivity.findViewById(R.id.adsView).isAccessibilityDataSensitive()).isTrue();
1541         assertThat(mActivity.findViewById(R.id.innerContainerView).isAccessibilityDataSensitive())
1542                 .isTrue();
1543         assertThat(mActivity.findViewById(R.id.innerView).isAccessibilityDataSensitive()).isTrue();
1544     }
1545 
1546     @Test
1547     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_checkAdsProperty_bottomUp()1548     public void testAccessibilityDataSensitive_checkAdsProperty_bottomUp() {
1549         // Accessing the View#isAccessibilityDataSensitive() property causes both the View & its
1550         // parent hierarchy to cache their values.
1551         // Assert that the property is as expected when starting from the bottom-most view.
1552         assertThat(mActivity.findViewById(R.id.innerView).isAccessibilityDataSensitive()).isTrue();
1553         assertThat(mActivity.findViewById(R.id.innerContainerView).isAccessibilityDataSensitive())
1554                 .isTrue();
1555         assertThat(mActivity.findViewById(R.id.adsView).isAccessibilityDataSensitive()).isTrue();
1556         assertThat(mActivity.findViewById(R.id.containerView).isAccessibilityDataSensitive())
1557                 .isFalse();
1558     }
1559 
1560     @Test
1561     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1562             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId",
1563             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByText",
1564             "android.view.accessibility.AccessibilityNodeInfo#getChild"})
testAccessibilityDataSensitive_hiddenFromSearches()1565     public void testAccessibilityDataSensitive_hiddenFromSearches() {
1566         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1567         try {
1568             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1569             final String adsViewName = mActivity.getResources().getResourceName(R.id.adsView);
1570             final String adsViewText = mActivity.getString(R.string.ads_desc);
1571 
1572             assertThat(root.findAccessibilityNodeInfosByViewId(adsViewName)).isEmpty();
1573             assertThat(root.findAccessibilityNodeInfosByText(adsViewText)).isEmpty();
1574             Deque<AccessibilityNodeInfo> deque = new ArrayDeque<>();
1575             deque.add(root);
1576             while (!deque.isEmpty()) {
1577                 AccessibilityNodeInfo node = deque.removeFirst();
1578                 assertThat(node.getContentDescription()).isNotEqualTo(adsViewText);
1579                 for (int i = node.getChildCount() - 1; i >= 0; i--) {
1580                     deque.addLast(node.getChild(i));
1581                 }
1582             }
1583         } finally {
1584             service.disableSelfAndRemove();
1585         }
1586     }
1587 
1588     @Test
1589     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1590             "android.accessibilityservice.AccessibilityService#findFocus"})
testAccessibilityDataSensitive_hiddenFromFindFocus()1591     public void testAccessibilityDataSensitive_hiddenFromFindFocus() {
1592         StubEventCapturingAccessibilityService toolService = null;
1593         InstrumentedAccessibilityService nonToolService = null;
1594         try {
1595             toolService = getServiceForA11yToolTests(true);
1596             nonToolService = getServiceForA11yToolTests(false);
1597 
1598             // Set up initial focus on the ADS view.
1599             toolService.setEventFilter(filterForEventType(TYPE_VIEW_ACCESSIBILITY_FOCUSED));
1600             assertThat(mActivity.findViewById(R.id.adsView).performAccessibilityAction(
1601                     ACTION_ACCESSIBILITY_FOCUS, null)).isTrue();
1602             toolService.waitOnEvent(DEFAULT_TIMEOUT_MS,
1603                     "Expected TYPE_VIEW_ACCESSIBILITY_FOCUSED event");
1604 
1605             assertThat(toolService.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY))
1606                     .isNotNull();
1607             assertThat(nonToolService.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY))
1608                     .isNull();
1609         } finally {
1610             if (toolService != null) {
1611                 toolService.disableSelfAndRemove();
1612             }
1613             if (nonToolService != null) {
1614                 nonToolService.disableSelfAndRemove();
1615             }
1616         }
1617     }
1618 
1619     @Test
1620     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
1621     @RequiresFlagsEnabled(FLAG_PREVENT_A11Y_NONTOOL_FROM_INJECTING_INTO_SENSITIVE_VIEWS)
testAccessibilityDataSensitive_observesGesturesFromTool()1622     public void testAccessibilityDataSensitive_observesGesturesFromTool() {
1623         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(true);
1624         try {
1625             AccessibilityServiceInfo info = service.getServiceInfo();
1626             info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
1627             service.setServiceInfo(info);
1628 
1629             final View adsView = mActivity.findViewById(R.id.innerView);
1630             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1631 
1632             dispatchAndAwaitTouchOnView(adsView, service);
1633         } finally {
1634             service.disableSelfAndRemove();
1635         }
1636     }
1637 
1638     @Test
1639     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
1640     @RequiresFlagsEnabled(FLAG_PREVENT_A11Y_NONTOOL_FROM_INJECTING_INTO_SENSITIVE_VIEWS)
testAccessibilityDataSensitive_hiddenFromGesturesFromNonTool()1641     public void testAccessibilityDataSensitive_hiddenFromGesturesFromNonTool() {
1642         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1643         try {
1644             AccessibilityServiceInfo info = service.getServiceInfo();
1645             info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
1646             service.setServiceInfo(info);
1647 
1648             final View adsView = mActivity.findViewById(R.id.innerView);
1649             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1650 
1651             assertThrows(AssertionError.class, () -> dispatchAndAwaitTouchOnView(adsView, service));
1652         } finally {
1653             service.disableSelfAndRemove();
1654         }
1655     }
1656 
dispatchAndAwaitTouchOnView(View view, InstrumentedAccessibilityService service)1657     private void dispatchAndAwaitTouchOnView(View view, InstrumentedAccessibilityService service) {
1658         final Object waitLock = new Object();
1659         final AtomicBoolean touched = new AtomicBoolean(false);
1660         final int[] location = new int[2];
1661         view.getLocationOnScreen(location);
1662         location[0] += view.getWidth() / 2;
1663         location[1] += view.getHeight() / 2;
1664         view.setOnTouchListener(
1665                 (v, event) -> {
1666                     synchronized (waitLock) {
1667                         touched.set(true);
1668                         waitLock.notifyAll();
1669                     }
1670                     return false;
1671                 });
1672 
1673         awaitDispatchGesture(service, null, click(new PointF(location[0], location[1])));
1674         TestUtils.waitOn(waitLock, touched::get, DEFAULT_TIMEOUT_MS, "Expected touch");
1675     }
1676 
1677     @Test
1678     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1679             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId",
1680             "android.view.accessibility.AccessibilityNodeInfo#getChild"})
testAccessibilityDataSensitive_excludedFromParent()1681     public void testAccessibilityDataSensitive_excludedFromParent() {
1682         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1683         try {
1684             final AccessibilityNodeInfo parentContainer =
1685                     service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1686                             mActivity.getResources().getResourceName(R.id.containerView)).get(0);
1687 
1688             assertThat(parentContainer.getChildCount()).isEqualTo(0);
1689             assertThat(parentContainer.getChild(0)).isNull();
1690         } finally {
1691             service.disableSelfAndRemove();
1692         }
1693     }
1694 
1695     @Test
1696     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1697             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId"})
testAccessibilityDataSensitive_innerChildHidden()1698     public void testAccessibilityDataSensitive_innerChildHidden() {
1699         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1700 
1701         try {
1702             assertThat(service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1703                     mActivity.getResources().getResourceName(R.id.innerView))).isEmpty();
1704         } finally {
1705             service.disableSelfAndRemove();
1706         }
1707     }
1708 
1709     @Test
1710     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1711             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testAccessibilityDataSensitive_hiddenFromEventPropagation()1712     public void testAccessibilityDataSensitive_hiddenFromEventPropagation() {
1713         final StubEventCapturingAccessibilityService service = getServiceForA11yToolTests(false);
1714         try {
1715             final View innerView = mActivity.findViewById(R.id.innerView);
1716             innerView.setOnClickListener(v -> {
1717                 // empty, but necessary for performClick to return true
1718             });
1719             assertTrue(innerView.isAccessibilityDataSensitive());
1720             assertTrue(innerView.isClickable());
1721 
1722             service.setEventFilter(filterForEventType(TYPE_VIEW_CLICKED));
1723             sInstrumentation.runOnMainSync(() -> assertThat(innerView.performClick()).isTrue());
1724             assertThrows("Received TYPE_VIEW_CLICKED event from accessibilityDataSensitive view.",
1725                     AssertionError.class,
1726                     () -> service.waitOnEvent(DEFAULT_TIMEOUT_MS, "(expected to timeout)"));
1727         } finally {
1728             service.disableSelfAndRemove();
1729         }
1730     }
1731 
1732     @Test
1733     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_hiddenIfFilterTouchesWhenObscured()1734     public void testAccessibilityDataSensitive_hiddenIfFilterTouchesWhenObscured() {
1735         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1736         try {
1737             View containerView = mActivity.findViewById(R.id.containerView);
1738             assertThat(containerView.isAccessibilityDataSensitive()).isFalse();
1739             assertThat(containerView.getFilterTouchesWhenObscured()).isFalse();
1740 
1741             mActivity.findViewById(R.id.containerView).setFilterTouchesWhenObscured(true);
1742 
1743             assertThat(containerView.isAccessibilityDataSensitive()).isTrue();
1744             assertThat(service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1745                     mActivity.getResources().getResourceName(R.id.containerView))).isEmpty();
1746         } finally {
1747             service.disableSelfAndRemove();
1748         }
1749     }
1750 
1751     @Test
1752     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1753             "android.view.View#setAccessibilityDataSensitive"})
testAccessibilityDataSensitive_changingValueUpdatesChildren_noFirst()1754     public void testAccessibilityDataSensitive_changingValueUpdatesChildren_noFirst() {
1755         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1756         try {
1757             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1758             // The view starts as ADS=true as defined in the XML.
1759             View adsView = mActivity.findViewById(R.id.adsView);
1760             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1761 
1762             // Set to NO, ensure we can find this view & all (grand)children.
1763             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_NO);
1764             assertThat(adsView.isAccessibilityDataSensitive()).isFalse();
1765             assertThat(root.findAccessibilityNodeInfosByViewId(
1766                     mActivity.getResources().getResourceName(R.id.adsView))).isNotEmpty();
1767             assertThat(root.findAccessibilityNodeInfosByViewId(
1768                     mActivity.getResources().getResourceName(
1769                             R.id.innerContainerView))).isNotEmpty();
1770             assertThat(root.findAccessibilityNodeInfosByViewId(
1771                     mActivity.getResources().getResourceName(R.id.innerView))).isNotEmpty();
1772 
1773             // Set back to YES, ensure this view & all (grand)children are hidden.
1774             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES);
1775             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1776             assertThat(root.findAccessibilityNodeInfosByViewId(
1777                     mActivity.getResources().getResourceName(R.id.adsView))).isEmpty();
1778             assertThat(root.findAccessibilityNodeInfosByViewId(
1779                     mActivity.getResources().getResourceName(R.id.innerContainerView))).isEmpty();
1780             assertThat(root.findAccessibilityNodeInfosByViewId(
1781                     mActivity.getResources().getResourceName(R.id.innerView))).isEmpty();
1782         } finally {
1783             service.disableSelfAndRemove();
1784         }
1785     }
1786 
1787     @Test
1788     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1789             "android.view.View#setAccessibilityDataSensitive"})
testAccessibilityDataSensitive_changingValueUpdatesChildren_yesFirst()1790     public void testAccessibilityDataSensitive_changingValueUpdatesChildren_yesFirst() {
1791         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1792         try {
1793             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1794             // The view starts as AccessibilityDataSensitive=true as defined in the XML.
1795             View adsView = mActivity.findViewById(R.id.adsView);
1796             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1797 
1798             // Explicitly set to YES, ensure this view & all (grand)children are hidden.
1799             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES);
1800             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1801             assertThat(root.findAccessibilityNodeInfosByViewId(
1802                     mActivity.getResources().getResourceName(R.id.adsView))).isEmpty();
1803             assertThat(root.findAccessibilityNodeInfosByViewId(
1804                     mActivity.getResources().getResourceName(R.id.innerContainerView))).isEmpty();
1805             assertThat(root.findAccessibilityNodeInfosByViewId(
1806                     mActivity.getResources().getResourceName(R.id.innerView))).isEmpty();
1807 
1808             // Set to NO, ensure we can find this view & all (grand)children.
1809             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_NO);
1810             assertThat(adsView.isAccessibilityDataSensitive()).isFalse();
1811             assertThat(root.findAccessibilityNodeInfosByViewId(
1812                     mActivity.getResources().getResourceName(R.id.adsView))).isNotEmpty();
1813             assertThat(root.findAccessibilityNodeInfosByViewId(
1814                     mActivity.getResources().getResourceName(
1815                             R.id.innerContainerView))).isNotEmpty();
1816             assertThat(root.findAccessibilityNodeInfosByViewId(
1817                     mActivity.getResources().getResourceName(R.id.innerView))).isNotEmpty();
1818         } finally {
1819             service.disableSelfAndRemove();
1820         }
1821     }
1822 
1823     @Test
1824     @ApiTest(apis = {
1825             "android.view.accessibility.AccessibilityManager#isRequestFromAccessibilityTool"})
testAccessibilityDataSensitive_requestIsFromAccessibilityTool_TrueForTool()1826     public void testAccessibilityDataSensitive_requestIsFromAccessibilityTool_TrueForTool() {
1827         checkIsRequestFromAccessibilityTool(true);
1828     }
1829 
1830     @Test
1831     @ApiTest(apis = {
1832             "android.view.accessibility.AccessibilityManager#isRequestFromAccessibilityTool"})
testAccessibilityDataSensitive_requestIsFromAccessibilityTool_FalseForNonTool()1833     public void testAccessibilityDataSensitive_requestIsFromAccessibilityTool_FalseForNonTool() {
1834         checkIsRequestFromAccessibilityTool(false);
1835     }
1836 
checkIsRequestFromAccessibilityTool(boolean serviceIsAccessibilityTool)1837     private void checkIsRequestFromAccessibilityTool(boolean serviceIsAccessibilityTool) {
1838         final InstrumentedAccessibilityService service =
1839             getServiceForA11yToolTests(serviceIsAccessibilityTool);
1840         try {
1841             final View view = mActivity.findViewById(R.id.listview);
1842             final String viewId = mActivity.getResources().getResourceName(R.id.listview);
1843             final AccessibilityManager accessibilityManager =
1844                     (AccessibilityManager) sInstrumentation.getContext().getSystemService(
1845                             Service.ACCESSIBILITY_SERVICE);
1846 
1847             final Object waitLock = new Object();
1848             final AtomicReference<Boolean> fromTool = new AtomicReference<>();
1849             view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
1850                 @Override
1851                 public void onInitializeAccessibilityNodeInfo(View host,
1852                         AccessibilityNodeInfo info) {
1853                     super.onInitializeAccessibilityNodeInfo(host, info);
1854                     synchronized (waitLock) {
1855                         fromTool.set(accessibilityManager.isRequestFromAccessibilityTool());
1856                         waitLock.notifyAll();
1857                     }
1858                 }
1859             });
1860 
1861             // Trigger node creation from the service-under-test.
1862             service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(viewId);
1863 
1864             TestUtils.waitOn(waitLock,
1865                     () -> fromTool.get() != null && fromTool.get() == serviceIsAccessibilityTool,
1866                     DEFAULT_TIMEOUT_MS,
1867                     "Expected isRequestFromAccessibilityTool to be "
1868                         + serviceIsAccessibilityTool);
1869         } finally {
1870             service.disableSelfAndRemove();
1871         }
1872     }
1873 
1874     @Test
1875     @ApiTest(apis = {
1876             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_NavigateHierarchy()1877     public void testDirectAccessibilityConnection_NavigateHierarchy() throws Throwable {
1878         View layoutView = mActivity.findViewById(R.id.buttonLayout);
1879         AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
1880 
1881         assertThat(layoutNode).isNotNull();
1882         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), true);
1883 
1884         // Access this node's children.
1885         assertThat(layoutNode.getChildCount()).isGreaterThan(0);
1886         for (int i = layoutNode.getChildCount() - 1; i >= 0; i--) {
1887             assertThat(layoutNode.getChild(i)).isNotNull();
1888         }
1889 
1890         // Find the root node by accessing parents going up the hierarchy.
1891         AccessibilityNodeInfo rootNode = layoutNode;
1892         while (rootNode.getParent() != null) {
1893             rootNode = rootNode.getParent();
1894         }
1895         assertThat(rootNode).isEqualTo(layoutView.getRootView().createAccessibilityNodeInfo());
1896 
1897         // Find more nodes, starting from the root.
1898         assertThat(rootNode.findAccessibilityNodeInfosByViewId(
1899                 "android.accessibilityservice.cts:id/button")).isNotEmpty();
1900         assertThat(rootNode.findAccessibilityNodeInfosByText(
1901                 mActivity.getString(R.string.button_title))).isNotEmpty();
1902 
1903         // Find and search the focus.
1904         try {
1905             // Enable touch exploration, needed for performAction(ACTION_ACCESSIBILITY_FOCUS).
1906             enableTouchExploration(true);
1907             final AccessibilityNodeInfo buttonNode = rootNode.findAccessibilityNodeInfosByViewId(
1908                     "android.accessibilityservice.cts:id/button").get(0);
1909             sUiAutomation.executeAndWaitForEvent(
1910                     () -> assertTrue(
1911                             buttonNode.performAction(
1912                                     AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)),
1913                     filterForEventType(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED),
1914                     DEFAULT_TIMEOUT_MS);
1915             assertThat(rootNode.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)).isEqualTo(
1916                     buttonNode);
1917             assertThat(rootNode.focusSearch(View.FOCUS_FORWARD)).isNotNull();
1918         } finally {
1919             enableTouchExploration(false);
1920         }
1921     }
1922 
1923     @Test
1924     @ApiTest(apis = {
1925             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_CanPerformAction()1926     public void testDirectAccessibilityConnection_CanPerformAction() {
1927         View button = mActivity.findViewById(R.id.button);
1928         AtomicBoolean clicked = new AtomicBoolean(false);
1929         button.setOnClickListener((view) -> clicked.set(true));
1930         AccessibilityNodeInfo buttonNode = button.createAccessibilityNodeInfo();
1931 
1932         assertThat(buttonNode).isNotNull();
1933         buttonNode.setQueryFromAppProcessEnabled(button.getRootView(), true);
1934 
1935         assertThat(buttonNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)).isTrue();
1936         assertThat(clicked.get()).isTrue();
1937     }
1938 
1939     @Test
1940     @ApiTest(apis = {
1941             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_CanDisable()1942     public void testDirectAccessibilityConnection_CanDisable() {
1943         View layoutView = mActivity.findViewById(R.id.buttonLayout);
1944         AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
1945         assertThat(layoutNode).isNotNull();
1946 
1947         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), true);
1948         assertThat(layoutNode.getParent()).isNotNull();
1949 
1950         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), false);
1951         try {
1952             layoutNode.getParent();
1953             fail("Should not be able to navigate node tree on node without any connection.");
1954         } catch (IllegalStateException e) {
1955             // expected due to undefined connection ID
1956         }
1957     }
1958 
1959     @Test
1960     @ApiTest(apis = {
1961             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_AccessibilityManagerEnabled()1962     public void testDirectAccessibilityConnection_AccessibilityManagerEnabled() {
1963         // Note: this test checks AM#hasAnyDirectConnection() as a proxy for #isEnabled because
1964         // #isEnabled is also modified by the UiAutomation used in this test.
1965 
1966         View layoutView = mActivity.findViewById(R.id.buttonLayout);
1967         AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
1968         final AccessibilityManager accessibilityManager =
1969                 (AccessibilityManager) sInstrumentation.getContext().getSystemService(
1970                         Service.ACCESSIBILITY_SERVICE);
1971 
1972         // Ensure no DirectConnection to start.
1973         assertThat(accessibilityManager.hasAnyDirectConnection()).isFalse();
1974 
1975         // Enable app-process querying, which adds a connection for this node.
1976         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), true);
1977         assertThat(accessibilityManager.hasAnyDirectConnection()).isTrue();
1978 
1979         // Disable app-process querying for this node.
1980         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), false);
1981         // The connection should still exist until ViewRootImpl detaches from the window, in case
1982         // other nodes in this view hierarchy use the connection.
1983         assertThat(accessibilityManager.hasAnyDirectConnection()).isTrue();
1984 
1985         // Detach the ViewRootImpl from the window by finishing the activity, then wait for the
1986         // change notification that comes from ViewRootImpl itself, after which the connection
1987         // should now be gone.
1988         final Object waitLock = new Object();
1989         final AtomicBoolean hasAnyDirectConnection = new AtomicBoolean(true);
1990         accessibilityManager.addAccessibilityStateChangeListener(
1991                 enabled -> {
1992                     synchronized (waitLock) {
1993                         hasAnyDirectConnection.set(accessibilityManager.hasAnyDirectConnection());
1994                         waitLock.notifyAll();
1995                     }
1996                 });
1997         mActivity.runOnUiThread(() -> mActivity.finish());
1998         TestUtils.waitOn(waitLock, () -> !hasAnyDirectConnection.get(), DEFAULT_TIMEOUT_MS,
1999                 "AccessibilityManager#hasAnyDirectConnection() still true");
2000     }
2001 
2002     @Test
2003     @ApiTest(apis = {
2004             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_UsesCurrentWindowSpec()2005     public void testDirectAccessibilityConnection_UsesCurrentWindowSpec() throws Throwable {
2006         if (isAutomotive(sInstrumentation.getTargetContext())) {
2007             Log.i(LOG_TAG, "Skipping: testDirectAccessibilityConnection_UsesCurrentWindowSpec"
2008                     + " - Automotive does not support magnification.");
2009             return;
2010         }
2011 
2012         // Store the initial bounds of the ANI.
2013         final View layoutView = mActivity.findViewById(R.id.buttonLayout);
2014         final AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
2015         final Rect initialBounds = new Rect();
2016         layoutNode.setQueryFromAppProcessEnabled(layoutView, true);
2017         layoutNode.getBoundsInScreen(initialBounds);
2018 
2019         // Magnify the screen.
2020         final StubMagnificationAccessibilityService service =
2021                 InstrumentedAccessibilityService.enableService(
2022                         StubMagnificationAccessibilityService.class);
2023         try {
2024             final MagnificationConfig magnificationConfig =
2025                     new MagnificationConfig.Builder().setMode(MAGNIFICATION_MODE_FULLSCREEN)
2026                             .setScale(2f).build();
2027             service.runOnServiceSync(
2028                     () -> service.getMagnificationController()
2029                             .setMagnificationConfig(magnificationConfig, false));
2030 
2031             // Check that the ANI bounds have changed.
2032             TestUtils.waitUntil("Failed to refresh node with updated boundsInScreen",
2033                     (int) DEFAULT_TIMEOUT_MS / 1000,
2034                     () -> {
2035                         final Rect boundsAfterMagnification = new Rect();
2036                         layoutNode.refresh();
2037                         layoutNode.getBoundsInScreen(boundsAfterMagnification);
2038                         return !boundsAfterMagnification.equals(initialBounds);
2039                     });
2040         } finally {
2041             service.disableSelfAndRemove();
2042         }
2043     }
2044 
2045     @Test
2046     @ApiTest(apis = {
2047             "android.view.accessibility.AccessibilityNodeInfo"
2048                     + "#setMinDurationBetweenContentChanges",
2049             "android.view.accessibility.AccessibilityNodeInfo"
2050                     + "#getMinDurationBetweenContentChanges"})
testSetMinDurationBetweenContentChanges()2051     public void testSetMinDurationBetweenContentChanges() {
2052         final View testView = mActivity.findViewById(R.id.buttonLayout);
2053         final AccessibilityNodeInfo nodeInfo = testView.createAccessibilityNodeInfo();
2054         nodeInfo.setMinDurationBetweenContentChanges(Duration.ofMillis(200));
2055         assertThat(nodeInfo.getMinDurationBetweenContentChanges().toMillis()).isEqualTo(200);
2056     }
2057 
2058     @Test
2059     @ApiTest(apis = {
2060             "android.view.accessibility.AccessibilityNodeInfo"
2061                     + "#setRequestInitialAccessibilityFocus",
2062             "android.view.accessibility.AccessibilityNodeInfo"
2063                     + "#hasRequestInitialAccessibilityFocus"})
testSetRequestInitialAccessibilityFocus()2064     public void testSetRequestInitialAccessibilityFocus() {
2065         final View testView = mActivity.findViewById(R.id.buttonLayout);
2066         final AccessibilityNodeInfo nodeInfo = testView.createAccessibilityNodeInfo();
2067         nodeInfo.setRequestInitialAccessibilityFocus(true);
2068         assertThat(nodeInfo.hasRequestInitialAccessibilityFocus()).isTrue();
2069     }
2070 
2071 
2072     @AsbSecurityTest(cveBugId = {243378132})
2073     @Test
testUninstallPackage_DisablesMultipleServices()2074     public void testUninstallPackage_DisablesMultipleServices() throws Exception {
2075         AccessibilityManager manager = mActivity.getSystemService(AccessibilityManager.class);
2076         final String apkPath =
2077                 "/data/local/tmp/cts/content/CtsAccessibilityMultipleServicesApp.apk";
2078         final String packageName = "foo.bar.multipleservices";
2079         final ComponentName service1 = ComponentName.createRelative(packageName, ".StubService1");
2080         final ComponentName service2 = ComponentName.createRelative(packageName, ".StubService2");
2081         // Match AccessibilityManagerService#COMPONENT_NAME_SEPARATOR
2082         final String componentNameSeparator = ":";
2083 
2084         final String originalEnabledServicesSetting = getEnabledServicesSetting();
2085 
2086         try {
2087             // Install the apk in this test method, instead of as part of the target preparer, to
2088             // allow repeated --iterations of the test.
2089             assertThat(SystemUtil.runShellCommand(sUiAutomation, "pm install " + apkPath))
2090                     .startsWith("Success");
2091             TestUtils.waitUntil(
2092                     "Failed to install services from " + apkPath,
2093                     (int) TIMEOUT_SERVICE_ENABLE / 1000,
2094                     () ->
2095                             manager.getInstalledAccessibilityServiceList().stream()
2096                                             .filter(info -> info.getId().startsWith(packageName))
2097                                             .count()
2098                                     == 2);
2099 
2100             // Enable the two services and wait until AccessibilityManager reports them as enabled.
2101             final String servicesToEnable = service1.flattenToShortString()
2102                     + componentNameSeparator + service2.flattenToShortString();
2103             ShellCommandBuilder.create(sUiAutomation)
2104                     .putSecureSetting(
2105                             Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, servicesToEnable)
2106                     .run();
2107             TestUtils.waitUntil("Failed to enable 2 services from package " + packageName,
2108                     (int) TIMEOUT_SERVICE_ENABLE / 1000,
2109                     () -> getEnabledServices().stream().filter(
2110                             info -> info.getId().startsWith(packageName)).count() == 2);
2111 
2112             // Uninstall the package that contains the services.
2113             assertThat(SystemUtil.runShellCommand(sUiAutomation, "pm uninstall " + packageName))
2114                     .startsWith("Success");
2115 
2116             // Ensure the uninstall removed the services from the secure setting.
2117             TestUtils.waitUntil(
2118                     "Failed to disable services after uninstalling package " + packageName,
2119                     (int) TIMEOUT_SERVICE_ENABLE / 1000,
2120                     () -> !getEnabledServicesSetting().contains(packageName));
2121         } finally {
2122             ShellCommandBuilder.create(sUiAutomation)
2123                     .putSecureSetting(
2124                             Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
2125                             originalEnabledServicesSetting)
2126                     .run();
2127             SystemUtil.runShellCommand(sUiAutomation, "pm uninstall " + packageName);
2128         }
2129     }
2130 
2131     @AsbSecurityTest(cveBugId = {282016107})
2132     @AppModeFull
2133     @Test
testInstallAppWithLargeServiceVolume_displaysServicesSuccessfully()2134     public void testInstallAppWithLargeServiceVolume_displaysServicesSuccessfully()
2135             throws Throwable {
2136 
2137         // The apk used for this test deliberately includes a large amount of junk services,
2138         // so we're installing/uninstalling it as part of the test instead of leaving it in.
2139         final String apkPath =
2140                 "/data/local/tmp/cts/content/CtsAccessibilityLargeServiceVolumeApp.apk";
2141         final String packageName = "foo.bar.multipleservices";
2142         final int installedServiceCount = 16; // 16 unique services present in manifest.
2143         AccessibilityManager manager = mActivity.getSystemService(AccessibilityManager.class);
2144 
2145         try {
2146             assertThat(SystemUtil.runShellCommand(sUiAutomation, "pm install " + apkPath))
2147                     .startsWith("Success");
2148             TestUtils.waitUntil(
2149                     "Installed services have not appeared on the list.",
2150                     TIMEOUT_SERVICE_ENABLE / 1000,
2151                     () -> {
2152                         List<AccessibilityServiceInfo> installedServices =
2153                                 manager.getInstalledAccessibilityServiceList();
2154                         int count = 0;
2155                         for (int i = 0; i < installedServices.size(); i++) {
2156                             if (installedServices.get(i).getId().contains("JunkService")) {
2157                                 count++;
2158                             }
2159                         }
2160                         return count == installedServiceCount;
2161                     }
2162             );
2163         } finally {
2164             SystemUtil.runShellCommand(sUiAutomation, "pm uninstall " + packageName);
2165         }
2166     }
2167 
2168     @Test
2169     @ApiTest(
2170             apis = {
2171                 "android.view.accessibility.AccessibilityNodeInfo#setSelection",
2172                 "android.view.accessibility.AccessibilityNodeInfo#getSelection",
2173                 "android.view.accessibility.AccessibilityNodeInfo#Selection",
2174                 "android.view.accessibility.AccessibilityNodeInfo#SelectionPosition"
2175             })
2176     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_SELECTION_API)
testExtendedSelectionInterop()2177     public void testExtendedSelectionInterop() throws Exception {
2178         final String text = "Hello World!";
2179         sUiAutomation.executeAndWaitForEvent(
2180                 () ->
2181                         mActivityRule
2182                                 .getScenario()
2183                                 .onActivity(
2184                                         activity -> {
2185                                             EditText myView = activity.findViewById(R.id.edittext);
2186                                             myView.setTextIsSelectable(true);
2187                                             myView.setText(text);
2188                                         }),
2189                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
2190                 DEFAULT_TIMEOUT_MS);
2191 
2192         sUiAutomation.executeAndWaitForEvent(
2193                 () ->
2194                         sInstrumentation.runOnMainSync(
2195                                 () -> {
2196                                     AccessibilityNodeInfo viewNode =
2197                                             sUiAutomation
2198                                                     .getRootInActiveWindow()
2199                                                     .findAccessibilityNodeInfosByText(text)
2200                                                     .getFirst();
2201 
2202                                     Bundle b = new Bundle();
2203                                     b.putInt(
2204                                             AccessibilityNodeInfo
2205                                                     .ACTION_ARGUMENT_SELECTION_START_INT,
2206                                             0);
2207                                     b.putInt(
2208                                             AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT,
2209                                             4);
2210                                     assertTrue(
2211                                             viewNode.performAction(
2212                                                     AccessibilityNodeInfo.ACTION_SET_SELECTION, b));
2213                                 }),
2214                 filterForEventType(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED),
2215                 DEFAULT_TIMEOUT_MS);
2216         try {
2217             AccessibilityNodeInfo myViewNode =
2218                     sUiAutomation
2219                             .getRootInActiveWindow()
2220                             .findAccessibilityNodeInfosByText(text)
2221                             .getFirst();
2222 
2223             assertThat(myViewNode.getTextSelectionStart()).isEqualTo(0);
2224             assertThat(myViewNode.getTextSelectionEnd()).isEqualTo(4);
2225             assertThat(myViewNode.getSelection()).isNotNull();
2226             assertThat(myViewNode.getSelection().getStart().getNode()).isEqualTo(myViewNode);
2227             assertThat(myViewNode.getSelection().getEnd().getNode()).isEqualTo(myViewNode);
2228             assertThat(myViewNode.getSelection().getStart().getOffset()).isEqualTo(0);
2229             assertThat(myViewNode.getSelection().getEnd().getOffset()).isEqualTo(4);
2230         } finally {
2231             // Wait for EditText finish show popup menu async and close then set the view back to
2232             // non selectable
2233             mActivityRule
2234                     .getScenario()
2235                     .onActivity(
2236                             activity -> {
2237                                 EditText myView = activity.findViewById(R.id.edittext);
2238                                 // clear the selection
2239                                 myView.setTextIsSelectable(false);
2240                             });
2241         }
2242     }
2243 
2244     @Test
2245     @ApiTest(apis = {
2246             "android.view.accessibility.AccessibilityNodeInfo#setContainerTitle"})
testSetContainerTitle()2247     public void testSetContainerTitle() {
2248         View testView = mActivity.findViewById(R.id.buttonLayout);
2249         AccessibilityNodeInfo nodeInfo = testView.createAccessibilityNodeInfo();
2250         nodeInfo.setContainerTitle("Container title");
2251         assertEquals("Container title", nodeInfo.getContainerTitle());
2252 
2253         nodeInfo.setContainerTitle(null);
2254         assertEquals(null, nodeInfo.getContainerTitle());
2255     }
2256 
2257     @Test
2258     @ApiTest(apis = {"android.accessibilityservice.AccessibilityService#onMotionEvent"})
testOnMotionEvent_interceptsEventFromRequestedSource_SetAndUnset()2259     public void testOnMotionEvent_interceptsEventFromRequestedSource_SetAndUnset() {
2260         final StubMotionInterceptingAccessibilityService service =
2261                 mMotionInterceptingServiceRule.enableService();
2262         final int canarySource1 = InputDevice.SOURCE_JOYSTICK;
2263         final int canarySource2 = InputDevice.SOURCE_SENSOR;
2264         final int interestedSource = InputDevice.SOURCE_DPAD;
2265 
2266         // Set our interestedSource, inject an event, and assert it arrives.
2267         service.setAndAwaitMotionEventSources(
2268                 sUiAutomation, canarySource1, interestedSource,
2269                 TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS);
2270         service.injectAndAwaitMotionEvent(sUiAutomation, interestedSource,
2271                 TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS);
2272 
2273         // Then unset our interested MotionEvent source (by updating it to 0), inject an
2274         // event of the interested source type, and assert it does not arrive back to us.
2275         service.setAndAwaitMotionEventSources(
2276                 sUiAutomation,
2277                 // Use a different canary to ensure we're waiting for this new update.
2278                 canarySource2,
2279                 /*interestedSource=*/0,
2280                 TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS);
2281         assertThrows("Expected no event from source " + interestedSource, AssertionError.class,
2282                 () -> service.injectAndAwaitMotionEvent(sUiAutomation, interestedSource,
2283                         TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS));
2284     }
2285 
2286     @Test
2287     @ApiTest(apis = {"android.accessibilityservice.AccessibilityService#onMotionEvent"})
testOnMotionEvent_ignoresEventFromDifferentSource()2288     public void testOnMotionEvent_ignoresEventFromDifferentSource() {
2289         final StubMotionInterceptingAccessibilityService service =
2290                 mMotionInterceptingServiceRule.enableService();
2291         final int canarySource = InputDevice.SOURCE_JOYSTICK;
2292         final int interestedSource = InputDevice.SOURCE_DPAD;
2293         final int actualSource = InputDevice.SOURCE_ROTARY_ENCODER;
2294 
2295         service.setAndAwaitMotionEventSources(
2296                 sUiAutomation, canarySource, interestedSource,
2297                 TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS);
2298 
2299         assertThrows("Expected no event from source " + actualSource, AssertionError.class,
2300                 () -> service.injectAndAwaitMotionEvent(sUiAutomation, actualSource,
2301                         TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS));
2302     }
2303 
2304     @Test
2305     @ApiTest(apis = {"android.accessibilityservice.AccessibilityService#onMotionEvent"})
testOnMotionEvent_ignoresTouchscreenEventWhenTouchExplorationEnabled()2306     public void testOnMotionEvent_ignoresTouchscreenEventWhenTouchExplorationEnabled() {
2307         final int canarySource = InputDevice.SOURCE_JOYSTICK;
2308         final int interestedSource = InputDevice.SOURCE_TOUCHSCREEN;
2309         final StubMotionInterceptingAccessibilityService motionInterceptingService =
2310                 mMotionInterceptingServiceRule.enableService();
2311         TouchExplorationStubAccessibilityService touchExplorationService =
2312                 enableService(TouchExplorationStubAccessibilityService.class);
2313         try {
2314             motionInterceptingService.setAndAwaitMotionEventSources(
2315                     sUiAutomation, canarySource, interestedSource,
2316                     TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS);
2317 
2318             assertThrows("Expected no event from source " + interestedSource, AssertionError.class,
2319                     () -> motionInterceptingService.injectAndAwaitMotionEvent(
2320                             sUiAutomation, interestedSource,
2321                             TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS));
2322         } finally {
2323             touchExplorationService.disableSelfAndRemove();
2324         }
2325     }
2326 
2327     /** Test the case where we want to intercept but not consume motion events. */
2328     @Test
2329     @ApiTest(apis = {"android.accessibilityservice.AccessibilityService#onMotionEvent"})
2330     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_MOTION_EVENT_OBSERVING)
testOnMotionEvent_interceptsEventFromRequestedSource_observesMotionEvents()2331     public void testOnMotionEvent_interceptsEventFromRequestedSource_observesMotionEvents() {
2332         // Don't run this test on systems without a touchscreen.
2333         PackageManager pm = sInstrumentation.getTargetContext().getPackageManager();
2334         assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN));
2335 
2336         sUiAutomation.adoptShellPermissionIdentity(
2337                 android.Manifest.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING);
2338         final int requestedSource = InputDevice.SOURCE_TOUCHSCREEN;
2339         final StubMotionInterceptingAccessibilityService service =
2340                 mMotionInterceptingServiceRule.enableService();
2341         service.setMotionEventSources(requestedSource);
2342         service.setObservedMotionEventSources(requestedSource);
2343         assertThat(service.getServiceInfo().getMotionEventSources()).isEqualTo(requestedSource);
2344         assertThat(service.getServiceInfo().getObservedMotionEventSources())
2345                 .isEqualTo(requestedSource);
2346         final Object waitObject = new Object();
2347         final AtomicInteger eventCount = new AtomicInteger(0);
2348         service.setOnMotionEventListener(
2349                 motionEvent -> {
2350                     synchronized (waitObject) {
2351                         if (motionEvent.getSource() == requestedSource) {
2352                             eventCount.incrementAndGet();
2353                         }
2354                         waitObject.notifyAll();
2355                     }
2356                 });
2357 
2358         // Simulate a tap on the center of the button.
2359         final Button button = (Button) mActivity.findViewById(R.id.button);
2360         final EventCapturingMotionEventListener listener = new EventCapturingMotionEventListener();
2361         button.setOnTouchListener(listener);
2362         int[] buttonLocation = new int[2];
2363         final int midX = button.getWidth() / 2;
2364         final int midY = button.getHeight() / 2;
2365         button.getLocationOnScreen(buttonLocation);
2366         PointF tapLocation = new PointF(buttonLocation[0] + midX, buttonLocation[1] + midY);
2367         awaitDispatchGesture(
2368                 service,
2369                 () -> {
2370                     eventCount.set(0);
2371                     listener.clear();
2372                 },
2373                 click(tapLocation));
2374 
2375         // We should find 2 events.
2376         TestUtils.waitOn(
2377                 waitObject,
2378                 () -> eventCount.get() == 2,
2379                 TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS,
2380                 "Service did not receive MotionEvent");
2381 
2382         // The view should still have seen two events.
2383         listener.assertPropagated(ACTION_DOWN, ACTION_UP);
2384         // Stop listening to events for this source, then inject 1 more event to the input filter.
2385         service.setMotionEventSources(0 /* no sources */);
2386         assertThat(service.getServiceInfo().getMotionEventSources()).isEqualTo(0);
2387         awaitDispatchGesture(
2388                 service,
2389                 () -> {
2390                     eventCount.set(2);
2391                     listener.clear();
2392                 },
2393                 click(tapLocation));
2394 
2395         // Assert we only received the original 2.
2396         try {
2397             TestUtils.waitOn(
2398                     waitObject,
2399                     () -> eventCount.get() == 3,
2400                     TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS,
2401                     "(expected)");
2402         } catch (AssertionError e) {
2403             // expected
2404         }
2405         assertThat(eventCount.get()).isEqualTo(2);
2406     }
2407 
2408     @AsbSecurityTest(cveBugId = 326485767)
2409     @Test
testUpdateServiceWithoutIntent_disablesService()2410     public void testUpdateServiceWithoutIntent_disablesService() throws Exception {
2411         AccessibilityManager manager = mActivity.getSystemService(AccessibilityManager.class);
2412         final String v1ApkPath =
2413                 "/data/local/tmp/cts/content/CtsAccessibilityUpdateServicesAppV1.apk";
2414         final String v2ApkPath =
2415                 "/data/local/tmp/cts/content/CtsAccessibilityUpdateServicesAppV2.apk";
2416         final String v3ApkPath =
2417                 "/data/local/tmp/cts/content/CtsAccessibilityUpdateServicesAppV3.apk";
2418         final String packageName = "foo.bar.updateservice";
2419         final ComponentName service = ComponentName.createRelative(packageName, ".StubService");
2420 
2421         // Match AccessibilityManagerService#COMPONENT_NAME_SEPARATOR
2422         final String componentNameSeparator = ":";
2423         final String originalEnabledServicesSetting = getEnabledServicesSetting();
2424         try {
2425             // Install the apk in this test method, instead of as part of the target preparer, to
2426             // allow repeated --iterations of the test.
2427             assertThat(SystemUtil.runShellCommand(sUiAutomation, "pm install " + v1ApkPath))
2428                     .startsWith("Success");
2429             // Wait for the service to register as installed.
2430             TestUtils.waitUntil(
2431                     "Failed to install service:" + v1ApkPath,
2432                     (int) TIMEOUT_SERVICE_ENABLE / 1000,
2433                     () ->
2434                             manager.getInstalledAccessibilityServiceList().stream()
2435                                             .filter(info -> info.getId().startsWith(packageName))
2436                                             .count()
2437                                     == 1);
2438 
2439             // Enable the service and wait until AccessibilityManager reports it is
2440             // enabled.
2441             final String servicesToEnable = service.flattenToShortString();
2442             ShellCommandBuilder.create(sUiAutomation)
2443                     .putSecureSetting(
2444                             Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, servicesToEnable)
2445                     .run();
2446             // Wait for the service to be enabled.
2447             TestUtils.waitUntil(
2448                     "Failed to enable service:" + servicesToEnable,
2449                     (int) TIMEOUT_SERVICE_ENABLE / 1000,
2450                     () ->
2451                             getEnabledServices().stream()
2452                                             .filter(info -> info.getId().startsWith(packageName))
2453                                             .count()
2454                                     == 1);
2455 
2456             // Update to a new version that doesn't have the intent declared.
2457             assertThat(SystemUtil.runShellCommand(sUiAutomation, "pm install " + v2ApkPath))
2458                     .startsWith("Success");
2459 
2460             // Wait for the install to finish and the service to be disabled.
2461             TestUtils.waitUntil(
2462                     "The service is still in the enabled services list.",
2463                     TIMEOUT_SERVICE_ENABLE / 1000,
2464                     () ->
2465                             Arrays.asList(getEnabledServicesSetting().split(componentNameSeparator))
2466                                             .stream()
2467                                             .filter(comp -> comp.startsWith(packageName))
2468                                             .count()
2469                                     == 0);
2470 
2471             // Update to version 3 that does have the intent declared.
2472             // The service should not re-enable.
2473             assertThat(SystemUtil.runShellCommand(sUiAutomation, "pm install " + v3ApkPath))
2474                     .startsWith("Success");
2475 
2476             // confirm the service is still not enabled.
2477             assertThrows(
2478                     "The service is still in the enabled services list.",
2479                     AssertionError.class,
2480                     () ->
2481                             TestUtils.waitUntil(
2482                                     "The service is still in the enabled services list.",
2483                                     TIMEOUT_SERVICE_ENABLE / 1000,
2484                                     () ->
2485                                             Arrays.asList(getEnabledServicesSetting()
2486                                             .split(componentNameSeparator))
2487                                                             .stream().filter(comp ->
2488                                                             comp.startsWith(packageName))
2489                                                             .count() == 1));
2490 
2491         } finally {
2492             ShellCommandBuilder.create(sUiAutomation)
2493                     .putSecureSetting(
2494                             Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
2495                             originalEnabledServicesSetting)
2496                     .run();
2497             SystemUtil.runShellCommand(sUiAutomation, "pm uninstall " + packageName);
2498         }
2499     }
2500 
2501     @Test
2502     @ApiTest(apis = {
2503             "android.view.accessibility.AccessibilityNodeInfo#getLabeledByList"
2504     })
2505     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testLabeledBy_addZeroTimes_getLabeledByListGetsEmptyArray()2506     public void testLabeledBy_addZeroTimes_getLabeledByListGetsEmptyArray() {
2507         final View editText = mActivity.findViewById(R.id.edittext);
2508         assertThat(editText).isNotNull();
2509 
2510         final AccessibilityNodeInfo editTextInfo =
2511                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
2512                         mActivity.getResources().getResourceName(R.id.edittext)).get(0);
2513         assertThat(editTextInfo).isNotNull();
2514         final List<AccessibilityNodeInfo> labels = editTextInfo.getLabeledByList();
2515 
2516         assertThat(labels).hasSize(0);
2517     }
2518 
2519     @Test
2520     @ApiTest(apis = {
2521             "android.view.accessibility.AccessibilityNodeInfo#addLabeledBy",
2522             "android.view.accessibility.AccessibilityNodeInfo#getLabeledByList",
2523             "android.view.accessibility.AccessibilityNodeInfo#getLabeledBy"
2524     })
2525     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testLabeledBy_addTwoTimes_getLabeledByListGetsTwo_getLabeledByGetsLast()2526     public void testLabeledBy_addTwoTimes_getLabeledByListGetsTwo_getLabeledByGetsLast() {
2527         final View labelOne = mActivity.findViewById(R.id.labelOne);
2528         final View labelTwo = mActivity.findViewById(R.id.labelTwo);
2529         final View editText = mActivity.findViewById(R.id.edittext);
2530         assertThat(labelOne).isNotNull();
2531         assertThat(labelTwo).isNotNull();
2532         assertThat(editText).isNotNull();
2533 
2534         editText.setAccessibilityDelegate(new View.AccessibilityDelegate() {
2535             @Override
2536             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
2537                 super.onInitializeAccessibilityNodeInfo(host, info);
2538                 info.addLabeledBy(labelOne);
2539                 info.addLabeledBy(labelTwo);
2540             }
2541         });
2542         final AccessibilityNodeInfo editTextInfo =
2543                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
2544                         mActivity.getResources().getResourceName(R.id.edittext)).get(0);
2545         assertThat(editTextInfo).isNotNull();
2546         final List<AccessibilityNodeInfo> labels = editTextInfo.getLabeledByList();
2547         final AccessibilityNodeInfo label = editTextInfo.getLabeledBy();
2548 
2549         assertThat(labels).hasSize(2);
2550         assertThat(labels.get(0).getViewIdResourceName()).isEqualTo(
2551                 mActivity.getResources().getResourceName(R.id.labelOne));
2552         assertThat(labels.get(1).getViewIdResourceName()).isEqualTo(
2553                 mActivity.getResources().getResourceName(R.id.labelTwo));
2554         assertThat(label).isNotNull();
2555         assertThat(label.getViewIdResourceName()).isEqualTo(
2556                 mActivity.getResources().getResourceName(R.id.labelTwo));
2557     }
2558 
2559     @Test
2560     @ApiTest(apis = {
2561             "android.view.accessibility.AccessibilityNodeInfo#addLabeledBy",
2562             "android.view.accessibility.AccessibilityNodeInfo#getLabeledByList",
2563             "android.view.accessibility.AccessibilityNodeInfo#getLabeledBy"
2564     })
2565     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testLabeledBy_provider_addTwoTimes_getLabeledByListGetsTwo_getLabeledByGetsLast()2566     public void testLabeledBy_provider_addTwoTimes_getLabeledByListGetsTwo_getLabeledByGetsLast() {
2567         final View root = mActivity.findViewById(R.id.autoImportantLinearLayout);
2568         assertThat(root).isNotNull();
2569 
2570         root.setAccessibilityDelegate(new View.AccessibilityDelegate() {
2571             @Nullable
2572             @Override
2573             public AccessibilityNodeProvider getAccessibilityNodeProvider(@NonNull View host) {
2574                 return new LabelNodeProviderTest(root) {
2575                     @Nullable
2576                     @Override
2577                     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
2578                             int virtualViewId) {
2579                         List<AccessibilityNodeInfo> result = new ArrayList<>();
2580                         if (text.equals(LABELED)) {
2581                             AccessibilityNodeInfo node =
2582                                     new AccessibilityNodeInfo(root, LABELED_ID);
2583                             node.setText(LABELED);
2584                             node.addLabeledBy(root, LABEL_ONE_ID);
2585                             node.addLabeledBy(root, LABEL_TWO_ID);
2586                             result.add(node);
2587                         }
2588                         return result;
2589                     }
2590                 };
2591             }
2592         });
2593         final AccessibilityNodeInfo labeledNodeInfo = sUiAutomation.getRootInActiveWindow()
2594                         .findAccessibilityNodeInfosByText(LabelNodeProviderTest.LABELED).get(0);
2595         assertThat(labeledNodeInfo).isNotNull();
2596         final List<AccessibilityNodeInfo> labels = labeledNodeInfo.getLabeledByList();
2597         final AccessibilityNodeInfo label = labeledNodeInfo.getLabeledBy();
2598 
2599         assertThat(labels).hasSize(2);
2600         assertThat(labels.get(0).getText().toString()).isEqualTo(LabelNodeProviderTest.LABEL_ONE);
2601         assertThat(labels.get(1).getText().toString()).isEqualTo(LabelNodeProviderTest.LABEL_TWO);
2602         assertThat(label).isNotNull();
2603         assertThat(label.getText().toString()).isEqualTo(LabelNodeProviderTest.LABEL_TWO);
2604     }
2605 
2606     @Test
2607     @ApiTest(apis = {
2608             "android.view.accessibility.AccessibilityNodeInfo#setLabeledBy",
2609             "android.view.accessibility.AccessibilityNodeInfo#getLabeledByList",
2610             "android.view.accessibility.AccessibilityNodeInfo#getLabeledBy"
2611     })
2612     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testLabeledBy_setTwoTimes_getLabeledByListGetsLast_getLabeledByGetsLast()2613     public void testLabeledBy_setTwoTimes_getLabeledByListGetsLast_getLabeledByGetsLast() {
2614         final View labelOne = mActivity.findViewById(R.id.labelOne);
2615         final View labelTwo = mActivity.findViewById(R.id.labelTwo);
2616         final View editText = mActivity.findViewById(R.id.edittext);
2617         assertThat(labelOne).isNotNull();
2618         assertThat(labelTwo).isNotNull();
2619         assertThat(editText).isNotNull();
2620 
2621         editText.setAccessibilityDelegate(new View.AccessibilityDelegate() {
2622             @Override
2623             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
2624                 super.onInitializeAccessibilityNodeInfo(host, info);
2625                 info.setLabeledBy(labelOne);
2626                 info.setLabeledBy(labelTwo);
2627             }
2628         });
2629         final AccessibilityNodeInfo editTextInfo =
2630                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
2631                         mActivity.getResources().getResourceName(R.id.edittext)).get(0);
2632         assertThat(editTextInfo).isNotNull();
2633         final List<AccessibilityNodeInfo> labels = editTextInfo.getLabeledByList();
2634         final AccessibilityNodeInfo label = editTextInfo.getLabeledBy();
2635 
2636         assertThat(labels).hasSize(1);
2637         assertThat(labels.get(0).getViewIdResourceName()).isEqualTo(
2638                 mActivity.getResources().getResourceName(R.id.labelTwo));
2639         assertThat(label).isNotNull();
2640         assertThat(label.getViewIdResourceName()).isEqualTo(
2641                 mActivity.getResources().getResourceName(R.id.labelTwo));
2642     }
2643 
2644     @Test
2645     @ApiTest(apis = {
2646             "android.view.accessibility.AccessibilityNodeInfo#setLabeledBy",
2647             "android.view.accessibility.AccessibilityNodeInfo#getLabeledByList",
2648             "android.view.accessibility.AccessibilityNodeInfo#getLabeledBy"
2649     })
2650     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testLabeledBy_provider_setTwoTimes_getLabeledByListGetsLast_getLabeledByGetsLast()2651     public void testLabeledBy_provider_setTwoTimes_getLabeledByListGetsLast_getLabeledByGetsLast() {
2652         final View root = mActivity.findViewById(R.id.autoImportantLinearLayout);
2653         assertThat(root).isNotNull();
2654 
2655         root.setAccessibilityDelegate(new View.AccessibilityDelegate() {
2656             @Nullable
2657             @Override
2658             public AccessibilityNodeProvider getAccessibilityNodeProvider(@NonNull View host) {
2659                 return new LabelNodeProviderTest(root) {
2660                     @Nullable
2661                     @Override
2662                     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
2663                             int virtualViewId) {
2664                         List<AccessibilityNodeInfo> result = new ArrayList<>();
2665                         if (text.equals(LABELED)) {
2666                             AccessibilityNodeInfo node =
2667                                     new AccessibilityNodeInfo(root, LABELED_ID);
2668                             node.setText(LABELED);
2669                             node.setLabeledBy(root, LABEL_ONE_ID);
2670                             node.setLabeledBy(root, LABEL_TWO_ID);
2671                             result.add(node);
2672                         }
2673                         return result;
2674                     }
2675                 };
2676             }
2677         });
2678         final AccessibilityNodeInfo labeledNodeInfo = sUiAutomation.getRootInActiveWindow()
2679                 .findAccessibilityNodeInfosByText(LabelNodeProviderTest.LABELED).get(0);
2680         assertThat(labeledNodeInfo).isNotNull();
2681         final List<AccessibilityNodeInfo> labels = labeledNodeInfo.getLabeledByList();
2682         final AccessibilityNodeInfo label = labeledNodeInfo.getLabeledBy();
2683 
2684         assertThat(labels).hasSize(1);
2685         assertThat(labels.get(0).getText().toString()).isEqualTo(LabelNodeProviderTest.LABEL_TWO);
2686         assertThat(label).isNotNull();
2687         assertThat(label.getText().toString()).isEqualTo(LabelNodeProviderTest.LABEL_TWO);
2688     }
2689 
2690     @Test
2691     @ApiTest(apis = {
2692             "android.view.accessibility.AccessibilityNodeInfo#removeLabeledBy",
2693             "android.view.accessibility.AccessibilityNodeInfo#getLabeledByList",
2694             "android.view.accessibility.AccessibilityNodeInfo#getLabeledBy"
2695     })
2696     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testLabeledBy_removeFirst_getLabeledByListGetsLast_getLabeledByGetsLast()2697     public void testLabeledBy_removeFirst_getLabeledByListGetsLast_getLabeledByGetsLast() {
2698         final View labelOne = mActivity.findViewById(R.id.labelOne);
2699         final View labelTwo = mActivity.findViewById(R.id.labelTwo);
2700         final View editText = mActivity.findViewById(R.id.edittext);
2701         assertThat(labelOne).isNotNull();
2702         assertThat(labelTwo).isNotNull();
2703         assertThat(editText).isNotNull();
2704 
2705         editText.setAccessibilityDelegate(new View.AccessibilityDelegate() {
2706             @Override
2707             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
2708                 super.onInitializeAccessibilityNodeInfo(host, info);
2709                 info.addLabeledBy(labelOne);
2710                 info.addLabeledBy(labelTwo);
2711                 info.removeLabeledBy(labelOne);
2712             }
2713         });
2714         final AccessibilityNodeInfo editTextInfo =
2715                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
2716                         mActivity.getResources().getResourceName(R.id.edittext)).get(0);
2717         assertThat(editTextInfo).isNotNull();
2718         final List<AccessibilityNodeInfo> labels = editTextInfo.getLabeledByList();
2719         final AccessibilityNodeInfo label = editTextInfo.getLabeledBy();
2720 
2721         assertThat(labels).hasSize(1);
2722         assertThat(labels.get(0).getViewIdResourceName()).isEqualTo(
2723                 mActivity.getResources().getResourceName(R.id.labelTwo));
2724         assertThat(label).isNotNull();
2725         assertThat(label.getViewIdResourceName()).isEqualTo(
2726                 mActivity.getResources().getResourceName(R.id.labelTwo));
2727     }
2728 
2729     @Test
2730     @ApiTest(apis = {
2731             "android.view.accessibility.AccessibilityNodeInfo#removeLabeledBy",
2732             "android.view.accessibility.AccessibilityNodeInfo#getLabeledByList",
2733             "android.view.accessibility.AccessibilityNodeInfo#getLabeledBy"
2734     })
2735     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testLabeledBy_provider_removeFirst_getLabeledByListGetsLast_getLabeledByGetsLast()2736     public void testLabeledBy_provider_removeFirst_getLabeledByListGetsLast_getLabeledByGetsLast() {
2737         final View root = mActivity.findViewById(R.id.autoImportantLinearLayout);
2738         assertThat(root).isNotNull();
2739 
2740         root.setAccessibilityDelegate(new View.AccessibilityDelegate() {
2741             @Nullable
2742             @Override
2743             public AccessibilityNodeProvider getAccessibilityNodeProvider(@NonNull View host) {
2744                 return new LabelNodeProviderTest(root) {
2745                     @Nullable
2746                     @Override
2747                     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
2748                             int virtualViewId) {
2749                         List<AccessibilityNodeInfo> result = new ArrayList<>();
2750                         if (text.equals(LABELED)) {
2751                             AccessibilityNodeInfo node =
2752                                     new AccessibilityNodeInfo(root, LABELED_ID);
2753                             node.setText(LABELED);
2754                             node.addLabeledBy(root, LABEL_ONE_ID);
2755                             node.addLabeledBy(root, LABEL_TWO_ID);
2756                             node.removeLabeledBy(root, LABEL_ONE_ID);
2757                             result.add(node);
2758                         }
2759                         return result;
2760                     }
2761                 };
2762             }
2763         });
2764         final AccessibilityNodeInfo labeledNodeInfo = sUiAutomation.getRootInActiveWindow()
2765                 .findAccessibilityNodeInfosByText(LabelNodeProviderTest.LABELED).get(0);
2766         assertThat(labeledNodeInfo).isNotNull();
2767         final List<AccessibilityNodeInfo> labels = labeledNodeInfo.getLabeledByList();
2768         final AccessibilityNodeInfo label = labeledNodeInfo.getLabeledBy();
2769 
2770         assertThat(labels).hasSize(1);
2771         assertThat(labels.get(0).getText().toString()).isEqualTo(LabelNodeProviderTest.LABEL_TWO);
2772         assertThat(label).isNotNull();
2773         assertThat(label.getText().toString()).isEqualTo(LabelNodeProviderTest.LABEL_TWO);
2774     }
2775 
2776     @Test
2777     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
testAddLabeledBy_viewOnInitializeAccessibilityNodeInfoInternal()2778     public void testAddLabeledBy_viewOnInitializeAccessibilityNodeInfoInternal() {
2779         mActivityRule
2780                 .getScenario()
2781                 .onActivity(
2782                         activity -> {
2783                             final View labelOne = activity.findViewById(R.id.labelOne);
2784                             final View editText = activity.findViewById(R.id.edittext);
2785                             assertThat(labelOne).isNotNull();
2786                             assertThat(editText).isNotNull();
2787                             labelOne.setLabelFor(R.id.edittext);
2788                             mActivity = activity;
2789                         });
2790 
2791         final AccessibilityNodeInfo editTextInfo =
2792                 sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
2793                         mActivity.getResources().getResourceName(R.id.edittext)).get(0);
2794         assertThat(editTextInfo).isNotNull();
2795         final List<AccessibilityNodeInfo> labels = editTextInfo.getLabeledByList();
2796         final AccessibilityNodeInfo label = editTextInfo.getLabeledBy();
2797 
2798         assertThat(labels).hasSize(1);
2799         assertThat(labels.get(0).getViewIdResourceName()).isEqualTo(
2800                 mActivity.getResources().getResourceName(R.id.labelOne));
2801         assertThat(label).isNotNull();
2802         assertThat(label.getViewIdResourceName()).isEqualTo(
2803                 mActivity.getResources().getResourceName(R.id.labelOne));
2804     }
2805 
2806     @Test
2807     @ApiTest(apis = {
2808             "android.view.View#getSupplementalDescription",
2809     })
2810     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPLEMENTAL_DESCRIPTION)
testSupplementalDescriptionXmlAttribute()2811     public void testSupplementalDescriptionXmlAttribute() {
2812         final Button button = mActivity.findViewById(R.id.buttonWithTooltip);
2813         assertTrue(TextUtils.equals(mActivity.getString(R.string.foo_bar_baz),
2814                 button.getSupplementalDescription()));
2815     }
2816 
2817     @Test
2818     @ApiTest(apis = {
2819             "android.view.View#getSupplementalDescription",
2820             "android.view.View#setSupplementalDescription",
2821     })
2822     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPLEMENTAL_DESCRIPTION)
testSetGetSupplementalDescription()2823     public void testSetGetSupplementalDescription() {
2824         mActivityRule
2825                 .getScenario()
2826                 .onActivity(
2827                         activity -> {
2828                             final Button button = activity.findViewById(R.id.buttonWithTooltip);
2829                             final String supplementalDescription = activity.getString(R.string.a_b);
2830                             button.setSupplementalDescription(supplementalDescription);
2831                             assertTrue(
2832                                     TextUtils.equals(
2833                                             supplementalDescription,
2834                                             button.getSupplementalDescription()));
2835                         });
2836     }
2837 
2838     @MediumTest
2839     @Test
2840     @ApiTest(apis = {
2841             "android.view.View#setSupplementalDescription",
2842             "android.view.accessibility.AccessibilityEvent"
2843                     + "#CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION"
2844     })
2845     @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_SUPPLEMENTAL_DESCRIPTION)
testSetSupplementalDescription_sendContentChangeTypeSupplementalDescriptionEvent()2846     public void testSetSupplementalDescription_sendContentChangeTypeSupplementalDescriptionEvent()
2847             throws Throwable {
2848         // create and populate the expected event
2849         final AccessibilityEvent expected = new AccessibilityEvent();
2850         expected.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
2851         expected.setContentChangeTypes(
2852                 AccessibilityEvent.CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION);
2853         expected.setClassName(Button.class.getName());
2854         expected.setPackageName(mActivity.getPackageName());
2855         expected.setDisplayId(mActivity.getDisplayId());
2856         expected.setEnabled(true);
2857 
2858         final Button button = mActivity.findViewById(R.id.buttonWithTooltip);
2859 
2860         // check the received event
2861         AccessibilityEvent awaitedEvent =
2862                 sUiAutomation.executeAndWaitForEvent(
2863                         () -> {
2864                             // trigger the event
2865                             mActivity.runOnUiThread(() -> button.setSupplementalDescription(
2866                                     mActivity.getString(R.string.a_b)));
2867                         },
2868                         event -> equalsAccessibilityEvent(event, expected),
2869                         DEFAULT_TIMEOUT_MS);
2870         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
2871     }
2872 
2873     private static class LabelNodeProviderTest extends AccessibilityNodeProvider {
2874         static final int LABELED_ID = 1;
2875         static final int LABEL_ONE_ID = 2;
2876         static final int LABEL_TWO_ID = 3;
2877         static final String LABELED = "labeled";
2878         static final String LABEL_ONE = "labelOne";
2879         static final String LABEL_TWO = "labelTwo";
2880 
2881         private final View mRoot;
2882 
LabelNodeProviderTest(View root)2883         LabelNodeProviderTest(View root) {
2884             this.mRoot = root;
2885         }
2886 
2887         @Nullable
2888         @Override
createAccessibilityNodeInfo(int virtualViewId)2889         public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
2890             final AccessibilityNodeInfo node = new AccessibilityNodeInfo(mRoot, virtualViewId);
2891             // This function is only used to get labels, so the below is sufficient.
2892             if (virtualViewId == LABEL_ONE_ID) {
2893                 node.setText(LABEL_ONE);
2894             } else if (virtualViewId == LABEL_TWO_ID) {
2895                 node.setText(LABEL_TWO);
2896             }
2897             return node;
2898         }
2899     }
2900 
getEnabledServices()2901     private List<AccessibilityServiceInfo> getEnabledServices() {
2902         return ((AccessibilityManager) sInstrumentation.getContext().getSystemService(
2903                 Context.ACCESSIBILITY_SERVICE)).getEnabledAccessibilityServiceList(
2904                 FEEDBACK_ALL_MASK);
2905     }
2906 
getEnabledServicesSetting()2907     private String getEnabledServicesSetting() {
2908         final String result = Settings.Secure.getString(
2909                 sInstrumentation.getContext().getContentResolver(),
2910                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
2911         return result != null ? result : "";
2912     }
2913 
assertPackageName(AccessibilityNodeInfo node, String packageName)2914     private static void assertPackageName(AccessibilityNodeInfo node, String packageName) {
2915         if (node == null) {
2916             return;
2917         }
2918         assertEquals(packageName, node.getPackageName());
2919         final int childCount = node.getChildCount();
2920         for (int i = 0; i < childCount; i++) {
2921             AccessibilityNodeInfo child = node.getChild(i);
2922             if (child != null) {
2923                 assertPackageName(child, packageName);
2924             }
2925         }
2926     }
2927 
enableTouchExploration(boolean enabled)2928     private static void enableTouchExploration(boolean enabled)
2929             throws InterruptedException {
2930         final int TIMEOUT_FOR_SERVICE_ENABLE = 10000; // millis; 10s
2931         final Object waitObject = new Object();
2932         final AtomicBoolean atomicBoolean = new AtomicBoolean(!enabled);
2933         AccessibilityManager.TouchExplorationStateChangeListener serviceListener = (boolean b) -> {
2934             synchronized (waitObject) {
2935                 atomicBoolean.set(b);
2936                 waitObject.notifyAll();
2937             }
2938         };
2939         final AccessibilityManager manager =
2940                 (AccessibilityManager) sInstrumentation.getContext().getSystemService(
2941                         Service.ACCESSIBILITY_SERVICE);
2942         manager.addTouchExplorationStateChangeListener(serviceListener);
2943 
2944         final AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
2945         assert info != null;
2946         if (enabled) {
2947             info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
2948         } else {
2949             info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
2950         }
2951         sUiAutomation.setServiceInfo(info);
2952 
2953         final long timeoutTime = System.currentTimeMillis() + TIMEOUT_FOR_SERVICE_ENABLE;
2954         synchronized (waitObject) {
2955             while ((enabled != atomicBoolean.get()) && (System.currentTimeMillis() < timeoutTime)) {
2956                 waitObject.wait(timeoutTime - System.currentTimeMillis());
2957             }
2958         }
2959         if (enabled) {
2960             assertTrue("Touch exploration state listener not called when services enabled",
2961                     atomicBoolean.get());
2962             assertTrue("Timed out enabling accessibility",
2963                     manager.isEnabled() && manager.isTouchExplorationEnabled());
2964         } else {
2965             assertFalse("Touch exploration state listener not called when services disabled",
2966                     atomicBoolean.get());
2967             assertFalse("Timed out disabling accessibility",
2968                     manager.isEnabled() && manager.isTouchExplorationEnabled());
2969         }
2970         manager.removeTouchExplorationStateChangeListener(serviceListener);
2971     }
2972 
2973     /**
2974      * Returns a service for testing how accessibility tools or non-tools react to the
2975      * {@link View#isAccessibilityDataSensitive} property.
2976      *
2977      * @return {@link StubA11yToolAccessibilityService} when <code>isAccessibilityTool</code> is
2978      * true, otherwise returns {@link StubNonA11yToolAccessibilityService}.
2979      */
getServiceForA11yToolTests( boolean isAccessibilityTool)2980     private StubEventCapturingAccessibilityService getServiceForA11yToolTests(
2981             boolean isAccessibilityTool) {
2982         final StubEventCapturingAccessibilityService service;
2983         if (isAccessibilityTool) {
2984             service = InstrumentedAccessibilityService.enableService(
2985                     StubA11yToolAccessibilityService.class);
2986         } else {
2987             service = InstrumentedAccessibilityService.enableService(
2988                     StubNonA11yToolAccessibilityService.class);
2989         }
2990         final AccessibilityServiceInfo info = service.getServiceInfo();
2991         if (info == null || info.isAccessibilityTool() != isAccessibilityTool) {
2992             service.disableSelfAndRemove();
2993             fail("Expected service to have isAccessibilityTool=" + isAccessibilityTool);
2994         }
2995         return service;
2996     }
2997 
matchHover(int action, int x, int y)2998     private static MotionEvent matchHover(int action, int x, int y) {
2999         return argThat(new CtsMouseUtil.PositionMatcher(action, x, y));
3000     }
3001 
injectHoverEvent(long downTime, boolean isFirstHoverEvent, int xOnScreen, int yOnScreen)3002     private static void injectHoverEvent(long downTime, boolean isFirstHoverEvent,
3003             int xOnScreen, int yOnScreen) {
3004         final long eventTime = isFirstHoverEvent ? SystemClock.uptimeMillis() : downTime;
3005         MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_HOVER_MOVE,
3006                 xOnScreen, yOnScreen, 0);
3007         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
3008         sInstrumentation.sendPointerSync(event);
3009         event.recycle();
3010     }
3011 
injectHoverExit(long eventTime, int xOnScreen, int yOnScreen)3012     private static void injectHoverExit(long eventTime, int xOnScreen, int yOnScreen) {
3013         MotionEvent event = MotionEvent.obtain(eventTime, eventTime, MotionEvent.ACTION_HOVER_EXIT,
3014                 xOnScreen, yOnScreen, 0);
3015         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
3016         sInstrumentation.sendPointerSync(event);
3017         event.recycle();
3018     }
3019 
getAppWidgetProviderInfo()3020     private AppWidgetProviderInfo getAppWidgetProviderInfo() {
3021         final ComponentName componentName = new ComponentName(
3022                 "foo.bar.baz", "foo.bar.baz.MyAppWidgetProvider");
3023         final List<AppWidgetProviderInfo> providers = getAppWidgetManager().getInstalledProviders();
3024         final int providerCount = providers.size();
3025         for (int i = 0; i < providerCount; i++) {
3026             final AppWidgetProviderInfo provider = providers.get(i);
3027             if (componentName.equals(provider.provider)
3028                     && Process.myUserHandle().equals(provider.getProfile())) {
3029                 return provider;
3030             }
3031         }
3032         return null;
3033     }
3034 
grantBindAppWidgetPermission()3035     private void grantBindAppWidgetPermission() throws Exception {
3036         ShellCommandBuilder.execShellCommand(sUiAutomation,
3037                 GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND + getCurrentUser());
3038     }
3039 
revokeBindAppWidgetPermission()3040     private void revokeBindAppWidgetPermission() throws Exception {
3041         ShellCommandBuilder.execShellCommand(sUiAutomation,
3042                 REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND + getCurrentUser());
3043     }
3044 
getAppWidgetManager()3045     private AppWidgetManager getAppWidgetManager() {
3046         return (AppWidgetManager) sInstrumentation.getTargetContext()
3047                 .getSystemService(Context.APPWIDGET_SERVICE);
3048     }
3049 
hasAppWidgets()3050     private boolean hasAppWidgets() {
3051         return sInstrumentation.getTargetContext().getPackageManager()
3052                 .hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS);
3053     }
3054 
3055     /**
3056      * Compares all properties of the <code>first</code> and the
3057      * <code>second</code>.
3058      */
equalsAccessibilityEvent(AccessibilityEvent first, AccessibilityEvent second)3059     private boolean equalsAccessibilityEvent(AccessibilityEvent first, AccessibilityEvent second) {
3060          return first.getEventType() == second.getEventType()
3061             && first.isChecked() == second.isChecked()
3062             && first.getCurrentItemIndex() == second.getCurrentItemIndex()
3063             && first.isEnabled() == second.isEnabled()
3064             && first.getFromIndex() == second.getFromIndex()
3065             && first.getItemCount() == second.getItemCount()
3066             && first.isPassword() == second.isPassword()
3067             && first.getRemovedCount() == second.getRemovedCount()
3068             && first.isScrollable()== second.isScrollable()
3069             && first.getToIndex() == second.getToIndex()
3070             && first.getRecordCount() == second.getRecordCount()
3071             && first.getScrollX() == second.getScrollX()
3072             && first.getScrollY() == second.getScrollY()
3073             && first.getAddedCount() == second.getAddedCount()
3074             && first.getDisplayId() == second.getDisplayId()
3075             && TextUtils.equals(first.getBeforeText(), second.getBeforeText())
3076             && TextUtils.equals(first.getClassName(), second.getClassName())
3077             && TextUtils.equals(first.getContentDescription(), second.getContentDescription())
3078             && equalsNotificationAsParcelableData(first, second)
3079             && equalsText(first, second);
3080     }
3081 
3082     /**
3083      * Compares the {@link android.os.Parcelable} data of the
3084      * <code>first</code> and <code>second</code>.
3085      */
equalsNotificationAsParcelableData(AccessibilityEvent first, AccessibilityEvent second)3086     private boolean equalsNotificationAsParcelableData(AccessibilityEvent first,
3087             AccessibilityEvent second) {
3088         Notification firstNotification = (Notification) first.getParcelableData();
3089         Notification secondNotification = (Notification) second.getParcelableData();
3090         if (firstNotification == null) {
3091             return (secondNotification == null);
3092         } else if (secondNotification == null) {
3093             return false;
3094         }
3095         return TextUtils.equals(firstNotification.tickerText, secondNotification.tickerText);
3096     }
3097 
3098     /**
3099      * Compares the text of the <code>first</code> and <code>second</code> text.
3100      */
equalsText(AccessibilityEvent first, AccessibilityEvent second)3101     private boolean equalsText(AccessibilityEvent first, AccessibilityEvent second) {
3102         List<CharSequence> firstText = first.getText();
3103         List<CharSequence> secondText = second.getText();
3104         if (firstText.size() != secondText.size()) {
3105             return false;
3106         }
3107         Iterator<CharSequence> firstIterator = firstText.iterator();
3108         Iterator<CharSequence> secondIterator = secondText.iterator();
3109         for (int i = 0; i < firstText.size(); i++) {
3110             if (!firstIterator.next().toString().equals(secondIterator.next().toString())) {
3111                 return false;
3112             }
3113         }
3114         return true;
3115     }
3116 
hasTooltipShowing(int id)3117     private boolean hasTooltipShowing(int id) {
3118         return getOnMain(sInstrumentation, () -> {
3119             final View viewWithTooltip = mActivity.findViewById(id);
3120             if (viewWithTooltip == null) {
3121                 return false;
3122             }
3123             final View tooltipView = viewWithTooltip.getTooltipView();
3124             return (tooltipView != null) && (tooltipView.getParent() != null);
3125         });
3126     }
3127 
awaitDispatchGesture( InstrumentedAccessibilityService service, @Nullable Runnable reset, StrokeDescription firstStroke, StrokeDescription... rest)3128     private void awaitDispatchGesture(
3129             InstrumentedAccessibilityService service,
3130             @Nullable Runnable reset,
3131             StrokeDescription firstStroke,
3132             StrokeDescription... rest) {
3133         GestureDescription.Builder builder =
3134                 new GestureDescription.Builder().addStroke(firstStroke);
3135         for (StrokeDescription stroke : rest) {
3136             builder.addStroke(stroke);
3137         }
3138         final GestureDescription gesture = builder.build();
3139         try {
3140             awaitDispatchGesture(service, gesture);
3141         } catch (RuntimeException e) {
3142             // The input filter could have been rebuilt causing this gesture to cancel.
3143             // Reset state and try one more time.
3144             if (reset != null) {
3145                 reset.run();
3146             }
3147             awaitDispatchGesture(service, gesture);
3148         }
3149     }
3150 
awaitDispatchGesture( InstrumentedAccessibilityService service, GestureDescription gesture)3151     private void awaitDispatchGesture(
3152             InstrumentedAccessibilityService service, GestureDescription gesture) {
3153         await(dispatchGesture(service, gesture));
3154     }
3155 
getCurrentUser()3156     private static int getCurrentUser() {
3157         return android.os.Process.myUserHandle().getIdentifier();
3158     }
3159 }
3160