1 /*
2  * Copyright 2025 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 @file:JvmName("UiDeviceExt")
17 
18 package androidx.test.uiautomator
19 
20 import android.app.Instrumentation
21 import android.os.Build
22 import android.view.KeyEvent
23 import android.view.accessibility.AccessibilityNodeInfo
24 import android.view.accessibility.AccessibilityWindowInfo
25 import androidx.test.uiautomator.internal.TimeoutClock
26 import androidx.test.uiautomator.internal.findViews
27 import androidx.test.uiautomator.internal.keyCharacterMap
28 import androidx.test.uiautomator.internal.notNull
29 import androidx.test.uiautomator.internal.uiDevice
30 
31 /** Returns all the windows on all the displays. */
<lambda>null32 public fun UiDevice.windows(): List<AccessibilityWindowInfo> = windowRoots.map { it.window }
33 
34 /** Returns the active window. */
activeWindownull35 public fun UiDevice.activeWindow(): AccessibilityWindowInfo = waitForRootInActiveWindow().window
36 
37 /**
38  * Waits for the root node to become available in this window.
39  *
40  * @param timeoutMs a timeout for the root node to become available.
41  * @param sleepIntervalMs a interval to wait before retrying checking if the node is available.
42  * @param clearCache whether the accessibility nodes cache should be cleared when checking.
43  * @return the root node for this window.
44  */
45 @JvmOverloads
46 public fun UiDevice.waitForRootInActiveWindow(
47     timeoutMs: Long = 10000L,
48     sleepIntervalMs: Long = 100L,
49     clearCache: Boolean = true,
50 ): AccessibilityNodeInfo {
51 
52     val clock = TimeoutClock(timeoutMs = timeoutMs, sleepIntervalMs = sleepIntervalMs)
53     while (uiAutomation.rootInActiveWindow == null) {
54 
55         // Clear accessibility cache: some nodes are cached and may not get updated.
56         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && clearCache) {
57             uiAutomation.clearCache()
58         }
59         if (clock.isTimeoutOrSleep()) {
60             throw IllegalStateException("Cannot acquire root view in active window.")
61         }
62     }
63     return uiAutomation.rootInActiveWindow
64 }
65 
66 /**
67  * Types the given [text] string simulating key press through [Instrumentation.sendKeySync]. This is
68  * similar to tapping the keys on a virtual keyboard and will trigger the same listeners in the
69  * target app, as opposed to [AccessibilityNodeInfo.setText] that programmaticaly sets the given
70  * text in the target node.
71  *
72  * @param text the text to type.
73  */
typenull74 public fun UiDevice.type(text: String) {
75     text
76         .flatMap { keyCharacterMap.getEvents(charArrayOf(it)).toList() }
77         .forEach { instrumentation.sendKeySync(it) }
78 }
79 
80 /**
81  * Similar to [type] but presses the delete key for the given [count] times.
82  *
83  * @param count how many times the press delete key should be pressed.
84  */
pressDeletenull85 public fun UiDevice.pressDelete(count: Int) {
86     (0 until count).map { KeyEvent.KEYCODE_DEL }.toIntArray().let { uiDevice.pressKeyCodes(it, 0) }
87 }
88 
89 /**
90  * Waits for an application to become visible. Note that internally it checks if an accessibility
91  * node with the given [appPackageName] exists in the accessibility tree.
92  *
93  * @param appPackageName the package name of the app to wait for. By default is the target app
94  *   package name.
95  * @param timeoutMs a timeout for the app to become visible.
96  * @return whether the app became visible in the given timeout.
97  */
98 @JvmOverloads
waitForAppToBeVisiblenull99 public fun UiDevice.waitForAppToBeVisible(
100     appPackageName: String,
101     timeoutMs: Long = 10000L,
102 ): Boolean = onViewOrNull(timeoutMs = timeoutMs) { packageName == appPackageName } != null
103 
104 /**
105  * Performs a DFS on the accessibility tree starting from the root node in the active window and
106  * returns the first node matching the given [block]. The node is returned as an [UiObject2] that
107  * allows interacting with it. If the requested node doesn't exist, a [ViewNotFoundException] is
108  * thrown. Internally it works searching periodically every [pollIntervalMs].
109  *
110  * Example:
111  * ```kotlin
112  * uiDevice.onView { textAsString == "Search" }.click()
113  * ```
114  *
115  * @param timeoutMs a timeout to find the view that satisfies the given condition.
116  * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for updates.
117  * @param block a block that specifies a condition on the node to find.
118  * @return a [UiObject2] from a node that matches the given [block] condition.
119  */
120 @JvmOverloads
onViewnull121 public fun UiDevice.onView(
122     timeoutMs: Long = 10000,
123     pollIntervalMs: Long = 100,
124     block: AccessibilityNodeInfo.() -> (Boolean),
125 ): UiObject2 =
126     onViewOrNull(timeoutMs = timeoutMs, pollIntervalMs = pollIntervalMs, block = block)
127         .notNull(ViewNotFoundException())
128 
129 /**
130  * Performs a DFS on the accessibility tree starting from the root node in the active window and
131  * returns the first node matching the given [block]. The node is returned as an [UiObject2] that
132  * allows interacting with it. If the requested node doesn't exist, null is returned. Internally it
133  * works searching periodically every [pollIntervalMs].
134  *
135  * Example:
136  * ```kotlin
137  * uiDevice.onView { textAsString == "Search" }.click()
138  * ```
139  *
140  * @param timeoutMs a timeout to find the view that satisfies the given condition.
141  * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for updates.
142  * @param block a block that specifies a condition on the node to find.
143  * @return a [UiObject2] from a node that matches the given [block] condition or null.
144  */
145 @JvmOverloads
146 public fun UiDevice.onViewOrNull(
147     timeoutMs: Long = 10000,
148     pollIntervalMs: Long = 100,
149     block: AccessibilityNodeInfo.() -> (Boolean),
150 ): UiObject2? =
151     findViews(
152             shouldStop = { it.size == 1 },
153             block = block,
154             timeoutMs = timeoutMs,
155             pollIntervalMs = pollIntervalMs,
<lambda>null156             rootNodeBlock = { windowRoots }
157         )
158         .firstOrNull()
159         ?.toUiObject2()
160 
161 /**
162  * Performs a DFS on the accessibility tree starting from this node and returns all the nodes
163  * matching the given [block]. This method stops waiting as soon as a single node with the given
164  * condition is returned. The nodes returned are [UiObject2] that allow interacting with them.
165  * Internally it works searching periodically every [pollIntervalMs].
166  *
167  * Example:
168  * ```kotlin
169  * node.onViews { className == Button::class.java.name }
170  * ```
171  *
172  * If multiple nodes are expected but they appear at different times, it's recommended to call
173  * [androidx.test.uiautomator.waitForStable] before, to ensure any operation is complete.
174  *
175  * @param timeoutMs a timeout to find the view that satisfies the given condition.
176  * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for updates.
177  * @param block a block that specifies a condition on the node to find.
178  * @return a list of [UiObject2] from nodes that matches the given [block] condition.
179  */
180 @JvmOverloads
onViewsnull181 public fun UiDevice.onViews(
182     timeoutMs: Long = 10000,
183     pollIntervalMs: Long = 100,
184     block: AccessibilityNodeInfo.() -> (Boolean),
185 ): List<UiObject2> =
186     findViews(
187             shouldStop = { false },
188             block = block,
189             timeoutMs = timeoutMs,
190             pollIntervalMs = pollIntervalMs,
<lambda>null191             rootNodeBlock = { windowRoots }
192         )
<lambda>null193         .mapNotNull { it.toUiObject2() }
194