• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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