• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 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.wm.shell.flicker.pip.tv
18 
19 import android.view.KeyEvent
20 import androidx.test.uiautomator.By
21 import androidx.test.uiautomator.BySelector
22 import androidx.test.uiautomator.UiDevice
23 import androidx.test.uiautomator.UiObject2
24 import androidx.test.uiautomator.Until
25 import com.android.wm.shell.flicker.utils.SYSTEM_UI_PACKAGE_NAME
26 
27 /** Id of the root view in the com.android.wm.shell.pip.tv.PipMenuActivity */
28 private const val TV_PIP_MENU_ROOT_ID = "tv_pip_menu"
29 private const val TV_PIP_MENU_BUTTONS_CONTAINER_ID = "tv_pip_menu_action_buttons"
30 private const val TV_PIP_MENU_CLOSE_BUTTON_ID = "tv_pip_menu_close_button"
31 private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "tv_pip_menu_fullscreen_button"
32 
33 private const val FOCUS_ATTEMPTS = 10
34 private const val WAIT_TIME_MS = 3_000L
35 
36 private val TV_PIP_MENU_SELECTOR = By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID)
37 private val TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR =
38     By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_BUTTONS_CONTAINER_ID)
39 private val TV_PIP_MENU_CLOSE_BUTTON_SELECTOR =
40     By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID)
41 private val TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR =
42     By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID)
43 
44 fun UiDevice.waitForTvPipMenu(): UiObject2? =
45     wait(Until.findObject(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS)
46 
47 fun UiDevice.waitForTvPipMenuToClose(): Boolean =
48     wait(Until.gone(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS)
49 
50 fun UiDevice.findTvPipMenuControls(): UiObject2? =
51     findTvPipMenuElement(TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR)
52 
53 fun UiDevice.findTvPipMenuCloseButton(): UiObject2? =
54     findTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR)
55 
56 fun UiDevice.findTvPipMenuFullscreenButton(): UiObject2? =
57     findTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR)
58 
59 fun UiDevice.findTvPipMenuElementWithDescription(desc: String): UiObject2? =
60     findTvPipMenuElement(By.desc(desc))
61 
62 private fun UiDevice.findTvPipMenuElement(selector: BySelector): UiObject2? =
63     findObject(TV_PIP_MENU_SELECTOR)?.findObject(selector)
64 
65 fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? {
66     // Ideally, we'd want to wait for an element with the given description that has the Pip Menu as
67     // its parent, but the API does not allow us to construct a query exactly that way.
68     // So instead we'll wait for a Pip Menu that has the element, which we are looking for, as a
69     // descendant and then retrieve the element from the menu and return to the caller of this
70     // method.
71     val elementSelector = By.desc(desc)
72     val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector)
73 
74     return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS)
75         ?.findObject(elementSelector)
76 }
77 
waitUntilTvPipMenuElementWithDescriptionIsGonenull78 fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boolean? {
79     val elementSelector = By.desc(desc)
80     val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector)
81 
82     return wait(Until.gone(menuContainingElementSelector), WAIT_TIME_MS)
83 }
84 
clickTvPipMenuCloseButtonnull85 fun UiDevice.clickTvPipMenuCloseButton() {
86     focusOnAndClickTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) ||
87         error("Could not focus on the Close button")
88 }
89 
UiDevicenull90 fun UiDevice.clickTvPipMenuFullscreenButton() {
91     focusOnAndClickTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) ||
92         error("Could not focus on the Fullscreen button")
93 }
94 
UiDevicenull95 fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) {
96     focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) ||
97         error("Could not focus on the Pip menu object with \"$desc\" description")
98     // So apparently Accessibility framework on TV is not very reliable and sometimes the state of
99     // the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of
100     // the "real" state of the "UI tree". It seems, however, that moving focus around the tree
101     // forces the AccessibilityNodeInfo tree to get properly updated.
102     // So since we suspect that clicking on a Pip Menu element may cause some UI changes and we want
103     // those changes to be seen by the UiAutomator, which is using Accessibility framework under the
104     // hood for inspecting UI, we'll move the focus around a little.
105     moveFocus()
106 }
107 
UiDevicenull108 private fun UiDevice.focusOnAndClickTvPipMenuElement(selector: BySelector): Boolean {
109     repeat(FOCUS_ATTEMPTS) {
110         val element =
111             findTvPipMenuElement(selector)
112                 ?: error("The Pip Menu element we try to focus on is gone.")
113 
114         if (element.isFocusedOrHasFocusedChild) {
115             pressDPadCenter()
116             return true
117         }
118 
119         findTvPipMenuElement(By.focused(true))?.let { focused ->
120             if (element.visibleCenter.x < focused.visibleCenter.x) pressDPadLeft()
121             else pressDPadRight()
122             waitForIdle()
123         }
124             ?: error("Pip menu does not contain a focused element")
125     }
126 
127     return false
128 }
129 
UiDevicenull130 fun UiDevice.closeTvPipWindow() {
131     // Check if Pip menu is Open. If it's not, open it.
132     if (findObject(TV_PIP_MENU_SELECTOR) == null) {
133         pressWindowKey()
134         waitForTvPipMenu() ?: error("Could not open Pip menu")
135     }
136 
137     clickTvPipMenuCloseButton()
138     waitForTvPipMenuToClose()
139 }
140 
141 /**
142  * Simply presses the D-Pad Left and Right buttons once, which should move the focus on the screen,
143  * which should cause Accessibility events to be fired, which should, hopefully, properly update
144  * AccessibilityNodeInfo tree dispatched by the platform to the Accessibility services, one of which
145  * is the UiAutomator.
146  */
UiDevicenull147 private fun UiDevice.moveFocus() {
148     waitForIdle()
149     pressDPadLeft()
150     waitForIdle()
151     pressDPadRight()
152     waitForIdle()
153 }
154 
UiDevicenull155 fun UiDevice.pressWindowKey() = pressKeyCode(KeyEvent.KEYCODE_WINDOW)
156 
157 fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean =
158     visibleBounds.run { height() == uiDevice.displayHeight && width() == uiDevice.displayWidth }
159 
160 val UiObject2.isFocusedOrHasFocusedChild: Boolean
161     get() = isFocused || findObject(By.focused(true)) != null
162