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.platform.systemui_tapl.utils.DeviceUtils.sysuiResSelector
20 import android.platform.systemui_tapl.utils.SETTINGS_PACKAGE
21 import android.platform.test.scenario.tapl_common.Gestures
22 import android.platform.test.scenario.tapl_common.Gestures.click
23 import android.platform.uiautomatorhelpers.DeviceHelpers.assertVisible
24 import android.platform.uiautomatorhelpers.DeviceHelpers.waitForObj
25 import android.platform.uiautomatorhelpers.WaitUtils.ensureThat
26 import android.text.TextUtils
27 import androidx.test.uiautomator.By
28 import androidx.test.uiautomator.BySelector
29 import androidx.test.uiautomator.UiObject2
30 import com.google.common.truth.Truth.assertWithMessage
31 import kotlin.reflect.KClass
32
33 /**
34 * Object to encapsulate a tile. See https://hsv.googleplex.com/4910828112314368
35 *
36 * In order to interact with the tile, [getBehavior] needs to be called, with the type of behavior
37 * needed. There are also convenience methods for calling [click], [toggleAndAssertToggled], and
38 * [longPress]. These methods will fail the test if the tile doesn't support that interaction.
39 */
40 abstract class ComposeQuickSettingsTile private constructor() {
41 /**
42 * Representation of the tile object. This should be made to retrieve the object every time (if
43 * possible) to prevent stale objects.
44 */
45 protected abstract val tile: UiObject2
46
47 /** Whether the tile is small (icon only) or large (icon + text). */
48 val isSmallTile: Boolean
49 get() {
50 val res = tile.resourceName
51 return when {
52 res.endsWith(SMALL_TILE_TAG) -> true
53 res.endsWith(LARGE_TILE_TAG) -> false
54 else -> error("Tile doesn't have a valid resource name: $res")
55 }
56 }
57
58 /** The human readable name of the tile. */
59 val tileName: String
60 get() =
61 if (!TextUtils.isEmpty(tile.contentDescription)) {
62 tile.contentDescription
63 } else {
64 tile.getTextFromSelfOrChild()
65 }
66
67 /**
68 * Get a specific behavior for the tile, by class. Prefer using [getBehavior] as it will return
69 * a casted value. This will be `null` if the tile does not support that behavior.
70 *
71 * The behavior is created new every time it's requested, tied to the backing UiObject2, so
72 * prefer creating it every time it's needed instead of storing it.
73 */
getBehaviornull74 fun <T : TileBehavior> getBehavior(behaviorType: KClass<T>): TileBehavior? {
75 return when (behaviorType) {
76 Toggleable::class -> tile.takeIf { it.isCheckable }?.let { ToggleableImpl(it) }
77 LongPressable::class ->
78 tile.takeIf { it.isLongClickable }?.let { LongPressableImpl(it) }
79 Clickable::class -> tile.takeIf { it.isClickable }?.let { ClickableImpl(it) }
80 DualTarget::class -> tile.findObject(INNER_TARGET_SELECTOR)?.let { DualTargetImpl(it) }
81 ToggleableDualTarget::class ->
82 tile.findObject(INNER_TARGET_SELECTOR)?.let {
83 it.takeIf { it.isCheckable }?.let { DualTargetImpl(it) }
84 }
85 else -> null
86 }
87 }
88
89 /** See [getBehavior]. */
getBehaviornull90 inline fun <reified T : TileBehavior> getBehavior(): T? {
91 return getBehavior(T::class) as? T
92 }
93
94 /**
95 * Perform a click on the tile with no validation of the effect. This will fail if the tile does
96 * not support [Clickable].
97 *
98 * See [Clickable.click]
99 */
clicknull100 fun click() {
101 getBehavior<Clickable>()!!.click()
102 }
103
104 /**
105 * Toggle the current checked state of the tile, validating that the state has changed. This
106 * will fail if the tile does not support [Toggleable].
107 *
108 * See [Toggleable.toggleAndAssertToggled]
109 */
toggleAndAssertTogglednull110 fun toggleAndAssertToggled() {
111 getBehavior<Toggleable>()!!.toggleAndAssertToggled()
112 }
113
114 /**
115 * Perform a long press on the tile, validating that [expectedSettingsPackage] (or
116 * [SETTINGS_PACKAGE] if `null`) is visible afterwards. This will fail if the tile does not
117 * support [LongPressable].
118 *
119 * See [LongPressable.longPress]
120 */
longPressnull121 fun longPress(expectedSettingsPackage: String? = null) {
122 getBehavior<LongPressable>()!!.longPress(expectedSettingsPackage)
123 }
124
125 companion object {
126 /** Create a [ComposeQuickSettingsTile] wrapper from a fixed [tile] ui object. */
createFromnull127 fun createFrom(tile: UiObject2): ComposeQuickSettingsTile {
128 return object : ComposeQuickSettingsTile() {
129 override val tile: UiObject2
130 get() = tile
131 }
132 }
133
134 /**
135 * Create a [ComposeQuickSettingsTile] wrapper based on a [selector]. The wrapper will
136 * re-fetch the ui object every time it's needed, giving more flexibility in case of stale.
137 */
createFromnull138 fun createFrom(selector: BySelector): ComposeQuickSettingsTile {
139 return object : ComposeQuickSettingsTile() {
140 override val tile: UiObject2
141 get() = waitForObj(selector)
142 }
143 }
144
145 /** See https://hsv.googleplex.com/4910828112314368?node=37 */
smallTileSelectornull146 fun smallTileSelector(description: String): BySelector {
147 return sysuiResSelector(SMALL_TILE_TAG).descStartsWith(description)
148 }
149
150 /** See https://hsv.googleplex.com/4910828112314368?node=28 */
largeTileSelectornull151 fun largeTileSelector(description: String): BySelector {
152 return sysuiResSelector(LARGE_TILE_TAG).hasChild(By.textStartsWith(description))
153 }
154
assertIsTilenull155 fun UiObject2.assertIsTile() {
156 assertWithMessage("Tile has id $resourceName which is not a tile id")
157 .that(
158 resourceName?.endsWith(SMALL_TILE_TAG) ?: false ||
159 resourceName?.endsWith(LARGE_TILE_TAG) ?: false
160 )
161 .isTrue()
162 }
163
164 const val SMALL_TILE_TAG = "qs_tile_small"
165 const val LARGE_TILE_TAG = "qs_tile_large"
166 private const val TOGGLE_TARGET_TAG = "qs_tile_toggle_target"
167
168 private val INNER_TARGET_SELECTOR = sysuiResSelector(TOGGLE_TARGET_TAG)
169 }
170 }
171
172 /** Behavior for a tile */
173 sealed interface TileBehavior
174
175 /** Behavior for clickable tiles. */
176 interface Clickable : TileBehavior {
177 /** Click on the tile. No verification is performed. */
clicknull178 fun click()
179 }
180
181 private class ClickableImpl(private val tile: UiObject2) : Clickable {
182 init {
183 check(tile.isClickable)
184 }
185
186 override fun click() {
187 click(tile, "Tile")
188 }
189 }
190
191 /**
192 * Behavior for tiles that are toggleable. This means that clicking on them will toggle them between
193 * and Off state and an On state
194 */
195 interface Toggleable : TileBehavior {
196 /** Whether the tile is currently in its On state */
197 val isChecked: Boolean
198
199 /** Toggle the tile between On/Off. Validates that the tile has changed checked state. */
toggleAndAssertTogglednull200 fun toggleAndAssertToggled()
201
202 /** Asserts the current checked state with a nice message. */
203 fun assertCheckedStatus(checked: Boolean)
204 }
205
206 private open class ToggleableImpl(private val tile: UiObject2) : Toggleable {
207 init {
208 check(tile.isCheckable)
209 check(tile.isClickable)
210 }
211
212 override val isChecked: Boolean
213 get() = tile.isChecked
214
215 override fun toggleAndAssertToggled() {
216 val wasChecked = isChecked
217 click(tile, "Tile")
218 assertCheckedStatus(!wasChecked)
219 }
220
221 override fun assertCheckedStatus(checked: Boolean) {
222 val expectedState = if (checked) "checked" else "unchecked"
223 ensureThat("tile is $expectedState") { isChecked == checked }
224 }
225 }
226
227 /** Behavior for tiles that support long press. */
228 interface LongPressable : TileBehavior {
229 /**
230 * Long press on the tile. Validates that a settings activity with the correct package was
231 * launched.
232 */
longPressnull233 fun longPress(expectedSettingsPackage: String? = null)
234 }
235
236 private class LongPressableImpl(private val tile: UiObject2) : LongPressable {
237 init {
238 check(tile.isLongClickable)
239 }
240
241 override fun longPress(expectedSettingsPackage: String?) {
242 Gestures.longClickDownUp(tile, "Quick settings tile") {
243 val packageName = expectedSettingsPackage ?: SETTINGS_PACKAGE
244 By.pkg(packageName).assertVisible { "$packageName didn't appear" }
245 }
246 }
247 }
248
249 /**
250 * Behavior for tiles that support a dual target. The dual target is not necessarily a toggle
251 * between On/Off
252 */
253 interface DualTarget : Clickable, LongPressable
254
255 /** Behavior for tiles that support a dual target that is an On/Off toggle */
256 interface ToggleableDualTarget : Toggleable, DualTarget
257
258 private class DualTargetImpl(innerTarget: UiObject2) :
259 ToggleableDualTarget,
260 Toggleable by ToggleableImpl(innerTarget),
261 Clickable by ClickableImpl(innerTarget),
262 LongPressable by LongPressableImpl(innerTarget)
263
getTextFromSelfOrChildnull264 private fun UiObject2.getTextFromSelfOrChild(): String {
265 return if (!TextUtils.isEmpty(text)) {
266 text
267 } else {
268 children.firstOrNull { !TextUtils.isEmpty(it.text) }?.text ?: ""
269 }
270 }
271