1 /*
2  * Copyright (C) 2023 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 com.android.systemui.accessibility.accessibilitymenu.tests;
18 
19 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN;
20 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS;
21 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_POWER_DIALOG;
22 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS;
23 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS;
24 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT;
25 
26 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION;
27 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA;
28 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU;
29 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU;
30 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
31 
32 import static com.google.common.truth.Truth.assertThat;
33 
34 import android.accessibilityservice.AccessibilityServiceInfo;
35 import android.app.Instrumentation;
36 import android.app.KeyguardManager;
37 import android.app.UiAutomation;
38 import android.content.BroadcastReceiver;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.IntentFilter;
42 import android.hardware.display.BrightnessInfo;
43 import android.hardware.display.DisplayManager;
44 import android.media.AudioManager;
45 import android.os.PowerManager;
46 import android.provider.Settings;
47 import android.util.Log;
48 import android.view.Display;
49 import android.view.accessibility.AccessibilityManager;
50 import android.view.accessibility.AccessibilityNodeInfo;
51 
52 import androidx.test.ext.junit.runners.AndroidJUnit4;
53 import androidx.test.platform.app.InstrumentationRegistry;
54 
55 import com.android.compatibility.common.util.TestUtils;
56 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId;
57 
58 import org.junit.After;
59 import org.junit.AfterClass;
60 import org.junit.Before;
61 import org.junit.BeforeClass;
62 import org.junit.Test;
63 import org.junit.runner.RunWith;
64 
65 import java.util.List;
66 import java.util.concurrent.atomic.AtomicInteger;
67 
68 @RunWith(AndroidJUnit4.class)
69 public class AccessibilityMenuServiceTest {
70     private static final String TAG = "A11yMenuServiceTest";
71     private static final int CLICK_ID = AccessibilityNodeInfo.ACTION_CLICK;
72 
73     private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5;
74     private static final int TIMEOUT_UI_CHANGE_S = 5;
75     private static final int NO_GLOBAL_ACTION = -1;
76     private static final String INPUT_KEYEVENT_KEYCODE_BACK = "input keyevent KEYCODE_BACK";
77     private static final String TEST_PIN = "1234";
78 
79     private static Instrumentation sInstrumentation;
80     private static UiAutomation sUiAutomation;
81     private static AtomicInteger sLastGlobalAction;
82 
83     private static AccessibilityManager sAccessibilityManager;
84     private static PowerManager sPowerManager;
85     private static KeyguardManager sKeyguardManager;
86     private static DisplayManager sDisplayManager;
87 
88     @BeforeClass
classSetup()89     public static void classSetup() throws Throwable {
90         final String serviceName = PACKAGE_NAME + "/.AccessibilityMenuService";
91         sInstrumentation = InstrumentationRegistry.getInstrumentation();
92         sUiAutomation = sInstrumentation.getUiAutomation(
93                 UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
94         sUiAutomation.adoptShellPermissionIdentity(
95                 UiAutomation.ALL_PERMISSIONS.toArray(new String[0]));
96 
97         final Context context = sInstrumentation.getTargetContext();
98         sAccessibilityManager = context.getSystemService(AccessibilityManager.class);
99         sPowerManager = context.getSystemService(PowerManager.class);
100         sKeyguardManager = context.getSystemService(KeyguardManager.class);
101         sDisplayManager = context.getSystemService(DisplayManager.class);
102 
103         // Disable all a11yServices if any are active.
104         if (!sAccessibilityManager.getEnabledAccessibilityServiceList(
105                 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty()) {
106             Settings.Secure.putString(context.getContentResolver(),
107                     Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "");
108             TestUtils.waitUntil("Failed to disable all services",
109                     TIMEOUT_SERVICE_STATUS_CHANGE_S,
110                     () -> sAccessibilityManager.getEnabledAccessibilityServiceList(
111                             AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty());
112         }
113 
114         // Enable a11yMenu service.
115         Settings.Secure.putString(context.getContentResolver(),
116                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, serviceName);
117 
118         TestUtils.waitUntil("Failed to enable service",
119                 TIMEOUT_SERVICE_STATUS_CHANGE_S,
120                 () -> sAccessibilityManager.getEnabledAccessibilityServiceList(
121                         AccessibilityServiceInfo.FEEDBACK_ALL_MASK).stream().filter(
122                                 info -> info.getId().contains(serviceName)).count() == 1);
123 
124         sLastGlobalAction = new AtomicInteger(NO_GLOBAL_ACTION);
125         context.registerReceiver(new BroadcastReceiver() {
126             @Override
127             public void onReceive(Context context, Intent intent) {
128                 Log.i(TAG, "Received global action intent.");
129                 sLastGlobalAction.set(
130                         intent.getIntExtra(INTENT_GLOBAL_ACTION_EXTRA, NO_GLOBAL_ACTION));
131             }},
132                 new IntentFilter(PACKAGE_NAME + INTENT_GLOBAL_ACTION),
133                 null, null, Context.RECEIVER_EXPORTED);
134     }
135 
136     @AfterClass
classTeardown()137     public static void classTeardown() throws Throwable {
138         clearPin();
139         Settings.Secure.putString(sInstrumentation.getTargetContext().getContentResolver(),
140                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "");
141     }
142 
143     @Before
setup()144     public void setup() throws Throwable {
145         clearPin();
146         wakeUpScreen();
147         sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
148         openMenu();
149     }
150 
151     @After
tearDown()152     public void tearDown() throws Throwable {
153         closeMenu();
154         sLastGlobalAction.set(NO_GLOBAL_ACTION);
155     }
156 
clearPin()157     private static void clearPin() throws Throwable {
158         sUiAutomation.executeShellCommand("locksettings clear --old " + TEST_PIN);
159         TestUtils.waitUntil("Device did not register as unlocked & insecure.",
160                 TIMEOUT_SERVICE_STATUS_CHANGE_S,
161                 () -> !sKeyguardManager.isDeviceSecure());
162     }
163 
setPin()164     private static void setPin() throws Throwable {
165         sUiAutomation.executeShellCommand("locksettings set-pin " + TEST_PIN);
166         TestUtils.waitUntil("Device did not recognize as locked & secure.",
167                 TIMEOUT_SERVICE_STATUS_CHANGE_S,
168                 () -> sKeyguardManager.isDeviceSecure());
169     }
170 
isMenuVisible()171     private static boolean isMenuVisible() {
172         AccessibilityNodeInfo root = sUiAutomation.getRootInActiveWindow();
173         return root != null && root.getPackageName().toString().equals(PACKAGE_NAME);
174     }
175 
wakeUpScreen()176     private static void wakeUpScreen() throws Throwable {
177         sUiAutomation.executeShellCommand("input keyevent KEYCODE_WAKEUP");
178         TestUtils.waitUntil("Screen did not wake up.",
179                 TIMEOUT_UI_CHANGE_S,
180                 () -> sPowerManager.isInteractive());
181     }
182 
closeScreen()183     private static void closeScreen() throws Throwable {
184         Display display = sDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
185         setPin();
186         sUiAutomation.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN);
187         TestUtils.waitUntil("Screen did not close.",
188                 TIMEOUT_UI_CHANGE_S,
189                 () -> !sPowerManager.isInteractive()
190                         && display.getState() == Display.STATE_OFF
191         );
192     }
193 
openMenu()194     private static void openMenu() throws Throwable {
195         Intent intent = new Intent(PACKAGE_NAME + INTENT_TOGGLE_MENU);
196         sInstrumentation.getContext().sendBroadcast(intent);
197 
198         TestUtils.waitUntil("Timed out before menu could appear.",
199                 TIMEOUT_UI_CHANGE_S,
200                 () -> {
201                     if (isMenuVisible()) {
202                         return true;
203                     } else {
204                         sInstrumentation.getContext().sendBroadcast(intent);
205                         return false;
206                     }
207                 });
208     }
209 
closeMenu()210     private static void closeMenu() throws Throwable {
211         if (!isMenuVisible()) {
212             return;
213         }
214         Intent intent = new Intent(PACKAGE_NAME + INTENT_HIDE_MENU);
215         sInstrumentation.getContext().sendBroadcast(intent);
216         TestUtils.waitUntil("Timed out before menu could close.",
217                 TIMEOUT_UI_CHANGE_S, () -> !isMenuVisible());
218     }
219 
220     /**
221      * Provides list of all present shortcut buttons.
222      * @return List of shortcut buttons.
223      */
getGridButtonList()224     private List<AccessibilityNodeInfo> getGridButtonList() {
225         return sUiAutomation.getRootInActiveWindow()
226                         .findAccessibilityNodeInfosByViewId(PACKAGE_NAME + ":id/shortcutIconBtn");
227     }
228 
229     /**
230      * Returns the first button whose uniqueID matches the provided text.
231      * @param buttons List of buttons.
232      * @param text Text to match button's uniqueID to.
233      * @return Button whose uniqueID matches text, {@code null} otherwise.
234      */
findGridButtonInfo( List<AccessibilityNodeInfo> buttons, String text)235     private AccessibilityNodeInfo findGridButtonInfo(
236             List<AccessibilityNodeInfo> buttons, String text) {
237         for (AccessibilityNodeInfo button: buttons) {
238             if (button.getUniqueId().equals(text)) {
239                 return button;
240             }
241         }
242         return null;
243     }
244 
245     @Test
testAdjustBrightness()246     public void testAdjustBrightness() throws Throwable {
247         Context context = sInstrumentation.getTargetContext();
248         DisplayManager displayManager = context.getSystemService(
249                 DisplayManager.class);
250         float resetBrightness = displayManager.getBrightness(context.getDisplayId());
251 
252         List<AccessibilityNodeInfo> buttons = getGridButtonList();
253         AccessibilityNodeInfo brightnessUpButton = findGridButtonInfo(buttons,
254                 String.valueOf(ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()));
255         AccessibilityNodeInfo brightnessDownButton = findGridButtonInfo(buttons,
256                 String.valueOf(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal()));
257 
258         BrightnessInfo brightnessInfo = displayManager.getDisplay(
259                 context.getDisplayId()).getBrightnessInfo();
260 
261         try {
262             displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum);
263             TestUtils.waitUntil("Could not change to minimum brightness",
264                     TIMEOUT_UI_CHANGE_S,
265                     () -> displayManager.getBrightness(context.getDisplayId())
266                             == brightnessInfo.brightnessMinimum);
267             brightnessUpButton.performAction(CLICK_ID);
268             TestUtils.waitUntil("Did not detect an increase in brightness.",
269                     TIMEOUT_UI_CHANGE_S,
270                     () -> displayManager.getBrightness(context.getDisplayId())
271                             > brightnessInfo.brightnessMinimum);
272 
273             displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum);
274             TestUtils.waitUntil("Could not change to maximum brightness",
275                     TIMEOUT_UI_CHANGE_S,
276                     () -> displayManager.getBrightness(context.getDisplayId())
277                             == brightnessInfo.brightnessMaximum);
278             brightnessDownButton.performAction(CLICK_ID);
279             TestUtils.waitUntil("Did not detect a decrease in brightness.",
280                     TIMEOUT_UI_CHANGE_S,
281                     () -> displayManager.getBrightness(context.getDisplayId())
282                             < brightnessInfo.brightnessMaximum);
283         } finally {
284             displayManager.setBrightness(context.getDisplayId(), resetBrightness);
285         }
286     }
287 
288     @Test
testAdjustVolume()289     public void testAdjustVolume() throws Throwable {
290         Context context = sInstrumentation.getTargetContext();
291         AudioManager audioManager = context.getSystemService(AudioManager.class);
292         int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
293 
294         List<AccessibilityNodeInfo> buttons = getGridButtonList();
295         AccessibilityNodeInfo volumeUpButton = findGridButtonInfo(buttons,
296                 String.valueOf(ShortcutId.ID_VOLUME_UP_VALUE.ordinal()));
297         AccessibilityNodeInfo volumeDownButton = findGridButtonInfo(buttons,
298                 String.valueOf(ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal()));
299 
300         try {
301             int min = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
302             audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, min,
303                     AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
304             TestUtils.waitUntil("Could not change audio stream to minimum volume.",
305                     TIMEOUT_UI_CHANGE_S,
306                     () -> audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) == min);
307             volumeUpButton.performAction(CLICK_ID);
308             TestUtils.waitUntil("Did not detect an increase in volume.",
309                     TIMEOUT_UI_CHANGE_S,
310                     () -> audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) > min);
311 
312             int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
313             audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, max,
314                     AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
315             TestUtils.waitUntil("Could not change audio stream to maximum volume.",
316                     TIMEOUT_UI_CHANGE_S,
317                     () -> audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) == max);
318             volumeDownButton.performAction(CLICK_ID);
319             TestUtils.waitUntil("Did not detect a decrease in volume.",
320                     TIMEOUT_UI_CHANGE_S,
321                     () -> audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < max);
322         } finally {
323             audioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
324                     resetVolume, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
325         }
326     }
327 
328     @Test
testAssistantButton_opensVoiceAssistant()329     public void testAssistantButton_opensVoiceAssistant() throws Throwable {
330         AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(),
331                 String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal()));
332         Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND);
333         String expectedPackage = expectedIntent.resolveActivity(
334                 sInstrumentation.getContext().getPackageManager()).getPackageName();
335 
336         sUiAutomation.executeAndWaitForEvent(
337                 () -> assistantButton.performAction(CLICK_ID),
338                 (event) -> expectedPackage.contains(event.getPackageName()),
339                 TIMEOUT_UI_CHANGE_S * 1000
340         );
341     }
342 
343     @Test
testAccessibilitySettingsButton_opensAccessibilitySettings()344     public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable {
345         AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(),
346                 String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal()));
347         Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
348         String expectedPackage = expectedIntent.resolveActivity(
349                 sInstrumentation.getContext().getPackageManager()).getPackageName();
350 
351         sUiAutomation.executeAndWaitForEvent(
352                 () -> settingsButton.performAction(CLICK_ID),
353                 (event) -> expectedPackage.contains(event.getPackageName()),
354                 TIMEOUT_UI_CHANGE_S * 1000
355         );
356     }
357 
358     @Test
testPowerButton_performsGlobalAction()359     public void testPowerButton_performsGlobalAction() throws Throwable {
360         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
361                 String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal()));
362 
363         button.performAction(CLICK_ID);
364         TestUtils.waitUntil("Did not detect Power action being performed.",
365                 TIMEOUT_UI_CHANGE_S,
366                 () -> sLastGlobalAction.compareAndSet(
367                         GLOBAL_ACTION_POWER_DIALOG, NO_GLOBAL_ACTION));
368     }
369 
370     @Test
testRecentButton_performsGlobalAction()371     public void testRecentButton_performsGlobalAction() throws Throwable {
372         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
373                 String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal()));
374 
375         button.performAction(CLICK_ID);
376         TestUtils.waitUntil("Did not detect Recents action being performed.",
377                 TIMEOUT_UI_CHANGE_S,
378                 () -> sLastGlobalAction.compareAndSet(
379                         GLOBAL_ACTION_RECENTS, NO_GLOBAL_ACTION));
380     }
381 
382     @Test
testLockButton_performsGlobalAction()383     public void testLockButton_performsGlobalAction() throws Throwable {
384         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
385                 String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()));
386 
387         button.performAction(CLICK_ID);
388         TestUtils.waitUntil("Did not detect Lock action being performed.",
389                 TIMEOUT_UI_CHANGE_S,
390                 () -> sLastGlobalAction.compareAndSet(
391                         GLOBAL_ACTION_LOCK_SCREEN, NO_GLOBAL_ACTION));
392     }
393 
394     @Test
testQuickSettingsButton_performsGlobalAction()395     public void testQuickSettingsButton_performsGlobalAction() throws Throwable {
396         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
397                 String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal()));
398 
399         button.performAction(CLICK_ID);
400         TestUtils.waitUntil("Did not detect Quick Settings action being performed.",
401                 TIMEOUT_UI_CHANGE_S,
402                 () -> sLastGlobalAction.compareAndSet(
403                         GLOBAL_ACTION_QUICK_SETTINGS, NO_GLOBAL_ACTION));
404     }
405 
406     @Test
testNotificationsButton_performsGlobalAction()407     public void testNotificationsButton_performsGlobalAction() throws Throwable {
408         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
409                 String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal()));
410 
411         button.performAction(CLICK_ID);
412         TestUtils.waitUntil("Did not detect Notifications action being performed.",
413                 TIMEOUT_UI_CHANGE_S,
414                 () -> sLastGlobalAction.compareAndSet(
415                         GLOBAL_ACTION_NOTIFICATIONS, NO_GLOBAL_ACTION));
416     }
417 
418     @Test
testScreenshotButton_performsGlobalAction()419     public void testScreenshotButton_performsGlobalAction() throws Throwable {
420         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
421                 String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal()));
422 
423         button.performAction(CLICK_ID);
424         TestUtils.waitUntil("Did not detect Screenshot action being performed.",
425                 TIMEOUT_UI_CHANGE_S,
426                 () -> sLastGlobalAction.compareAndSet(
427                         GLOBAL_ACTION_TAKE_SCREENSHOT, NO_GLOBAL_ACTION));
428     }
429 
430     @Test
testOnScreenLock_closesMenu()431     public void testOnScreenLock_closesMenu() throws Throwable {
432         closeScreen();
433         wakeUpScreen();
434 
435         assertThat(isMenuVisible()).isFalse();
436     }
437 
438     @Test
testOnScreenLock_cannotOpenMenu()439     public void testOnScreenLock_cannotOpenMenu() throws Throwable {
440         closeScreen();
441         wakeUpScreen();
442 
443         boolean timedOut = false;
444         try {
445             openMenu();
446         } catch (AssertionError e) {
447             // Expected
448             timedOut = true;
449         }
450         assertThat(timedOut).isTrue();
451     }
452 }
453