1 /* 2 * Copyright (C) 2024 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.platform.systemui_tapl.ui 18 19 import android.graphics.Point 20 import android.graphics.PointF 21 import android.graphics.Rect 22 import android.os.RemoteException 23 import android.os.SystemClock 24 import android.platform.systemui_tapl.controller.LockscreenController 25 import android.platform.systemui_tapl.controller.NotificationIdentity 26 import android.platform.systemui_tapl.ui.ExpandedBubbleStack.Companion.BUBBLE_EXPANDED_VIEW 27 import android.platform.systemui_tapl.utils.DeviceUtils.LONG_WAIT 28 import android.platform.systemui_tapl.utils.DeviceUtils.sysuiResSelector 29 import android.platform.uiautomatorhelpers.BetterSwipe 30 import android.platform.uiautomatorhelpers.DeviceHelpers 31 import android.platform.uiautomatorhelpers.DeviceHelpers.assertInvisible 32 import android.platform.uiautomatorhelpers.DeviceHelpers.assertVisible 33 import android.platform.uiautomatorhelpers.DeviceHelpers.betterSwipe 34 import android.platform.uiautomatorhelpers.DeviceHelpers.uiDevice 35 import android.platform.uiautomatorhelpers.FLING_GESTURE_INTERPOLATOR 36 import android.platform.uiautomatorhelpers.TracingUtils.trace 37 import android.view.InputDevice 38 import android.view.InputEvent 39 import android.view.KeyCharacterMap 40 import android.view.KeyEvent 41 import android.view.WindowInsets 42 import android.view.WindowManager 43 import android.view.WindowMetrics 44 import androidx.test.platform.app.InstrumentationRegistry 45 import androidx.test.uiautomator.By 46 import androidx.test.uiautomator.UiSelector 47 import androidx.test.uiautomator.Until 48 import com.android.launcher3.tapl.LauncherInstrumentation 49 import com.android.launcher3.tapl.Workspace 50 import com.google.common.truth.Truth.assertThat 51 import java.time.Duration 52 import org.junit.Assert 53 54 /** 55 * The root class for System UI test automation objects. All System UI test automation objects are 56 * produced by this class or other System UI test automation objects. 57 */ 58 class Root private constructor() { 59 60 /** 61 * Opens the notification shade. Use this if there is no need to assert the way of opening it. 62 * 63 * Uses AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS to open the shade, because it turned 64 * out to be more reliable than swipe gestures. Note that GLOBAL_ACTION_NOTIFICATIONS won't open 65 * notifications shade if the lockscreen screen is shown. 66 */ openNotificationShadenull67 fun openNotificationShade(): NotificationShade { 68 return openNotificationShadeViaGlobalAction() 69 } 70 71 /** Opens the notification shade via AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS. */ openNotificationShadeViaGlobalActionnull72 fun openNotificationShadeViaGlobalAction(): NotificationShade { 73 trace("Opening notification shade via global action") { 74 uiDevice.openNotification() 75 waitForShadeToOpen() 76 return NotificationShade() 77 } 78 } 79 80 /** Opens the notification shade via two fingers wipe. */ openNotificationShadeViaTwoFingersSwipenull81 fun openNotificationShadeViaTwoFingersSwipe(): NotificationShade { 82 return openNotificationShadeViaTwoFingersSwipe(Duration.ofMillis(300)) 83 } 84 85 /** Opens the notification shade via slow swipe. */ openNotificationShadeViaSlowSwipenull86 fun openNotificationShadeViaSlowSwipe(): NotificationShade { 87 return openNotificationShadeViaSwipe(Duration.ofMillis(3000)) 88 } 89 90 /** 91 * Opens the notification shade via swipe with a default speed of 500ms and default start point 92 * of 10% of the display height. NOTE: with b/277063189, the default start point of a quarter of 93 * the way down the screen can overlap a widget and the shade won't open. 94 * 95 * @param swipeDuration amount of time the swipe will last from start to finish 96 * @param heightFraction fraction of the height of the display to start from. 97 */ 98 @JvmOverloads openNotificationShadeViaSwipenull99 fun openNotificationShadeViaSwipe( 100 swipeDuration: Duration = Duration.ofMillis(500), 101 heightFraction: Float = 0.1F, 102 ): NotificationShade { 103 trace("Opening notification shade via swipe") { 104 val device = uiDevice 105 val width = device.displayWidth.toFloat() 106 val height = device.displayHeight.toFloat() 107 BetterSwipe.from(PointF(width / 2, height * heightFraction)) 108 .to(PointF(width / 2, height), swipeDuration, FLING_GESTURE_INTERPOLATOR) 109 .release() 110 waitForShadeToOpen() 111 return NotificationShade() 112 } 113 } 114 115 /** 116 * Opens the notification shade via swipe from top of screen. Needed for opening shade while in 117 * an app. 118 */ openNotificationShadeViaSwipeFromTopnull119 fun openNotificationShadeViaSwipeFromTop(): NotificationShade { 120 val device = uiDevice 121 // Swipe in first quarter to avoid desktop windowing app handle interactions. 122 val swipeXCoordinate = (device.displayWidth / 4).toFloat() 123 val height = device.displayHeight.toFloat() 124 BetterSwipe.from(PointF(swipeXCoordinate, 0f)) 125 .to( 126 PointF(swipeXCoordinate, height), 127 Duration.ofMillis(500), 128 FLING_GESTURE_INTERPOLATOR, 129 ) 130 .release() 131 waitForShadeToOpen() 132 return NotificationShade() 133 } 134 135 /** Opens the notification shade via swipe. */ openNotificationShadeViaTwoFingersSwipenull136 private fun openNotificationShadeViaTwoFingersSwipe( 137 swipeDuration: Duration 138 ): NotificationShade { 139 val device = uiDevice 140 val width = device.displayWidth 141 val distance = device.displayHeight / 3 * 2 142 // Steps are injected about 5 milliseconds apart 143 val steps = swipeDuration.toMillisPart() / 5 144 val resId = "com.google.android.apps.nexuslauncher:id/workspace" 145 // Wait is only available for UiObject2 146 DeviceHelpers.waitForObj(By.res(resId)) 147 val obj = device.findObject(UiSelector().resourceId(resId)) 148 obj.performTwoPointerGesture( 149 Point(width / 3, 0), 150 Point(width / 3 * 2, 0), 151 Point(width / 3, distance), 152 Point(width / 3 * 2, distance), 153 steps, 154 ) 155 waitForShadeToOpen() 156 return NotificationShade() 157 } 158 159 /** 160 * Finds a HUN by its identity. Fails if the notification can't be found. 161 * 162 * @param identity The NotificationIdentity used to find the HUN 163 * @param assertIsHunState When it's true, findHeadsUpNotification would fail if the 164 * notification is not at the HUN state (eg. showing in the Shade), or its HUN state cannot be 165 * verified. An action button is necessary for the verification. Consider posting the HUN with 166 * NotificationController#postBigTextHeadsUpNotification if you need to assert the HUN state. 167 * Expanded HUN state cannot be asserted. 168 */ 169 @JvmOverloads findHeadsUpNotificationnull170 fun findHeadsUpNotification( 171 identity: NotificationIdentity, 172 assertIsHunState: Boolean = true, 173 ): Notification { 174 return NotificationStack.findHeadsUpNotification( 175 identity = identity, 176 assertIsHunState = assertIsHunState, 177 ) 178 } 179 180 /** 181 * Ensures there is not a HUN with this identity. Fails if the HUN is found, or the identity 182 * doesn't have an action button. 183 * 184 * @param identity The NotificationIdentity used to find the HUN, an action button is necessary 185 */ 186 // TODO(b/295209746): More robust (and more performant) assertion for "HUN does not appear" ensureNoHeadsUpNotificationnull187 fun ensureNoHeadsUpNotification(identity: NotificationIdentity) { 188 Assert.assertTrue( 189 "HUN state Assertion usage error: Notification: ${identity.title} " + 190 "| You can only assert the HUN State of a notification that has an action " + 191 "button.", 192 identity.hasAction, 193 ) 194 Assert.assertThrows(IllegalStateException::class.java) { 195 findHeadsUpNotification(identity, assertIsHunState = false) 196 } 197 } 198 199 /** Opens the quick settings via AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS. */ openQuickSettingsViaGlobalActionnull200 fun openQuickSettingsViaGlobalAction(): QuickSettings { 201 val device = uiDevice 202 device.openQuickSettings() 203 // Quick Settings isn't always open when this is complete. Explicitly wait for the Quick 204 // Settings footer to make sure that the buttons are accessible when the bar is open and 205 // this call is complete. 206 FOOTER_SELECTOR.assertVisible() 207 // Wait an extra bit for the animation to complete. If we return to early, future callers 208 // that are trying to find the location of the footer will get incorrect coordinates 209 device.waitForIdle(LONG_TIMEOUT.toLong()) 210 return QuickSettings() 211 } 212 213 /** Gets status bar. */ 214 val statusBar: StatusBar 215 get() = StatusBar() 216 217 /** Gets an alert dialog. */ 218 val alertDialog: AlertDialog 219 get() = AlertDialog() 220 221 /** Gets a media projection permission dialog. */ 222 val mediaProjectionPermissionDialog: MediaProjectionPermissionDialog 223 get() = MediaProjectionPermissionDialog() 224 225 /** Gets a media projection app selector. */ 226 val mediaProjectionAppSelector: MediaProjectionAppSelector 227 get() = MediaProjectionAppSelector() 228 229 /** Asserts that the media projection permission dialog is not visible. */ assertMediaProjectionPermissionDialogNotVisiblenull230 fun assertMediaProjectionPermissionDialogNotVisible() { 231 MediaProjectionPermissionDialog.assertSpinnerVisibility(false) 232 } 233 234 /** Gets lock screen. Fails if lock screen is not visible. */ 235 val lockScreen: LockScreen 236 get() = LockScreen() 237 238 /** Gets primary bouncer. Fails if the primary bouncer is not visible. */ 239 val primaryBouncer: Bouncer 240 get() = Bouncer(null) 241 242 /** Gets Aod. Fails if Aod is not visible. */ 243 val aod: Aod 244 get() = Aod() 245 246 /** Gets ChooseScreenLock. Fails if ChooseScreenLock is not visible. */ 247 val chooseScreenLock: ChooseScreenLock 248 get() = ChooseScreenLock() 249 250 /** Gets the bubble. Fails if there is no bubble. */ 251 val bubble: Bubble 252 get() { 253 val bubbleViews = Bubble.bubbleViews 254 return Bubble(bubbleViews[0]) 255 } 256 257 /** 258 * Returns the selected bubble. 259 * 260 * Bubbles in the collapsed stack are reversed. The selected bubble is the last bubble in the 261 * view hierarchy. 262 */ 263 val selectedBubble: Bubble 264 get() { 265 val bubbleViews = Bubble.bubbleViews 266 return Bubble(bubbleViews.last()) 267 } 268 269 /** Gets the expanded bubble stack. Fails if no stack or if the stack is not expanded. */ 270 val expandedBubbleStack: ExpandedBubbleStack 271 get() = ExpandedBubbleStack() 272 273 /** Gets the collapsed bubble bar in launcher. */ 274 val bubbleBar: BubbleBar 275 get() = BubbleBar() 276 277 /** Verifies that the bubble bar is hidden. */ verifyBubbleBarIsHiddennull278 fun verifyBubbleBarIsHidden() { 279 BubbleBar.BUBBLE_BAR_VIEW.assertInvisible(LONG_WAIT) 280 } 281 282 /** Verifies that no bubbles or an expanded bubble stack are visible. */ verifyNoBubbleIsVisiblenull283 fun verifyNoBubbleIsVisible() { 284 Bubble.BUBBLE_VIEW.assertInvisible(timeout = Bubble.FIND_OBJECT_TIMEOUT) 285 verifyNoExpandedBubbleStackIsVisible() 286 } 287 288 /** Verifies that expanded bubble stack is not visible. */ verifyNoExpandedBubbleStackIsVisiblenull289 fun verifyNoExpandedBubbleStackIsVisible() { 290 BUBBLE_EXPANDED_VIEW.assertInvisible(timeout = Bubble.FIND_OBJECT_TIMEOUT) 291 } 292 293 /** Verifies that status bar is hidden by checking StatusBar's clock icon whether it exists. */ verifyStatusBarIsHiddennull294 fun verifyStatusBarIsHidden() { 295 assertThat( 296 uiDevice.wait( 297 Until.gone(sysuiResSelector(StatusBar.CLOCK_ID)), 298 SHORT_TIMEOUT.toLong(), 299 ) 300 ) 301 .isTrue() 302 } 303 304 /** Takes a screenshot and returns the actions panel that appears. */ screenshotnull305 fun screenshot(): ScreenshotActions { 306 val device = uiDevice 307 device.pressKeyCode(KeyEvent.KEYCODE_SYSRQ) 308 check( 309 device.wait(Until.hasObject(GLOBAL_SCREENSHOT_SELECTOR), SCREENSHOT_POST_TIMEOUT_MSEC) 310 ) { 311 "Can't find screenshot image" 312 } 313 return ScreenshotActions() 314 } 315 316 /** Gets the power panel. Fails if there is no power panel visible. */ 317 val powerPanel: PowerPanel 318 get() = PowerPanel() 319 320 /** 321 * Goes to Launcher workspace by sending KeyEvent.KEYCODE_HOME. This method is not 322 * representative of real user's actions, but it's more stable than 323 * LauncherInstrumentation.goHome because LauncherInstrumentation.goHome expects all prior 324 * animations to settle before it's used, which is true for Launcher tests that use it, but not 325 * necessarily true for SysUI tests. 326 * 327 * @return the Workspace object. 328 */ goHomeViaKeycodenull329 fun goHomeViaKeycode(): Workspace { 330 uiDevice.pressHome() 331 // getWorkspace will check `expectedRotation` and fail if it doesn't match the one from 332 // the device. However, if the test has an Orientation annotation, the orientation won't 333 // be fixed back until after this is run, possibly failing the test. 334 val instrumentation = LauncherInstrumentation() 335 instrumentation.setExpectedRotation(uiDevice.displayRotation) 336 return instrumentation.getWorkspace() 337 } 338 wakeUpnull339 private fun wakeUp() { 340 try { 341 uiDevice.wakeUp() 342 } catch (e: RemoteException) { 343 e.printStackTrace() 344 } 345 } 346 347 /** Returns the volume dialog or fails if it's invisible. */ 348 val volumeDialog: VolumeDialog 349 get() = VolumeDialog() 350 351 /** Asserts that the volume dialog is not visible. */ assertVolumeDialogNotVisiblenull352 fun assertVolumeDialogNotVisible() { 353 VolumeDialog.PAGE_TITLE_SELECTOR.assertInvisible() 354 } 355 356 /** Asserts that lock screen is invisible. */ assertLockScreenNotVisiblenull357 fun assertLockScreenNotVisible() { 358 LockScreen.LOCKSCREEN_SELECTOR.assertInvisible() 359 } 360 361 // TODO (b/277105514): Determine whether this is an idiomatic method of determing visibility. 362 /** Asserts that launcher is visible. */ assertLauncherVisiblenull363 fun assertLauncherVisible() { 364 By.pkg("com.google.android.apps.nexuslauncher").assertVisible() 365 } 366 367 val keyboardBacklightIndicatorDialog: KeyboardBacklightIndicatorDialog 368 get() = KeyboardBacklightIndicatorDialog() 369 assertKeyboardBacklightIndicatorDialogNotVisiblenull370 fun assertKeyboardBacklightIndicatorDialogNotVisible() { 371 KeyboardBacklightIndicatorDialog.CONTAINER_SELECTOR.assertInvisible() 372 } 373 injectEventSyncnull374 private fun injectEventSync(event: InputEvent): Boolean { 375 return InstrumentationRegistry.getInstrumentation() 376 .uiAutomation 377 .injectInputEvent(event, true) 378 } 379 sendKeynull380 private fun sendKey(keyCode: Int, metaState: Int, eventTime: Long): Boolean { 381 val downEvent = 382 KeyEvent( 383 eventTime, 384 eventTime, 385 KeyEvent.ACTION_DOWN, 386 keyCode, 387 0, 388 metaState, 389 KeyCharacterMap.VIRTUAL_KEYBOARD, 390 0, 391 0, 392 InputDevice.SOURCE_KEYBOARD, 393 ) 394 if (injectEventSync(downEvent)) { 395 val upEvent = 396 KeyEvent( 397 eventTime, 398 eventTime, 399 KeyEvent.ACTION_UP, 400 keyCode, 401 0, 402 metaState, 403 KeyCharacterMap.VIRTUAL_KEYBOARD, 404 0, 405 0, 406 InputDevice.SOURCE_KEYBOARD, 407 ) 408 if (injectEventSync(upEvent)) { 409 return true 410 } 411 } 412 return false 413 } 414 pressKeyCodenull415 private fun pressKeyCode(keyCode: Int, eventTime: Long) { 416 sendKey(keyCode, /* metaState= */ 0, eventTime) 417 } 418 419 /** Double-taps the power button. Can be used to bring up the camera app. */ doubleTapPowerButtonnull420 fun doubleTapPowerButton() { 421 val eventTime = SystemClock.uptimeMillis() 422 pressKeyCode(KeyEvent.KEYCODE_POWER, eventTime) 423 pressKeyCode(KeyEvent.KEYCODE_POWER, eventTime + 1) 424 } 425 426 /** Opens the tutorial by swiping. */ openTutorialViaSwipenull427 fun openTutorialViaSwipe(): OneHandModeTutorial { 428 NotificationShade.waitForShadeToClose() 429 val windowMetrics: WindowMetrics = 430 DeviceHelpers.context 431 .getSystemService(WindowManager::class.java)!! 432 .getCurrentWindowMetrics() 433 val insets: WindowInsets = windowMetrics.getWindowInsets() 434 val displayBounds: Rect = windowMetrics.getBounds() 435 val bottomMandatoryGestureHeight: Int = 436 insets 437 .getInsetsIgnoringVisibility( 438 WindowInsets.Type.navigationBars() or WindowInsets.Type.displayCutout() 439 ) 440 .bottom 441 NotificationShade.waitForShadeToClose() 442 uiDevice.betterSwipe( 443 displayBounds.width() / 2, 444 displayBounds.height() - Math.round(bottomMandatoryGestureHeight * 2.5f), 445 displayBounds.width() / 2, 446 displayBounds.height(), 447 ) 448 NotificationShade.waitForShadeToClose() 449 return OneHandModeTutorial() 450 } 451 452 /** 453 * Turn the device off and on, and check for the lockScreen. This should be used instead of 454 * LockscreenUtils.goToLockScreen() because LockscreenController validates that the screen is 455 * off or on, rather than just sleeping and waking up the device. "return lockScreen" calls the 456 * LockScreen constructor, which ensures that the lockscreen clock is visible 457 * 458 * TODO: replace LockscreenUtils.goToLockscreen() with this once it's submitted: b/322870306 459 */ goToLockscreennull460 fun goToLockscreen(): LockScreen { 461 LockscreenController.get().turnScreenOff() 462 LockscreenController.get().turnScreenOn() 463 return lockScreen 464 } 465 466 companion object { 467 private val QS_HEADER_SELECTOR = 468 if (com.android.systemui.Flags.sceneContainer()) { 469 sysuiResSelector("shade_header_root") 470 } else { 471 sysuiResSelector("split_shade_status_bar") 472 } 473 private val NOTIFICATION_SHADE_OPEN_TIMEOUT = Duration.ofSeconds(20) 474 private const val LONG_TIMEOUT = 2000 475 private const val SHORT_TIMEOUT = 500 476 private val FOOTER_SELECTOR = sysuiResSelector("qs_footer_actions") 477 private const val SCREENSHOT_POST_TIMEOUT_MSEC: Long = 20000 478 private val GLOBAL_SCREENSHOT_SELECTOR = sysuiResSelector("screenshot_actions") 479 480 /** Returns an instance of Root. */ 481 @JvmStatic getnull482 fun get(): Root { 483 return Root() 484 } 485 waitForShadeToOpennull486 private fun waitForShadeToOpen() { 487 // Note that this duplicates the tracing done by assertVisible, but with a better name. 488 trace("waitForShadeToOpen") { 489 QS_HEADER_SELECTOR.assertVisible( 490 timeout = NOTIFICATION_SHADE_OPEN_TIMEOUT, 491 errorProvider = { "Notification shade didn't open" }, 492 ) 493 } 494 } 495 } 496 } 497