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