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.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventType;
20 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithResource;
21 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.findWindowByTitle;
22 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.getActivityTitle;
23 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
24 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
25 import static android.accessibilityservice.cts.utils.RunOnMainUtils.getOnMain;
26 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_HIDE_TOOLTIP;
27 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_TOOLTIP;
28 
29 import static org.hamcrest.Matchers.in;
30 import static org.hamcrest.Matchers.not;
31 import static org.hamcrest.core.IsEqual.equalTo;
32 import static org.hamcrest.core.IsNull.notNullValue;
33 import static org.hamcrest.core.IsNull.nullValue;
34 import static org.junit.Assert.assertEquals;
35 import static org.junit.Assert.assertFalse;
36 import static org.junit.Assert.assertNotNull;
37 import static org.junit.Assert.assertThat;
38 import static org.junit.Assert.assertTrue;
39 import static org.junit.Assert.fail;
40 import static org.mockito.ArgumentMatchers.any;
41 import static org.mockito.ArgumentMatchers.argThat;
42 import static org.mockito.ArgumentMatchers.eq;
43 import static org.mockito.Mockito.inOrder;
44 import static org.mockito.Mockito.mock;
45 import static org.mockito.Mockito.timeout;
46 import static org.mockito.Mockito.verify;
47 
48 import android.accessibility.cts.common.InstrumentedAccessibilityService;
49 import android.accessibility.cts.common.ShellCommandBuilder;
50 import android.accessibilityservice.AccessibilityServiceInfo;
51 import android.accessibilityservice.cts.activities.AccessibilityEndToEndActivity;
52 import android.app.Activity;
53 import android.app.AlertDialog;
54 import android.app.Instrumentation;
55 import android.app.Notification;
56 import android.app.NotificationChannel;
57 import android.app.NotificationManager;
58 import android.app.PendingIntent;
59 import android.app.Service;
60 import android.app.UiAutomation;
61 import android.appwidget.AppWidgetHost;
62 import android.appwidget.AppWidgetManager;
63 import android.appwidget.AppWidgetProviderInfo;
64 import android.content.ComponentName;
65 import android.content.Context;
66 import android.content.Intent;
67 import android.content.pm.PackageManager;
68 import android.content.res.Configuration;
69 import android.content.res.Resources;
70 import android.graphics.Rect;
71 import android.graphics.Region;
72 import android.os.Process;
73 import android.os.SystemClock;
74 import android.platform.test.annotations.AppModeFull;
75 import android.platform.test.annotations.Presubmit;
76 import android.test.suitebuilder.annotation.MediumTest;
77 import android.text.TextUtils;
78 import android.util.Log;
79 import android.view.InputDevice;
80 import android.view.MotionEvent;
81 import android.view.TouchDelegate;
82 import android.view.View;
83 import android.view.Window;
84 import android.view.WindowManager;
85 import android.view.accessibility.AccessibilityEvent;
86 import android.view.accessibility.AccessibilityManager;
87 import android.view.accessibility.AccessibilityNodeInfo;
88 import android.view.accessibility.AccessibilityWindowInfo;
89 import android.widget.Button;
90 import android.widget.EditText;
91 import android.widget.ListView;
92 
93 import androidx.test.InstrumentationRegistry;
94 import androidx.test.rule.ActivityTestRule;
95 import androidx.test.runner.AndroidJUnit4;
96 
97 import com.android.compatibility.common.util.CtsMouseUtil;
98 
99 import org.junit.AfterClass;
100 import org.junit.Before;
101 import org.junit.BeforeClass;
102 import org.junit.Rule;
103 import org.junit.Test;
104 import org.junit.runner.RunWith;
105 
106 import java.util.Iterator;
107 import java.util.List;
108 import java.util.concurrent.TimeoutException;
109 import java.util.concurrent.atomic.AtomicBoolean;
110 
111 /**
112  * This class performs end-to-end testing of the accessibility feature by
113  * creating an {@link Activity} and poking around so {@link AccessibilityEvent}s
114  * are generated and their correct dispatch verified.
115  */
116 @RunWith(AndroidJUnit4.class)
117 public class AccessibilityEndToEndTest {
118 
119     private static final String LOG_TAG = "AccessibilityEndToEndTest";
120 
121     private static final String GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND =
122             "appwidget grantbind --package android.accessibilityservice.cts --user ";
123 
124     private static final String REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND =
125             "appwidget revokebind --package android.accessibilityservice.cts --user ";
126 
127     private static final String APP_WIDGET_PROVIDER_PACKAGE = "foo.bar.baz";
128 
129     private static Instrumentation sInstrumentation;
130     private static UiAutomation sUiAutomation;
131 
132     private AccessibilityEndToEndActivity mActivity;
133 
134     @Rule
135     public ActivityTestRule<AccessibilityEndToEndActivity> mActivityRule =
136             new ActivityTestRule<>(AccessibilityEndToEndActivity.class, false, false);
137 
138     @BeforeClass
oneTimeSetup()139     public static void oneTimeSetup() throws Exception {
140         sInstrumentation = InstrumentationRegistry.getInstrumentation();
141         sUiAutomation = sInstrumentation.getUiAutomation();
142     }
143 
144     @AfterClass
postTestTearDown()145     public static void postTestTearDown() {
146         sUiAutomation.destroy();
147     }
148 
149     @Before
setUp()150     public void setUp() throws Exception {
151         mActivity = launchActivityAndWaitForItToBeOnscreen(
152                 sInstrumentation, sUiAutomation, mActivityRule);
153     }
154 
155     @MediumTest
156     @Presubmit
157     @Test
testTypeViewSelectedAccessibilityEvent()158     public void testTypeViewSelectedAccessibilityEvent() throws Throwable {
159         // create and populate the expected event
160         final AccessibilityEvent expected = AccessibilityEvent.obtain();
161         expected.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
162         expected.setClassName(ListView.class.getName());
163         expected.setPackageName(mActivity.getPackageName());
164         expected.getText().add(mActivity.getString(R.string.second_list_item));
165         expected.setItemCount(2);
166         expected.setCurrentItemIndex(1);
167         expected.setEnabled(true);
168         expected.setScrollable(false);
169         expected.setFromIndex(0);
170         expected.setToIndex(1);
171 
172         final ListView listView = (ListView) mActivity.findViewById(R.id.listview);
173 
174         AccessibilityEvent awaitedEvent =
175             sUiAutomation.executeAndWaitForEvent(
176                 new Runnable() {
177             @Override
178             public void run() {
179                 // trigger the event
180                 mActivity.runOnUiThread(new Runnable() {
181                     @Override
182                     public void run() {
183                         listView.setSelection(1);
184                     }
185                 });
186             }},
187             new UiAutomation.AccessibilityEventFilter() {
188                 // check the received event
189                 @Override
190                 public boolean accept(AccessibilityEvent event) {
191                     return equalsAccessiblityEvent(event, expected);
192                 }
193             },
194                     DEFAULT_TIMEOUT_MS);
195         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
196     }
197 
198     @MediumTest
199     @Presubmit
200     @Test
testTypeViewClickedAccessibilityEvent()201     public void testTypeViewClickedAccessibilityEvent() throws Throwable {
202         // create and populate the expected event
203         final AccessibilityEvent expected = AccessibilityEvent.obtain();
204         expected.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED);
205         expected.setClassName(Button.class.getName());
206         expected.setPackageName(mActivity.getPackageName());
207         expected.getText().add(mActivity.getString(R.string.button_title));
208         expected.setEnabled(true);
209 
210         final Button button = (Button) mActivity.findViewById(R.id.button);
211 
212         AccessibilityEvent awaitedEvent =
213             sUiAutomation.executeAndWaitForEvent(
214                 new Runnable() {
215             @Override
216             public void run() {
217                 // trigger the event
218                 mActivity.runOnUiThread(new Runnable() {
219                     @Override
220                     public void run() {
221                         button.performClick();
222                     }
223                 });
224             }},
225             new UiAutomation.AccessibilityEventFilter() {
226                 // check the received event
227                 @Override
228                 public boolean accept(AccessibilityEvent event) {
229                     return equalsAccessiblityEvent(event, expected);
230                 }
231             },
232                     DEFAULT_TIMEOUT_MS);
233         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
234     }
235 
236     @MediumTest
237     @Presubmit
238     @Test
testTypeViewLongClickedAccessibilityEvent()239     public void testTypeViewLongClickedAccessibilityEvent() throws Throwable {
240         // create and populate the expected event
241         final AccessibilityEvent expected = AccessibilityEvent.obtain();
242         expected.setEventType(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
243         expected.setClassName(Button.class.getName());
244         expected.setPackageName(mActivity.getPackageName());
245         expected.getText().add(mActivity.getString(R.string.button_title));
246         expected.setEnabled(true);
247 
248         final Button button = (Button) mActivity.findViewById(R.id.button);
249 
250         AccessibilityEvent awaitedEvent =
251             sUiAutomation.executeAndWaitForEvent(
252                 new Runnable() {
253             @Override
254             public void run() {
255                 // trigger the event
256                 mActivity.runOnUiThread(new Runnable() {
257                     @Override
258                     public void run() {
259                         button.performLongClick();
260                     }
261                 });
262             }},
263             new UiAutomation.AccessibilityEventFilter() {
264                 // check the received event
265                 @Override
266                 public boolean accept(AccessibilityEvent event) {
267                     return equalsAccessiblityEvent(event, expected);
268                 }
269             },
270                     DEFAULT_TIMEOUT_MS);
271         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
272     }
273 
274     @MediumTest
275     @Presubmit
276     @Test
testTypeViewFocusedAccessibilityEvent()277     public void testTypeViewFocusedAccessibilityEvent() throws Throwable {
278         // create and populate the expected event
279         final AccessibilityEvent expected = AccessibilityEvent.obtain();
280         expected.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
281         expected.setClassName(Button.class.getName());
282         expected.setPackageName(mActivity.getPackageName());
283         expected.getText().add(mActivity.getString(R.string.button_title));
284         expected.setItemCount(5);
285         expected.setCurrentItemIndex(3);
286         expected.setEnabled(true);
287 
288         final Button button = (Button) mActivity.findViewById(R.id.buttonWithTooltip);
289 
290         AccessibilityEvent awaitedEvent =
291             sUiAutomation.executeAndWaitForEvent(
292                     () -> mActivity.runOnUiThread(() -> button.requestFocus()),
293                     (event) -> equalsAccessiblityEvent(event, expected),
294                     DEFAULT_TIMEOUT_MS);
295         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
296     }
297 
298     @MediumTest
299     @Presubmit
300     @Test
testTypeViewTextChangedAccessibilityEvent()301     public void testTypeViewTextChangedAccessibilityEvent() throws Throwable {
302         // focus the edit text
303         final EditText editText = (EditText) mActivity.findViewById(R.id.edittext);
304 
305         AccessibilityEvent awaitedFocusEvent =
306             sUiAutomation.executeAndWaitForEvent(
307                 new Runnable() {
308             @Override
309             public void run() {
310                 // trigger the event
311                 mActivity.runOnUiThread(new Runnable() {
312                     @Override
313                     public void run() {
314                         editText.requestFocus();
315                     }
316                 });
317             }},
318             new UiAutomation.AccessibilityEventFilter() {
319                 // check the received event
320                 @Override
321                 public boolean accept(AccessibilityEvent event) {
322                     return event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED;
323                 }
324             },
325                     DEFAULT_TIMEOUT_MS);
326         assertNotNull("Did not receive expected focuss event.", awaitedFocusEvent);
327 
328         final String beforeText = mActivity.getString(R.string.text_input_blah);
329         final String newText = mActivity.getString(R.string.text_input_blah_blah);
330         final String afterText = beforeText.substring(0, 3) + newText;
331 
332         // create and populate the expected event
333         final AccessibilityEvent expected = AccessibilityEvent.obtain();
334         expected.setEventType(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
335         expected.setClassName(EditText.class.getName());
336         expected.setPackageName(mActivity.getPackageName());
337         expected.getText().add(afterText);
338         expected.setBeforeText(beforeText);
339         expected.setFromIndex(3);
340         expected.setAddedCount(9);
341         expected.setRemovedCount(1);
342         expected.setEnabled(true);
343 
344         AccessibilityEvent awaitedTextChangeEvent =
345             sUiAutomation.executeAndWaitForEvent(
346                 new Runnable() {
347             @Override
348             public void run() {
349                 // trigger the event
350                 mActivity.runOnUiThread(new Runnable() {
351                     @Override
352                     public void run() {
353                         editText.getEditableText().replace(3, 4, newText);
354                     }
355                 });
356             }},
357             new UiAutomation.AccessibilityEventFilter() {
358                 // check the received event
359                 @Override
360                 public boolean accept(AccessibilityEvent event) {
361                     return equalsAccessiblityEvent(event, expected);
362                 }
363             },
364                     DEFAULT_TIMEOUT_MS);
365         assertNotNull("Did not receive expected event: " + expected, awaitedTextChangeEvent);
366     }
367 
368     @MediumTest
369     @Presubmit
370     @Test
testTypeWindowStateChangedAccessibilityEvent()371     public void testTypeWindowStateChangedAccessibilityEvent() throws Throwable {
372         // create and populate the expected event
373         final AccessibilityEvent expected = AccessibilityEvent.obtain();
374         expected.setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
375         expected.setClassName(AlertDialog.class.getName());
376         expected.setPackageName(mActivity.getPackageName());
377         expected.getText().add(mActivity.getString(R.string.alert_title));
378         expected.getText().add(mActivity.getString(R.string.alert_message));
379         expected.setEnabled(true);
380 
381         AccessibilityEvent awaitedEvent =
382             sUiAutomation.executeAndWaitForEvent(
383                 new Runnable() {
384             @Override
385             public void run() {
386                 // trigger the event
387                 mActivity.runOnUiThread(new Runnable() {
388                     @Override
389                     public void run() {
390                         (new AlertDialog.Builder(mActivity).setTitle(R.string.alert_title)
391                                 .setMessage(R.string.alert_message)).create().show();
392                     }
393                 });
394             }},
395             new UiAutomation.AccessibilityEventFilter() {
396                 // check the received event
397                 @Override
398                 public boolean accept(AccessibilityEvent event) {
399                     return equalsAccessiblityEvent(event, expected);
400                 }
401             },
402                     DEFAULT_TIMEOUT_MS);
403         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
404     }
405 
406     @MediumTest
407     @AppModeFull
408     @SuppressWarnings("deprecation")
409     @Presubmit
410     @Test
testTypeNotificationStateChangedAccessibilityEvent()411     public void testTypeNotificationStateChangedAccessibilityEvent() throws Throwable {
412         // No notification UI on televisions.
413         if ((mActivity.getResources().getConfiguration().uiMode
414                 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) {
415             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
416                     " - No notification UI on televisions.");
417             return;
418         }
419         PackageManager pm = sInstrumentation.getTargetContext().getPackageManager();
420         if (pm.hasSystemFeature(pm.FEATURE_WATCH)) {
421             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
422                     " - Watches have different notification system.");
423             return;
424         }
425         if (pm.hasSystemFeature(pm.FEATURE_AUTOMOTIVE)) {
426             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
427                     " - Automotive handle notifications differently.");
428             return;
429         }
430 
431         String message = mActivity.getString(R.string.notification_message);
432 
433         final NotificationManager notificationManager =
434                 (NotificationManager) mActivity.getSystemService(Service.NOTIFICATION_SERVICE);
435         final NotificationChannel channel =
436                 new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT);
437         try {
438             // create the notification to send
439             channel.enableVibration(true);
440             channel.enableLights(true);
441             channel.setBypassDnd(true);
442             notificationManager.createNotificationChannel(channel);
443             NotificationChannel created =
444                     notificationManager.getNotificationChannel(channel.getId());
445             final int notificationId = 1;
446             final Notification notification =
447                     new Notification.Builder(mActivity, channel.getId())
448                             .setSmallIcon(android.R.drawable.stat_notify_call_mute)
449                             .setContentIntent(PendingIntent.getActivity(mActivity, 0,
450                                     new Intent(),
451                                     PendingIntent.FLAG_CANCEL_CURRENT))
452                             .setTicker(message)
453                             .setContentTitle("")
454                             .setContentText("")
455                             .setPriority(Notification.PRIORITY_MAX)
456                             // Mark the notification as "interruptive" by specifying a vibration
457                             // pattern. This ensures it's announced properly on watch-type devices.
458                             .setVibrate(new long[]{})
459                             .build();
460 
461             // create and populate the expected event
462             final AccessibilityEvent expected = AccessibilityEvent.obtain();
463             expected.setEventType(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
464             expected.setClassName(Notification.class.getName());
465             expected.setPackageName(mActivity.getPackageName());
466             expected.getText().add(message);
467             expected.setParcelableData(notification);
468 
469             AccessibilityEvent awaitedEvent =
470                     sUiAutomation.executeAndWaitForEvent(
471                             new Runnable() {
472                                 @Override
473                                 public void run() {
474                                     // trigger the event
475                                     mActivity.runOnUiThread(new Runnable() {
476                                         @Override
477                                         public void run() {
478                                             // trigger the event
479                                             notificationManager
480                                                     .notify(notificationId, notification);
481                                             mActivity.finish();
482                                         }
483                                     });
484                                 }
485                             },
486                             new UiAutomation.AccessibilityEventFilter() {
487                                 // check the received event
488                                 @Override
489                                 public boolean accept(AccessibilityEvent event) {
490                                     return equalsAccessiblityEvent(event, expected);
491                                 }
492                             },
493                             DEFAULT_TIMEOUT_MS);
494             assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
495         } finally {
496             notificationManager.deleteNotificationChannel(channel.getId());
497         }
498     }
499 
500     @MediumTest
501     @Test
testInterrupt_notifiesService()502     public void testInterrupt_notifiesService() {
503         sInstrumentation
504                 .getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
505         InstrumentedAccessibilityService service = InstrumentedAccessibilityService.enableService(
506                 sInstrumentation, InstrumentedAccessibilityService.class);
507         try {
508             assertFalse(service.wasOnInterruptCalled());
509 
510             mActivity.runOnUiThread(() -> {
511                 AccessibilityManager accessibilityManager = (AccessibilityManager) mActivity
512                         .getSystemService(Service.ACCESSIBILITY_SERVICE);
513                 accessibilityManager.interrupt();
514             });
515 
516             Object waitObject = service.getInterruptWaitObject();
517             synchronized (waitObject) {
518                 if (!service.wasOnInterruptCalled()) {
519                     try {
520                         waitObject.wait(DEFAULT_TIMEOUT_MS);
521                     } catch (InterruptedException e) {
522                         // Do nothing
523                     }
524                 }
525             }
526             assertTrue(service.wasOnInterruptCalled());
527         } finally {
528             service.disableSelfAndRemove();
529         }
530     }
531 
532     @MediumTest
533     @Test
testPackageNameCannotBeFaked()534     public void testPackageNameCannotBeFaked() throws Exception {
535         mActivity.runOnUiThread(() -> {
536             // Set the activity to report fake package for events and nodes
537             mActivity.setReportedPackageName("foo.bar.baz");
538 
539             // Make sure node package cannot be faked
540             AccessibilityNodeInfo root = sUiAutomation
541                     .getRootInActiveWindow();
542             assertPackageName(root, mActivity.getPackageName());
543         });
544 
545         // Make sure event package cannot be faked
546         try {
547             sUiAutomation.executeAndWaitForEvent(() ->
548                 sInstrumentation.runOnMainSync(() ->
549                     mActivity.findViewById(R.id.button).requestFocus())
550                 , (AccessibilityEvent event) ->
551                     event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
552                             && event.getPackageName().equals(mActivity.getPackageName())
553                 , DEFAULT_TIMEOUT_MS);
554         } catch (TimeoutException e) {
555             fail("Events from fake package should be fixed to use the correct package");
556         }
557     }
558 
559     @AppModeFull
560     @MediumTest
561     @Presubmit
562     @Test
testPackageNameCannotBeFakedAppWidget()563     public void testPackageNameCannotBeFakedAppWidget() throws Exception {
564         if (!hasAppWidgets()) {
565             return;
566         }
567 
568         sInstrumentation.runOnMainSync(() -> {
569             // Set the activity to report fake package for events and nodes
570             mActivity.setReportedPackageName(APP_WIDGET_PROVIDER_PACKAGE);
571 
572             // Make sure we cannot report nodes as if from the widget package
573             AccessibilityNodeInfo root = sUiAutomation
574                     .getRootInActiveWindow();
575             assertPackageName(root, mActivity.getPackageName());
576         });
577 
578         // Make sure we cannot send events as if from the widget package
579         try {
580             sUiAutomation.executeAndWaitForEvent(() ->
581                 sInstrumentation.runOnMainSync(() ->
582                     mActivity.findViewById(R.id.button).requestFocus())
583                 , (AccessibilityEvent event) ->
584                     event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
585                             && event.getPackageName().equals(mActivity.getPackageName())
586                 , DEFAULT_TIMEOUT_MS);
587         } catch (TimeoutException e) {
588             fail("Should not be able to send events from a widget package if no widget hosted");
589         }
590 
591         // Create a host and start listening.
592         final AppWidgetHost host = new AppWidgetHost(sInstrumentation.getTargetContext(), 0);
593         host.deleteHost();
594         host.startListening();
595 
596         // Well, app do not have this permission unless explicitly granted
597         // by the user. Now we will pretend for the user and grant it.
598         grantBindAppWidgetPermission();
599 
600         // Allocate an app widget id to bind.
601         final int appWidgetId = host.allocateAppWidgetId();
602         try {
603             // Grab a provider we defined to be bound.
604             final AppWidgetProviderInfo provider = getAppWidgetProviderInfo();
605 
606             // Bind the widget.
607             final boolean widgetBound = getAppWidgetManager().bindAppWidgetIdIfAllowed(
608                     appWidgetId, provider.getProfile(), provider.provider, null);
609             assertTrue(widgetBound);
610 
611             // Make sure the app can use the package of a widget it hosts
612             sInstrumentation.runOnMainSync(() -> {
613                 // Make sure we can report nodes as if from the widget package
614                 AccessibilityNodeInfo root = sUiAutomation
615                         .getRootInActiveWindow();
616                 assertPackageName(root, APP_WIDGET_PROVIDER_PACKAGE);
617             });
618 
619             // Make sure we can send events as if from the widget package
620             try {
621                 sUiAutomation.executeAndWaitForEvent(() ->
622                     sInstrumentation.runOnMainSync(() ->
623                         mActivity.findViewById(R.id.button).performClick())
624                     , (AccessibilityEvent event) ->
625                             event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED
626                                     && event.getPackageName().equals(APP_WIDGET_PROVIDER_PACKAGE)
627                     , DEFAULT_TIMEOUT_MS);
628             } catch (TimeoutException e) {
629                 fail("Should be able to send events from a widget package if widget hosted");
630             }
631         } finally {
632             // Clean up.
633             host.deleteAppWidgetId(appWidgetId);
634             host.deleteHost();
635             revokeBindAppWidgetPermission();
636         }
637     }
638 
639     @MediumTest
640     @Presubmit
641     @Test
testViewHeadingReportedToAccessibility()642     public void testViewHeadingReportedToAccessibility() throws Exception {
643         final EditText editText = (EditText) getOnMain(sInstrumentation, () -> {
644             return mActivity.findViewById(R.id.edittext);
645         });
646         // Make sure the edittext was populated properly from xml
647         final boolean editTextIsHeading = getOnMain(sInstrumentation, () -> {
648             return editText.isAccessibilityHeading();
649         });
650         assertTrue("isAccessibilityHeading not populated properly from xml", editTextIsHeading);
651 
652         final AccessibilityNodeInfo editTextNode = sUiAutomation.getRootInActiveWindow()
653                 .findAccessibilityNodeInfosByViewId(
654                         "android.accessibilityservice.cts:id/edittext")
655                 .get(0);
656         assertTrue("isAccessibilityHeading not reported to accessibility",
657                 editTextNode.isHeading());
658 
659         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() ->
660                         editText.setAccessibilityHeading(false)),
661                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
662                 DEFAULT_TIMEOUT_MS);
663         editTextNode.refresh();
664         assertFalse("isAccessibilityHeading not reported to accessibility after update",
665                 editTextNode.isHeading());
666     }
667 
668     @MediumTest
669     @Presubmit
670     @Test
testTooltipTextReportedToAccessibility()671     public void testTooltipTextReportedToAccessibility() {
672         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
673                 .findAccessibilityNodeInfosByViewId(
674                         "android.accessibilityservice.cts:id/buttonWithTooltip")
675                 .get(0);
676         assertEquals("Tooltip text not reported to accessibility",
677                 sInstrumentation.getContext().getString(R.string.button_tooltip),
678                 buttonNode.getTooltipText());
679     }
680 
681     @MediumTest
682     @Test
testTooltipTextActionsReportedToAccessibility()683     public void testTooltipTextActionsReportedToAccessibility() throws Exception {
684         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
685                 .findAccessibilityNodeInfosByViewId(
686                         "android.accessibilityservice.cts:id/buttonWithTooltip")
687                 .get(0);
688         assertFalse(hasTooltipShowing(R.id.buttonWithTooltip));
689         assertThat(ACTION_SHOW_TOOLTIP, in(buttonNode.getActionList()));
690         assertThat(ACTION_HIDE_TOOLTIP, not(in(buttonNode.getActionList())));
691         sUiAutomation.executeAndWaitForEvent(() -> buttonNode.performAction(
692                 ACTION_SHOW_TOOLTIP.getId()),
693                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
694                 DEFAULT_TIMEOUT_MS);
695 
696         // The button should now be showing the tooltip, so it should have the option to hide it.
697         buttonNode.refresh();
698         assertThat(ACTION_HIDE_TOOLTIP, in(buttonNode.getActionList()));
699         assertThat(ACTION_SHOW_TOOLTIP, not(in(buttonNode.getActionList())));
700         assertTrue(hasTooltipShowing(R.id.buttonWithTooltip));
701     }
702 
703     @MediumTest
704     @Test
testTraversalBeforeReportedToAccessibility()705     public void testTraversalBeforeReportedToAccessibility() throws Exception {
706         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
707                 .findAccessibilityNodeInfosByViewId(
708                         "android.accessibilityservice.cts:id/buttonWithTooltip")
709                 .get(0);
710         final AccessibilityNodeInfo beforeNode = buttonNode.getTraversalBefore();
711         assertThat(beforeNode, notNullValue());
712         assertThat(beforeNode.getViewIdResourceName(),
713                 equalTo("android.accessibilityservice.cts:id/edittext"));
714 
715         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(
716                 () -> mActivity.findViewById(R.id.buttonWithTooltip)
717                         .setAccessibilityTraversalBefore(View.NO_ID)),
718                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
719                 DEFAULT_TIMEOUT_MS);
720 
721         buttonNode.refresh();
722         assertThat(buttonNode.getTraversalBefore(), nullValue());
723     }
724 
725     @MediumTest
726     @Test
testTraversalAfterReportedToAccessibility()727     public void testTraversalAfterReportedToAccessibility() throws Exception {
728         final AccessibilityNodeInfo editNode = sUiAutomation.getRootInActiveWindow()
729                 .findAccessibilityNodeInfosByViewId(
730                         "android.accessibilityservice.cts:id/edittext")
731                 .get(0);
732         final AccessibilityNodeInfo afterNode = editNode.getTraversalAfter();
733         assertThat(afterNode, notNullValue());
734         assertThat(afterNode.getViewIdResourceName(),
735                 equalTo("android.accessibilityservice.cts:id/buttonWithTooltip"));
736 
737         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(
738                 () -> mActivity.findViewById(R.id.edittext)
739                         .setAccessibilityTraversalAfter(View.NO_ID)),
740                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
741                 DEFAULT_TIMEOUT_MS);
742 
743         editNode.refresh();
744         assertThat(editNode.getTraversalAfter(), nullValue());
745     }
746 
747     @MediumTest
748     @Test
testLabelForReportedToAccessibility()749     public void testLabelForReportedToAccessibility() throws Exception {
750         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() -> mActivity
751                 .findViewById(R.id.edittext).setLabelFor(R.id.buttonWithTooltip)),
752                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
753                 DEFAULT_TIMEOUT_MS);
754         // TODO: b/78022650: This code should move above the executeAndWait event. It's here because
755         // the a11y cache doesn't get notified when labelFor changes, so the node with the
756         // labledBy isn't updated.
757         final AccessibilityNodeInfo editNode = sUiAutomation.getRootInActiveWindow()
758                 .findAccessibilityNodeInfosByViewId(
759                         "android.accessibilityservice.cts:id/edittext")
760                 .get(0);
761         editNode.refresh();
762         final AccessibilityNodeInfo labelForNode = editNode.getLabelFor();
763         assertThat(labelForNode, notNullValue());
764         // Labeled node should indicate that it is labeled by the other one
765         assertThat(labelForNode.getLabeledBy(), equalTo(editNode));
766     }
767 
768     @MediumTest
769     @Test
testA11yActionTriggerMotionEventActionOutside()770     public void testA11yActionTriggerMotionEventActionOutside() throws Exception {
771         final View.OnTouchListener listener = mock(View.OnTouchListener.class);
772         final AccessibilityNodeInfo button = sUiAutomation.getRootInActiveWindow()
773                 .findAccessibilityNodeInfosByViewId(
774                         "android.accessibilityservice.cts:id/button")
775                 .get(0);
776         final String title = sInstrumentation.getContext().getString(R.string.alert_title);
777 
778         // Add a dialog that is watching outside touch
779         sUiAutomation.executeAndWaitForEvent(
780                 () -> sInstrumentation.runOnMainSync(() -> {
781                             final AlertDialog dialog = new AlertDialog.Builder(mActivity)
782                                     .setTitle(R.string.alert_title)
783                                     .setMessage(R.string.alert_message)
784                                     .create();
785                             final Window window = dialog.getWindow();
786                             window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
787                                     | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
788                             window.getDecorView().setOnTouchListener(listener);
789                             window.setTitle(title);
790                             dialog.show();
791                     }),
792                 (event) -> {
793                     // Ensure the dialog is shown over the activity
794                     final AccessibilityWindowInfo dialog = findWindowByTitle(
795                             sUiAutomation, title);
796                     final AccessibilityWindowInfo activity = findWindowByTitle(
797                             sUiAutomation, getActivityTitle(sInstrumentation, mActivity));
798                     return (dialog != null && activity != null)
799                             && (dialog.getLayer() > activity.getLayer());
800                 }, DEFAULT_TIMEOUT_MS);
801 
802         // Perform an action and wait for an event
803         sUiAutomation.executeAndWaitForEvent(
804                 () -> button.performAction(AccessibilityNodeInfo.ACTION_CLICK),
805                 filterForEventType(AccessibilityEvent.TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS);
806 
807         // Make sure the MotionEvent.ACTION_OUTSIDE is received.
808         verify(listener, timeout(DEFAULT_TIMEOUT_MS).atLeastOnce()).onTouch(any(View.class),
809                 argThat(event -> event.getActionMasked() == MotionEvent.ACTION_OUTSIDE));
810     }
811 
812     @MediumTest
813     @Test
testTouchDelegateInfoReportedToAccessibility()814     public void testTouchDelegateInfoReportedToAccessibility() {
815         final Button button = getOnMain(sInstrumentation, () -> mActivity.findViewById(
816                 R.id.button));
817         final View parent = (View) button.getParent();
818         final Rect rect = new Rect();
819         button.getHitRect(rect);
820         parent.setTouchDelegate(new TouchDelegate(rect, button));
821 
822         final AccessibilityNodeInfo nodeInfo = sUiAutomation.getRootInActiveWindow()
823                 .findAccessibilityNodeInfosByViewId(
824                         "android.accessibilityservice.cts:id/buttonLayout")
825                 .get(0);
826         AccessibilityNodeInfo.TouchDelegateInfo targetMapInfo =
827                 nodeInfo.getTouchDelegateInfo();
828         assertNotNull("Did not receive TouchDelegate target map", targetMapInfo);
829         assertEquals("Incorrect target map size", 1, targetMapInfo.getRegionCount());
830         assertEquals("Incorrect target map region", new Region(rect),
831                 targetMapInfo.getRegionAt(0));
832         final AccessibilityNodeInfo node = targetMapInfo.getTargetForRegion(
833                 targetMapInfo.getRegionAt(0));
834         assertEquals("Incorrect target map view",
835                 "android.accessibilityservice.cts:id/button",
836                 node.getViewIdResourceName());
837         node.recycle();
838     }
839 
840     @MediumTest
841     @Test
testTouchDelegateWithEbtBetweenView_ReHoverDelegate_FocusTargetAgain()842     public void testTouchDelegateWithEbtBetweenView_ReHoverDelegate_FocusTargetAgain()
843             throws Throwable {
844         mActivity.waitForEnterAnimationComplete();
845 
846         final Resources resources = sInstrumentation.getTargetContext().getResources();
847         final String buttonResourceName = resources.getResourceName(R.id.button);
848         final Button button = mActivity.findViewById(R.id.button);
849         final int[] buttonLocation = new int[2];
850         button.getLocationOnScreen(buttonLocation);
851         final int buttonX = button.getWidth() / 2;
852         final int buttonY = button.getHeight() / 2;
853         final int hoverY = buttonLocation[1] + buttonY;
854         final Button buttonWithTooltip = mActivity.findViewById(R.id.buttonWithTooltip);
855         final int touchableSize = 48;
856         final int hoverRight = buttonWithTooltip.getLeft() + touchableSize / 2;
857         final int hoverLeft = button.getRight() + touchableSize / 2;
858         final int hoverMiddle = (hoverLeft + hoverRight) / 2;
859         final View.OnHoverListener listener = CtsMouseUtil.installHoverListener(button, false);
860         enableTouchExploration(sInstrumentation, true);
861 
862         try {
863             // common downTime for touch explorer injected events
864             final long downTime = SystemClock.uptimeMillis();
865             // hover through delegate, parent, 2nd view, parent and delegate again
866             sUiAutomation.executeAndWaitForEvent(
867                     () -> injectHoverEvent(downTime, false, hoverLeft, hoverY),
868                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
869                             buttonResourceName), DEFAULT_TIMEOUT_MS);
870             assertTrue(button.isHovered());
871             sUiAutomation.executeAndWaitForEvent(
872                     () -> {
873                         injectHoverEvent(downTime, true, hoverMiddle, hoverY);
874                         injectHoverEvent(downTime, true, hoverRight, hoverY);
875                         injectHoverEvent(downTime, true, hoverMiddle, hoverY);
876                         injectHoverEvent(downTime, true, hoverLeft, hoverY);
877                     },
878                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
879                             buttonResourceName), DEFAULT_TIMEOUT_MS);
880             // delegate target has a11y focus again
881             assertTrue(button.isHovered());
882 
883             CtsMouseUtil.clearHoverListener(button);
884             View.OnHoverListener verifier = inOrder(listener).verify(listener);
885             verifier.onHover(eq(button),
886                     matchHover(MotionEvent.ACTION_HOVER_ENTER, buttonX, buttonY));
887             verifier.onHover(eq(button),
888                     matchHover(MotionEvent.ACTION_HOVER_MOVE, buttonX, buttonY));
889             verifier.onHover(eq(button),
890                     matchHover(MotionEvent.ACTION_HOVER_MOVE, hoverMiddle, buttonY));
891             verifier.onHover(eq(button),
892                     matchHover(MotionEvent.ACTION_HOVER_EXIT, buttonX, buttonY));
893             verifier.onHover(eq(button),
894                     matchHover(MotionEvent.ACTION_HOVER_ENTER, buttonX, buttonY));
895             verifier.onHover(eq(button),
896                     matchHover(MotionEvent.ACTION_HOVER_MOVE, buttonX, buttonY));
897         } catch (TimeoutException e) {
898             fail("Accessibility events should be received as expected " + e.getMessage());
899         } finally {
900             enableTouchExploration(sInstrumentation, false);
901         }
902     }
903 
904     @MediumTest
905     @Test
testTouchDelegateCoverParentWithEbt_HoverChildAndBack_FocusTargetAgain()906     public void testTouchDelegateCoverParentWithEbt_HoverChildAndBack_FocusTargetAgain()
907             throws Throwable {
908         mActivity.waitForEnterAnimationComplete();
909 
910         final int touchableSize = 48;
911         final Resources resources = sInstrumentation.getTargetContext().getResources();
912         final String targetResourceName = resources.getResourceName(R.id.buttonDelegated);
913         final View textView = mActivity.findViewById(R.id.delegateText);
914         final Button target = mActivity.findViewById(R.id.buttonDelegated);
915         int[] location = new int[2];
916         textView.getLocationOnScreen(location);
917         final int textX = location[0] + touchableSize/2;
918         final int textY = location[1] + textView.getHeight() / 2;
919         final int delegateX = location[0] - touchableSize/2;
920         final int targetX = target.getWidth() / 2;
921         final int targetY = target.getHeight() / 2;
922         final View.OnHoverListener listener = CtsMouseUtil.installHoverListener(target, false);
923         enableTouchExploration(sInstrumentation, true);
924 
925         try {
926             final long downTime = SystemClock.uptimeMillis();
927             // Like switch bar, it has a text view, a button and a delegate covers parent layout.
928             // hover the delegate, text and delegate again.
929             sUiAutomation.executeAndWaitForEvent(
930                     () -> injectHoverEvent(downTime, false, delegateX, textY),
931                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
932                            targetResourceName), DEFAULT_TIMEOUT_MS);
933             assertTrue(target.isHovered());
934             sUiAutomation.executeAndWaitForEvent(
935                     () -> injectHoverEvent(downTime, true, textX, textY),
936                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT,
937                            targetResourceName), DEFAULT_TIMEOUT_MS);
938             sUiAutomation.executeAndWaitForEvent(
939                     () -> injectHoverEvent(downTime, true, delegateX, textY),
940                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
941                            targetResourceName), DEFAULT_TIMEOUT_MS);
942             assertTrue(target.isHovered());
943 
944             CtsMouseUtil.clearHoverListener(target);
945             View.OnHoverListener verifier = inOrder(listener).verify(listener);
946             verifier.onHover(eq(target),
947                     matchHover(MotionEvent.ACTION_HOVER_ENTER, targetX, targetY));
948             verifier.onHover(eq(target),
949                     matchHover(MotionEvent.ACTION_HOVER_MOVE, targetX, targetY));
950             verifier.onHover(eq(target),
951                     matchHover(MotionEvent.ACTION_HOVER_MOVE, textX, textY));
952             verifier.onHover(eq(target),
953                     matchHover(MotionEvent.ACTION_HOVER_EXIT, targetX, targetY));
954             verifier.onHover(eq(target),
955                     matchHover(MotionEvent.ACTION_HOVER_ENTER, targetX, targetY));
956             verifier.onHover(eq(target),
957                     matchHover(MotionEvent.ACTION_HOVER_MOVE, targetX, targetY));
958         } catch (TimeoutException e) {
959             fail("Accessibility events should be received as expected " + e.getMessage());
960         } finally {
961             enableTouchExploration(sInstrumentation, false);
962         }
963     }
964 
assertPackageName(AccessibilityNodeInfo node, String packageName)965     private static void assertPackageName(AccessibilityNodeInfo node, String packageName) {
966         if (node == null) {
967             return;
968         }
969         assertEquals(packageName, node.getPackageName());
970         final int childCount = node.getChildCount();
971         for (int i = 0; i < childCount; i++) {
972             AccessibilityNodeInfo child = node.getChild(i);
973             if (child != null) {
974                 assertPackageName(child, packageName);
975             }
976         }
977     }
978 
enableTouchExploration(Instrumentation instrumentation, boolean enabled)979     private static void enableTouchExploration(Instrumentation instrumentation, boolean enabled)
980             throws InterruptedException {
981         final int TIMEOUT_FOR_SERVICE_ENABLE = 10000; // millis; 10s
982         final Object waitObject = new Object();
983         final AtomicBoolean atomicBoolean = new AtomicBoolean(!enabled);
984         AccessibilityManager.TouchExplorationStateChangeListener serviceListener = (boolean b) -> {
985             synchronized (waitObject) {
986                 atomicBoolean.set(b);
987                 waitObject.notifyAll();
988             }
989         };
990         final AccessibilityManager manager =
991                 (AccessibilityManager) instrumentation.getContext().getSystemService(
992                         Service.ACCESSIBILITY_SERVICE);
993         manager.addTouchExplorationStateChangeListener(serviceListener);
994 
995         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
996         final AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
997         assert info != null;
998         if (enabled) {
999             info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
1000         } else {
1001             info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
1002         }
1003         uiAutomation.setServiceInfo(info);
1004 
1005         final long timeoutTime = System.currentTimeMillis() + TIMEOUT_FOR_SERVICE_ENABLE;
1006         synchronized (waitObject) {
1007             while ((enabled != atomicBoolean.get()) && (System.currentTimeMillis() < timeoutTime)) {
1008                 waitObject.wait(timeoutTime - System.currentTimeMillis());
1009             }
1010         }
1011         if (enabled) {
1012             assertTrue("Touch exploration state listener not called when services enabled",
1013                     atomicBoolean.get());
1014             assertTrue("Timed out enabling accessibility",
1015                     manager.isEnabled() && manager.isTouchExplorationEnabled());
1016         } else {
1017             assertFalse("Touch exploration state listener not called when services disabled",
1018                     atomicBoolean.get());
1019             assertFalse("Timed out disabling accessibility",
1020                     manager.isEnabled() && manager.isTouchExplorationEnabled());
1021         }
1022         manager.removeTouchExplorationStateChangeListener(serviceListener);
1023     }
1024 
matchHover(int action, int x, int y)1025     private static MotionEvent matchHover(int action, int x, int y) {
1026         return argThat(new CtsMouseUtil.PositionMatcher(action, x, y));
1027     }
1028 
injectHoverEvent(long downTime, boolean isFirstHoverEvent, int xOnScreen, int yOnScreen)1029     private static void injectHoverEvent(long downTime, boolean isFirstHoverEvent,
1030             int xOnScreen, int yOnScreen) {
1031         final long eventTime = isFirstHoverEvent ? SystemClock.uptimeMillis() : downTime;
1032         MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_HOVER_MOVE,
1033                 xOnScreen, yOnScreen, 0);
1034         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
1035         sUiAutomation.injectInputEvent(event, true);
1036         event.recycle();
1037     }
1038 
getAppWidgetProviderInfo()1039     private AppWidgetProviderInfo getAppWidgetProviderInfo() {
1040         final ComponentName componentName = new ComponentName(
1041                 "foo.bar.baz", "foo.bar.baz.MyAppWidgetProvider");
1042         final List<AppWidgetProviderInfo> providers = getAppWidgetManager().getInstalledProviders();
1043         final int providerCount = providers.size();
1044         for (int i = 0; i < providerCount; i++) {
1045             final AppWidgetProviderInfo provider = providers.get(i);
1046             if (componentName.equals(provider.provider)
1047                     && Process.myUserHandle().equals(provider.getProfile())) {
1048                 return provider;
1049             }
1050         }
1051         return null;
1052     }
1053 
grantBindAppWidgetPermission()1054     private void grantBindAppWidgetPermission() throws Exception {
1055         ShellCommandBuilder.execShellCommand(sUiAutomation,
1056                 GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND + getCurrentUser());
1057     }
1058 
revokeBindAppWidgetPermission()1059     private void revokeBindAppWidgetPermission() throws Exception {
1060         ShellCommandBuilder.execShellCommand(sUiAutomation,
1061                 REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND + getCurrentUser());
1062     }
1063 
getAppWidgetManager()1064     private AppWidgetManager getAppWidgetManager() {
1065         return (AppWidgetManager) sInstrumentation.getTargetContext()
1066                 .getSystemService(Context.APPWIDGET_SERVICE);
1067     }
1068 
hasAppWidgets()1069     private boolean hasAppWidgets() {
1070         return sInstrumentation.getTargetContext().getPackageManager()
1071                 .hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS);
1072     }
1073 
1074     /**
1075      * Compares all properties of the <code>first</code> and the
1076      * <code>second</code>.
1077      */
equalsAccessiblityEvent(AccessibilityEvent first, AccessibilityEvent second)1078     private boolean equalsAccessiblityEvent(AccessibilityEvent first, AccessibilityEvent second) {
1079          return first.getEventType() == second.getEventType()
1080             && first.isChecked() == second.isChecked()
1081             && first.getCurrentItemIndex() == second.getCurrentItemIndex()
1082             && first.isEnabled() == second.isEnabled()
1083             && first.getFromIndex() == second.getFromIndex()
1084             && first.getItemCount() == second.getItemCount()
1085             && first.isPassword() == second.isPassword()
1086             && first.getRemovedCount() == second.getRemovedCount()
1087             && first.isScrollable()== second.isScrollable()
1088             && first.getToIndex() == second.getToIndex()
1089             && first.getRecordCount() == second.getRecordCount()
1090             && first.getScrollX() == second.getScrollX()
1091             && first.getScrollY() == second.getScrollY()
1092             && first.getAddedCount() == second.getAddedCount()
1093             && TextUtils.equals(first.getBeforeText(), second.getBeforeText())
1094             && TextUtils.equals(first.getClassName(), second.getClassName())
1095             && TextUtils.equals(first.getContentDescription(), second.getContentDescription())
1096             && equalsNotificationAsParcelableData(first, second)
1097             && equalsText(first, second);
1098     }
1099 
1100     /**
1101      * Compares the {@link android.os.Parcelable} data of the
1102      * <code>first</code> and <code>second</code>.
1103      */
equalsNotificationAsParcelableData(AccessibilityEvent first, AccessibilityEvent second)1104     private boolean equalsNotificationAsParcelableData(AccessibilityEvent first,
1105             AccessibilityEvent second) {
1106         Notification firstNotification = (Notification) first.getParcelableData();
1107         Notification secondNotification = (Notification) second.getParcelableData();
1108         if (firstNotification == null) {
1109             return (secondNotification == null);
1110         } else if (secondNotification == null) {
1111             return false;
1112         }
1113         return TextUtils.equals(firstNotification.tickerText, secondNotification.tickerText);
1114     }
1115 
1116     /**
1117      * Compares the text of the <code>first</code> and <code>second</code> text.
1118      */
equalsText(AccessibilityEvent first, AccessibilityEvent second)1119     private boolean equalsText(AccessibilityEvent first, AccessibilityEvent second) {
1120         List<CharSequence> firstText = first.getText();
1121         List<CharSequence> secondText = second.getText();
1122         if (firstText.size() != secondText.size()) {
1123             return false;
1124         }
1125         Iterator<CharSequence> firstIterator = firstText.iterator();
1126         Iterator<CharSequence> secondIterator = secondText.iterator();
1127         for (int i = 0; i < firstText.size(); i++) {
1128             if (!firstIterator.next().toString().equals(secondIterator.next().toString())) {
1129                 return false;
1130             }
1131         }
1132         return true;
1133     }
1134 
hasTooltipShowing(int id)1135     private boolean hasTooltipShowing(int id) {
1136         return getOnMain(sInstrumentation, () -> {
1137             final View viewWithTooltip = mActivity.findViewById(id);
1138             if (viewWithTooltip == null) {
1139                 return false;
1140             }
1141             final View tooltipView = viewWithTooltip.getTooltipView();
1142             return (tooltipView != null) && (tooltipView.getParent() != null);
1143         });
1144     }
1145 
getCurrentUser()1146     private static int getCurrentUser() {
1147         return android.os.Process.myUserHandle().getIdentifier();
1148     }
1149 }
1150