/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.platform.systemui_tapl.ui import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.os.RemoteException import android.os.SystemClock import android.os.SystemClock.sleep import android.platform.helpers.ShadeUtils import android.platform.systemui_tapl.controller.LockscreenController import android.platform.systemui_tapl.controller.NotificationIdentity import android.platform.systemui_tapl.ui.ExpandedBubbleStack.Companion.BUBBLE_EXPANDED_VIEW import android.platform.systemui_tapl.ui.NotificationShade.Companion.waitForShadeToClose import android.platform.systemui_tapl.ui.quicksettings.BrightnessSlider import android.platform.systemui_tapl.ui.quicksettings.PowerPanel import android.platform.systemui_tapl.ui.quicksettings.QuickSettings import android.platform.systemui_tapl.utils.DeviceUtils.LONG_WAIT import android.platform.systemui_tapl.utils.DeviceUtils.sysuiResSelector import android.platform.systemui_tapl.utils.LAUNCHER_PACKAGE import android.platform.uiautomatorhelpers.BetterSwipe import android.platform.uiautomatorhelpers.DeviceHelpers import android.platform.uiautomatorhelpers.DeviceHelpers.assertInvisible import android.platform.uiautomatorhelpers.DeviceHelpers.assertVisible import android.platform.uiautomatorhelpers.DeviceHelpers.betterSwipe import android.platform.uiautomatorhelpers.DeviceHelpers.uiDevice import android.platform.uiautomatorhelpers.DeviceHelpers.waitForNullableObj import android.platform.uiautomatorhelpers.DeviceHelpers.waitForObj import android.platform.uiautomatorhelpers.FailedEnsureException import android.view.Display.DEFAULT_DISPLAY import android.view.InputDevice import android.view.InputEvent import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.WindowInsets import android.view.WindowManager import android.view.WindowMetrics import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import com.android.app.tracing.traceSection import com.android.launcher3.tapl.LauncherInstrumentation import com.android.launcher3.tapl.Workspace import com.android.systemui.Flags import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import java.time.Duration import org.junit.Assert.assertThrows /** * The root class for System UI test automation objects. All System UI test automation objects are * produced by this class or other System UI test automation objects. * * @param displayId The ID of the display to interact with. */ class Root private constructor(val displayId: Int = DEFAULT_DISPLAY) { /** * Opens the notification shade. Use this if there is no need to assert the way of opening it. * * Uses AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS to open the shade, because it turned * out to be more reliable than swipe gestures. Note that GLOBAL_ACTION_NOTIFICATIONS won't open * notifications shade if the lockscreen screen is shown. */ fun openNotificationShade(): NotificationShade { if (Flags.sceneContainer()) { uiDevice.executeShellCommand("cmd statusbar expand-notifications-instant") waitForShadeToOpen() return NotificationShade(displayId) } else { return openNotificationShadeViaGlobalAction() } } /** * Opens the notification shade with retrying on failure. Use this if there is no need to assert * the way of opening it. * * This function can be used on the desktop environment. * * @return A [NotificationShade] instance representing the opened shade. * @throws IllegalStateException if the shade fails to open after all retry attempts. */ fun openNotificationShadeWithRetry(): NotificationShade { for (attempt in 1..MAX_RETRY_ATTEMPTS) { try { val shade = if (Flags.sceneContainer()) { uiDevice.executeShellCommand("cmd statusbar expand-notifications-instant") waitForNotificationStackScroller() NotificationShade(displayId) } else { openNotificationShadeViaGlobalAction() } return shade } catch (e: FailedEnsureException) { if (attempt < MAX_RETRY_ATTEMPTS) { sleep(RETRY_TIME_INTERVAL.toMillis()) } } } throw IllegalStateException( "Failed to open notification shade on display $displayId after $MAX_RETRY_ATTEMPTS " + "attempts." ) } private val notificationStackScrollerSelector = sysuiResSelector("notification_stack_scroller", displayId) private fun waitForNotificationStackScroller() { assert(Flags.sceneContainer()) traceSection("waitForNotificationStackScrollerToShow") { notificationStackScrollerSelector.assertVisible( timeout = NOTIFICATION_STACK_SCROLLER_OPEN_TIMEOUT, errorProvider = { "Notification stack scroller didn't show on display $displayId" }, ) } } /** Opens the notification shade via AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS. */ fun openNotificationShadeViaGlobalAction(): NotificationShade { traceSection("Opening shade via global action") { uiDevice.openNotification() waitForShadeToOpen() return NotificationShade(displayId) } } /** Opens the notification shade via two fingers wipe. */ fun openNotificationShadeViaTwoFingersSwipe(): NotificationShade { assertWithMessage( "two finger swipe is only supported on the default display, because it relies on " + "UiObject#performTwoPointerGesture, and UiObject has no concept of displayId " + "(unlike UiObject2)" ) .that(displayId) .isEqualTo(DEFAULT_DISPLAY) return openNotificationShadeViaTwoFingersSwipe(Duration.ofMillis(300)) } /** Opens the notification shade via slow swipe. */ fun openNotificationShadeViaSlowSwipe(): NotificationShade { return openNotificationShadeViaSwipe(Duration.ofMillis(3000)) } /** * Opens the notification shade via swipe with a default speed of 500ms and default start point * of 10% of the display height. NOTE: with b/277063189, the default start point of a quarter of * the way down the screen can overlap a widget and the shade won't open. * * @param swipeDuration amount of time the swipe will last from start to finish * @param heightFraction fraction of the height of the display to start from. */ @JvmOverloads fun openNotificationShadeViaSwipe( swipeDuration: Duration = Duration.ofMillis(500), heightFraction: Float = 0.1F, ): NotificationShade { traceSection("Opening shade via swipe") { val device = uiDevice val height = device.getDisplayHeight(displayId).toFloat() BetterSwipe.swipe( PointF(notificationSwipeX, height * heightFraction), PointF(notificationSwipeX, height), swipeDuration, displayId = displayId, ) waitForShadeToOpen() return NotificationShade(displayId) } } /** * Opens the notification shade via swipe from top of screen. Needed for opening shade while in * an app. */ fun openNotificationShadeViaSwipeFromTop(): NotificationShade { val height = uiDevice.getDisplayHeight(displayId).toFloat() BetterSwipe.swipe( PointF(notificationSwipeX, 0f), PointF(notificationSwipeX, height), displayId = displayId, ) waitForShadeToOpen() return NotificationShade(displayId) } /** Opens the notification shade via swipe. */ private fun openNotificationShadeViaTwoFingersSwipe( swipeDuration: Duration ): NotificationShade { val device = uiDevice val width = device.displayWidth val distance = device.displayHeight / 3 * 2 // Steps are injected about 5 milliseconds apart val steps = swipeDuration.toMillisPart() / 5 val resId = "com.google.android.apps.nexuslauncher:id/workspace" // Wait is only available for UiObject2 DeviceHelpers.waitForObj(By.res(resId)) val obj = device.findObject(UiSelector().resourceId(resId)) obj.performTwoPointerGesture( Point(width / 3, 0), Point(width / 3 * 2, 0), Point(width / 3, distance), Point(width / 3 * 2, distance), steps, ) waitForShadeToOpen() return NotificationShade() } /** Opens notification shade via 3-finger swipe on trackpad */ fun openNotificationShadeViaTrackpadSwipe(): NotificationShade { // Swiping on horizontal center, vertically from upper quarter to 3/4 down, as to // give enough space for shade to open fully, and not losing focus by touching screen edge val startX = uiDevice.getDisplayWidth(displayId) / 2f val startY = uiDevice.getDisplayHeight(displayId) / 4f val endY = uiDevice.getDisplayHeight(displayId) * 3f / 4 val deltaY = endY - startY BetterSwipe.threeFingerTrackpadSwipe( start1 = PointF(startX, startY), start2 = PointF(startX - TOUCHPAD_POINTER_SPACING, startY), start3 = PointF(startX + TOUCHPAD_POINTER_SPACING, startY), delta = PointF(0f, deltaY), displayId = displayId, ) waitForShadeToOpen() return NotificationShade(displayId) } private val qsSelector = sysuiResSelector("quick_settings_panel", displayId) private val notificationSwipeX: Float get() = uiDevice.getDisplayWidth(displayId) / 4f private val qsSwipeX: Float get() = 3 * uiDevice.getDisplayWidth(displayId) / 4f /** * Finds a HUN by its identity. Fails if the notification can't be found. * * @param identity The NotificationIdentity used to find the HUN * @param assertIsHunState When it's true, findHeadsUpNotification would fail if the * notification is not at the HUN state (eg. showing in the Shade), or its HUN state cannot be * verified. An action button is necessary for the verification. Consider posting the HUN with * NotificationController#postBigTextHeadsUpNotification if you need to assert the HUN state. * Expanded HUN state cannot be asserted. * @param assertIsPromotedOngoingState see [NotificationStack.findHeadsUpNotification]. */ @JvmOverloads fun findHeadsUpNotification( identity: NotificationIdentity, assertIsHunState: Boolean = true, assertIsPromotedOngoingState: Boolean = false, ): Notification { return NotificationStack.findHeadsUpNotification( identity = identity, assertIsHunState = assertIsHunState, assertIsPromotedOngoingState = assertIsPromotedOngoingState, ) } /** * Ensures there is not a HUN with this identity. Fails if the HUN is found, or the identity * doesn't have an action button. * * @param assertIsHunState see [findHeadsUpNotification]. * @param identity The NotificationIdentity used to find the HUN, an action button is necessary */ // TODO(b/295209746): More robust (and more performant) assertion for "HUN does not appear" fun ensureNoHeadsUpNotification( identity: NotificationIdentity, assertIsHunState: Boolean = true, ) { if (assertIsHunState) { assertWithMessage( "HUN state Assertion usage error: Notification: ${identity.title} " + "| You can only assert the HUN State of a notification that has an action " + "button." ) .that(identity.hasAction) .isTrue() } assertThrows(IllegalStateException::class.java) { findHeadsUpNotification(identity, assertIsHunState = false) } } /** Opens quick settings. */ fun openQuickSettings(): QuickSettings { if (Flags.sceneContainer()) { uiDevice.executeShellCommand("cmd statusbar expand-settings-instant") } else { uiDevice.executeShellCommand("cmd statusbar expand-settings") } waitForQuickSettingsToOpen() return QuickSettings(displayId) } /** Opens quick settings with a swipe gesture that depends on form factor. */ fun openQuickSettingsWithSwipe(): QuickSettings { if (ShadeUtils.isDualShadeConfig()) { BetterSwipe.swipe( PointF(qsSwipeX, 1f), PointF(qsSwipeX, uiDevice.getDisplayHeight(displayId).toFloat() - 2f), displayId = displayId, ) } else { openNotificationShadeViaSwipeFromTop().openQuickSettingsWithSwipe() } waitForQuickSettingsToOpen() return QuickSettings(displayId) } /** Opens the quick settings via AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS. */ fun openQuickSettingsViaGlobalAction(): QuickSettings { val device = uiDevice device.openQuickSettings() // Quick Settings isn't always open when this is complete. Explicitly wait for the Quick // Settings panel to make sure that the buttons are accessible when the bar is open and // this call is complete. qsSelector.assertVisible() // Wait an extra bit for the animation to complete. If we return to early, future callers // that are trying to find the location of the footer will get incorrect coordinates device.waitForIdle(LONG_TIMEOUT) return QuickSettings(displayId) } /** Collapses any open shade, i.e. either Notifications or QS. */ fun collapseAnyShade() { if (Flags.sceneContainer()) { uiDevice.executeShellCommand("cmd statusbar collapse-instant") } else { uiDevice.executeShellCommand("cmd statusbar collapse") } waitForShadeToClose(displayId) } /** Gets status bar. */ val statusBar: StatusBar get() = StatusBar(displayId) /** Gets an alert dialog. */ val alertDialog: AlertDialog get() = AlertDialog() /** Gets a media projection permission dialog. */ val mediaProjectionPermissionDialog: MediaProjectionPermissionDialog get() = MediaProjectionPermissionDialog() /** Gets a media projection app selector. */ val mediaProjectionAppSelector: MediaProjectionAppSelector get() = MediaProjectionAppSelector() /** Asserts that the media projection permission dialog is not visible. */ fun assertMediaProjectionPermissionDialogNotVisible() { MediaProjectionPermissionDialog.assertSpinnerVisibility(false) } /** Gets lock screen. Fails if lock screen is not visible. */ val lockScreen: LockScreen get() = LockScreen(displayId) /** Gets primary bouncer. Fails if the primary bouncer is not visible. */ val primaryBouncer: Bouncer get() = Bouncer(null) /** Gets Aod. Fails if Aod is not visible. */ val aod: Aod get() = Aod(displayId) /** Gets Aod RON Skeleton. Fails if Aod is not visible. */ val aodRON: AodRON get() = AodRON() /** Gets ChooseScreenLock. Fails if ChooseScreenLock is not visible. */ val chooseScreenLock: ChooseScreenLock get() = ChooseScreenLock() /** Gets the bubble. Fails if there is no bubble. */ val bubble: Bubble get() { val bubbleViews = Bubble.bubbleViews return Bubble(bubbleViews[0]) } /** * Returns the selected bubble. * * Bubbles in the collapsed stack are reversed. The selected bubble is the last bubble in the * view hierarchy. */ val selectedBubble: Bubble get() { val bubbleViews = Bubble.bubbleViews return Bubble(bubbleViews.last()) } /** Gets the expanded bubble stack. Fails if no stack or if the stack is not expanded. */ val expandedBubbleStack: ExpandedBubbleStack get() = ExpandedBubbleStack() /** Gets the collapsed bubble bar in launcher. */ val bubbleBar: BubbleBar get() = BubbleBar() /** Get the stashed bubble bar handle in launcher */ val stashedBubbleBar: StashedBubbleBar get() = StashedBubbleBar() /** Gets the bubble bar flyout in launcher. */ val bubbleBarFlyout: BubbleBarFlyout get() = BubbleBarFlyout() /** * Try to expand the bubble bar by either clicking on the [BubbleBar] itself, or if it is not * shown, try to click on [StashedBubbleBar] handle. */ fun expandBubbleBar(): ExpandedBubbleBar { // Perform a quick check for bubble bar and handle so we don't have to wait for them to show try { if (uiDevice.hasObject(BubbleBar.BUBBLE_BAR_VIEW)) { return bubbleBar.expand() } } catch (e: FailedEnsureException) { // Bubble bar may have been animating to handle. By the time we try to click, it may be // gone, ignore the failure and try to click on the handle. } if (uiDevice.hasObject(StashedBubbleBar.HANDLE_VIEW)) { return stashedBubbleBar.click() } // Wait for bubble bar or handle to show waitForNullableObj(BubbleBar.BUBBLE_BAR_VIEW)?.let { return bubbleBar.expand() } waitForNullableObj(StashedBubbleBar.HANDLE_VIEW)?.let { return stashedBubbleBar.click() } throw AssertionError("Could not expand bubble bar as bar or handle is not visible") } /** Verifies that the bubble bar is hidden. */ fun verifyBubbleBarIsHidden() { BubbleBar.BUBBLE_BAR_VIEW.assertInvisible(LONG_WAIT) } /** Verifies that the stashed bubble bar handle is hidden */ fun verifyStashedBubbleBarHandleIsHidden() { StashedBubbleBar.HANDLE_VIEW.assertInvisible(timeout = Bubble.FIND_OBJECT_TIMEOUT) } /** Verifies that no bubbles or an expanded bubble stack are visible. */ fun verifyNoBubbleIsVisible() { Bubble.BUBBLE_VIEW.assertInvisible(timeout = Bubble.FIND_OBJECT_TIMEOUT) verifyNoExpandedBubbleIsVisible() } /** Verifies that expanded bubble stack (or bar) is not visible. */ fun verifyNoExpandedBubbleIsVisible() { BUBBLE_EXPANDED_VIEW.assertInvisible(timeout = Bubble.FIND_OBJECT_TIMEOUT) } /** Verifies that bubble flyout is not visible. Checks for both stack and bar flyout. */ fun verifyBubbleFlyoutIsHidden() { BubbleBarFlyout.BUBBLE_BAR_FLYOUT_VIEW.assertInvisible(timeout = Bubble.FIND_OBJECT_TIMEOUT) BubbleFlyout.BUBBLE_FLYOUT_TEXT_CONTAINER_VIEW.assertInvisible( timeout = Bubble.FIND_OBJECT_TIMEOUT ) } /** Verifies that status bar is hidden by checking StatusBar's clock icon whether it exists. */ fun verifyStatusBarIsHidden() { assertThat(uiDevice.wait(Until.gone(sysuiResSelector(StatusBar.CLOCK_ID)), SHORT_TIMEOUT)) .isTrue() } /** Takes a screenshot and returns the actions panel that appears. */ fun screenshot(): ScreenshotActions { val device = uiDevice device.pressKeyCode(KeyEvent.KEYCODE_SYSRQ) check( device.wait(Until.hasObject(GLOBAL_SCREENSHOT_SELECTOR), SCREENSHOT_POST_TIMEOUT_MSEC) ) { "Can't find screenshot image" } return ScreenshotActions() } /** Gets the power panel. Fails if there is no power panel visible. */ val powerPanel: PowerPanel get() = PowerPanel() /** * Goes to Launcher workspace by sending KeyEvent.KEYCODE_HOME. This method is not * representative of real user's actions, but it's more stable than * LauncherInstrumentation.goHome because LauncherInstrumentation.goHome expects all prior * animations to settle before it's used, which is true for Launcher tests that use it, but not * necessarily true for SysUI tests. * * @return the Workspace object. */ fun goHomeViaKeycode(): Workspace { uiDevice.pressHome() // getWorkspace will check `expectedRotation` and fail if it doesn't match the one from // the device. However, if the test has an Orientation annotation, the orientation won't // be fixed back until after this is run, possibly failing the test. val instrumentation = LauncherInstrumentation() instrumentation.setExpectedRotation(uiDevice.displayRotation) return instrumentation.getWorkspace() } private fun wakeUp() { try { uiDevice.wakeUp() } catch (e: RemoteException) { e.printStackTrace() } } /** Returns the volume dialog or fails if it's invisible. */ val volumeDialog: VolumeDialog get() = VolumeDialog() /** Asserts that the volume dialog is not visible. */ fun assertVolumeDialogNotVisible() { VolumeDialog.PAGE_TITLE_SELECTOR.assertInvisible() } /** Asserts that lock screen is invisible. */ fun assertLockScreenNotVisible() { LockScreen.lockScreenSelector(displayId).assertInvisible() } /** Asserts that brightness slider is not visible (i.e. when shade is on a external display.) */ fun assertBrightnessSliderNotVisible() { BrightnessSlider.sliderSelector(displayId).assertInvisible() } // TODO (b/277105514): Determine whether this is an idiomatic method of determining visibility. /** Asserts that launcher is visible. */ fun assertLauncherVisible() { By.displayId(displayId).pkg(LAUNCHER_PACKAGE).assertVisible() } // TODO (b/277105514): Determine whether this is an idiomatic method of determining visibility. /** Asserts that launcher is visible. */ fun assertLauncherNotVisible() { By.displayId(displayId).pkg(LAUNCHER_PACKAGE).assertInvisible() } val keyboardBacklightIndicatorDialog: KeyboardBacklightIndicatorDialog get() = KeyboardBacklightIndicatorDialog() fun assertKeyboardBacklightIndicatorDialogNotVisible() { KeyboardBacklightIndicatorDialog.CONTAINER_SELECTOR.assertInvisible() } fun assertShadeNotVisible() { qsHeaderSelector.assertInvisible { "Shade should not be visible" } } private val qsHeaderSelector = if (Flags.sceneContainer()) { sysuiResSelector("shade_header_root", displayId) } else { sysuiResSelector("split_shade_status_bar", displayId) } fun assertQuickSettingsNotVisible() { sysuiResSelector("quick_settings_panel", displayId).assertInvisible() } /** * Verifies that shade is open. The success condition is the visibility of the Quick Settings * (QS) header. * * NOTE: This function should not be used for desktop, as the QS header does not exist. * * @throws FailedEnsureException if the QS header does not become visible within the specified * timeout. */ fun waitForShadeToOpen() { // Note that this duplicates the tracing done by assertVisible, but with a better name. traceSection("waitForShadeToOpen") { qsHeaderSelector.assertVisible( timeout = NOTIFICATION_SHADE_OPEN_TIMEOUT, errorProvider = { "Shade didn't open on display $displayId" }, ) } } private fun waitForQuickSettingsToOpen() { waitForObj(sysuiResSelector("quick_settings_panel", displayId)) } fun assertBouncerNotVisible() { assertThrows(IllegalStateException::class.java) { primaryBouncer } } fun pressBackOnDisplay() { if (displayId == DEFAULT_DISPLAY) { LauncherInstrumentation().pressBack() } else { // replicate UiDevice#pressBack() for a display other than DEFAULT_DISPLAY sendKey(KeyEvent.KEYCODE_BACK, 0, SystemClock.uptimeMillis()) uiDevice.waitForWindowUpdate(null, LONG_TIMEOUT) } } fun pressHomeOnDisplay() { if (displayId == DEFAULT_DISPLAY) { uiDevice.pressHome() } else { // Check the display is available before attempting to send key events By.displayId(displayId).assertVisible { "Can't press home on display $displayId: display not found" } // replicate UiDevice#pressHome() for a display other than DEFAULT_DISPLAY sendKey(KeyEvent.KEYCODE_HOME, 0, SystemClock.uptimeMillis()) uiDevice.waitForWindowUpdate(LAUNCHER_PACKAGE, LONG_TIMEOUT) } } private fun injectEventSync(event: InputEvent): Boolean { return InstrumentationRegistry.getInstrumentation() .uiAutomation .injectInputEvent(event, true) } private fun createKeyEvent( keyCode: Int, metaState: Int, eventTime: Long, action: Int, ): KeyEvent = KeyEvent( eventTime, eventTime, action, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD, ) .apply { this.displayId = this@Root.displayId } private fun sendKey(keyCode: Int, metaState: Int, eventTime: Long): Boolean { val downEvent = createKeyEvent(keyCode, metaState, eventTime, KeyEvent.ACTION_DOWN) if (injectEventSync(downEvent)) { val upEvent = createKeyEvent(keyCode, metaState, eventTime, KeyEvent.ACTION_UP) if (injectEventSync(upEvent)) { return true } } return false } private fun pressKeyCode(keyCode: Int, eventTime: Long) { sendKey(keyCode, /* metaState= */ 0, eventTime) } /** Double-taps the power button. Can be used to bring up the camera app. */ fun doubleTapPowerButton() { val eventTime = SystemClock.uptimeMillis() pressKeyCode(KeyEvent.KEYCODE_POWER, eventTime) pressKeyCode(KeyEvent.KEYCODE_POWER, eventTime + 1) } /** Opens the tutorial by swiping. */ fun openTutorialViaSwipe(): OneHandModeTutorial { NotificationShade.waitForShadeToClose(displayId) val windowMetrics: WindowMetrics = DeviceHelpers.context .getSystemService(WindowManager::class.java)!! .getCurrentWindowMetrics() val insets: WindowInsets = windowMetrics.getWindowInsets() val displayBounds: Rect = windowMetrics.getBounds() val bottomMandatoryGestureHeight: Int = insets .getInsetsIgnoringVisibility( WindowInsets.Type.navigationBars() or WindowInsets.Type.displayCutout() ) .bottom NotificationShade.waitForShadeToClose(displayId) uiDevice.betterSwipe( displayBounds.width() / 2, displayBounds.height() - Math.round(bottomMandatoryGestureHeight * 2.5f), displayBounds.width() / 2, displayBounds.height(), ) NotificationShade.waitForShadeToClose(displayId) return OneHandModeTutorial() } /** * Turn the device off and on, and check for the lockScreen. This should be used instead of * LockscreenUtils.goToLockScreen() because LockscreenController validates that the screen is * off or on, rather than just sleeping and waking up the device. "return lockScreen" calls the * LockScreen constructor, which ensures that the lockscreen clock is visible * * TODO: replace LockscreenUtils.goToLockscreen() with this once it's submitted: b/322870306 */ fun goToLockscreen(): LockScreen { LockscreenController.get().turnScreenOff() LockscreenController.get().turnScreenOn() return lockScreen } companion object { private val NOTIFICATION_SHADE_OPEN_TIMEOUT = Duration.ofSeconds(20) private val NOTIFICATION_STACK_SCROLLER_OPEN_TIMEOUT = Duration.ofSeconds(5) private const val MAX_RETRY_ATTEMPTS = 3 private val RETRY_TIME_INTERVAL = Duration.ofSeconds(1) private const val LONG_TIMEOUT: Long = 2000 private const val SHORT_TIMEOUT: Long = 500 private const val SCREENSHOT_POST_TIMEOUT_MSEC: Long = 20000 private const val TOUCHPAD_POINTER_SPACING = 50 private val GLOBAL_SCREENSHOT_SELECTOR = sysuiResSelector("screenshot_actions") /** * Returns an instance of Root. * * @param displayId The display ID used for UI operations. Defaults to the default display. */ @JvmStatic @JvmOverloads fun get(displayId: Int = DEFAULT_DISPLAY): Root = Root(displayId) } }