• 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.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