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