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