1 /*
2  * Copyright (C) 2022 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.uiautomator_helpers
18 
19 import android.os.SystemClock.uptimeMillis
20 import android.animation.TimeInterpolator
21 import android.app.Instrumentation
22 import android.content.Context
23 import android.graphics.PointF
24 import android.os.Bundle
25 import android.platform.uiautomator_helpers.TracingUtils.trace
26 import android.platform.uiautomator_helpers.WaitUtils.ensureThat
27 import android.platform.uiautomator_helpers.WaitUtils.waitFor
28 import android.platform.uiautomator_helpers.WaitUtils.waitForNullable
29 import android.platform.uiautomator_helpers.WaitUtils.waitForPossibleEmpty
30 import android.platform.uiautomator_helpers.WaitUtils.waitForValueToSettle
31 import android.util.Log
32 import androidx.test.platform.app.InstrumentationRegistry
33 import androidx.test.uiautomator.BySelector
34 import androidx.test.uiautomator.UiDevice
35 import androidx.test.uiautomator.UiObject2
36 import java.io.IOException
37 import java.time.Duration
38 
39 private const val TAG = "DeviceHelpers"
40 
41 object DeviceHelpers {
42     private val SHORT_WAIT = Duration.ofMillis(1500)
43     private val LONG_WAIT = Duration.ofSeconds(10)
44     private val DOUBLE_TAP_INTERVAL = Duration.ofMillis(100)
45 
46     private val instrumentationRegistry = InstrumentationRegistry.getInstrumentation()
47 
48     @JvmStatic
49     val uiDevice: UiDevice
50         get() = UiDevice.getInstance(instrumentationRegistry)
51 
52     @JvmStatic
53     val context: Context
54         get() = instrumentationRegistry.targetContext
55 
56     /**
57      * Waits for an object to be visible and returns it.
58      *
59      * Throws an error with message provided by [errorProvider] if the object is not found.
60      */
61     @Deprecated(
62         "Use [DeviceHelpers.waitForObj] instead.",
63         ReplaceWith("DeviceHelpers.waitForObj(selector, timeout, errorProvider)")
64     )
waitForObjnull65     fun UiDevice.waitForObj(
66         selector: BySelector,
67         timeout: Duration = LONG_WAIT,
68         errorProvider: () -> String = { "Object $selector not found" },
69     ): UiObject2 = DeviceHelpers.waitForObj(selector, timeout, errorProvider)
70 
71     /**
72      * Waits for an object to be visible and returns it.
73      *
74      * Throws an error with message provided by [errorProvider] if the object is not found.
75      */
76     @JvmOverloads
77     @JvmStatic
waitForObjnull78     fun waitForObj(
79         selector: BySelector,
80         timeout: Duration = LONG_WAIT,
81         errorProvider: () -> String = { "Object $selector not found" },
82     ): UiObject2 =
<lambda>null83         waitFor("$selector object", timeout, errorProvider) { uiDevice.findObject(selector) }
84 
85     /**
86      * Waits for an object to be visible and returns it.
87      *
88      * Throws an error with message provided by [errorProvider] if the object is not found.
89      */
waitForObjnull90     fun UiObject2.waitForObj(
91         selector: BySelector,
92         timeout: Duration = LONG_WAIT,
93         errorProvider: () -> String = { "Object $selector not found" },
<lambda>null94     ): UiObject2 = waitFor("$selector object", timeout, errorProvider) { findObject(selector) }
95 
96     /**
97      * Waits for an object to be visible and returns it. Returns `null` if the object is not found.
98      */
99     @Deprecated(
100         "Use [DeviceHelpers.waitForNullableObj] instead.",
101         ReplaceWith("DeviceHelpers.waitForNullableObj(selector, timeout)")
102     )
waitForNullableObjnull103     fun UiDevice.waitForNullableObj(
104         selector: BySelector,
105         timeout: Duration = SHORT_WAIT,
106     ): UiObject2? = DeviceHelpers.waitForNullableObj(selector, timeout)
107 
108     /**
109      * Waits for an object to be visible and returns it. Returns `null` if the object is not found.
110      */
111     fun waitForNullableObj(
112         selector: BySelector,
113         timeout: Duration = SHORT_WAIT,
114     ): UiObject2? =
115         waitForNullable("nullable $selector objects", timeout) { uiDevice.findObject(selector) }
116 
117     /**
118      * Waits for an object to be visible and returns it. Returns `null` if the object is not found.
119      */
waitForNullableObjnull120     fun UiObject2.waitForNullableObj(
121         selector: BySelector,
122         timeout: Duration = SHORT_WAIT,
123     ): UiObject2? = waitForNullable("nullable $selector objects", timeout) { findObject(selector) }
124 
125     /**
126      * Waits for objects matched by [selector] to be visible and returns them. Returns `null` if no
127      * objects are found
128      */
129     @Deprecated(
130         "Use DeviceHelpers.waitForPossibleEmpty",
131         ReplaceWith(
132             "waitForPossibleEmpty(selector, timeout)",
133             "android.platform.uiautomator_helpers.DeviceHelpers.waitForPossibleEmpty"
134         )
135     )
waitForNullableObjectsnull136     fun waitForNullableObjects(
137         selector: BySelector,
138         timeout: Duration = SHORT_WAIT,
139     ): List<UiObject2>? = waitForPossibleEmpty(selector, timeout)
140 
141     /**
142      * Waits for objects matched by selector to be visible. Returns an empty list when none is
143      * visible.
144      */
145     fun waitForPossibleEmpty(
146         selector: BySelector,
147         timeout: Duration = SHORT_WAIT,
148     ): List<UiObject2> =
149         waitForPossibleEmpty("$selector objects", timeout) { uiDevice.findObjects(selector) }
150 
151     /**
152      * Waits for objects matched by [selector] to be visible and returns them. Returns `null` if no
153      * objects are found
154      */
155     @Deprecated(
156         "Use DeviceHelpers.waitForNullableObjects",
157         ReplaceWith("DeviceHelpers.waitForNullableObjects(selector, timeout)")
158     )
waitForNullableObjectsnull159     fun UiDevice.waitForNullableObjects(
160         selector: BySelector,
161         timeout: Duration = SHORT_WAIT,
162     ): List<UiObject2>? = DeviceHelpers.waitForNullableObjects(selector, timeout)
163 
164     /** Returns [true] when the [selector] is visible. */
165     fun hasObject(
166         selector: BySelector,
167     ): Boolean = trace("Checking if device has $selector") { uiDevice.hasObject(selector) }
168 
169     /** Finds an object with this selector and clicks on it. */
BySelectornull170     fun BySelector.click() {
171         trace("Clicking $this") { waitForObj(this).click() }
172     }
173 
174     /**
175      * Asserts visibility of a [selector], waiting for [timeout] until visibility matches the
176      * expected.
177      *
178      * If [container] is provided, the object is searched only inside of it.
179      */
180     @JvmOverloads
181     @JvmStatic
182     @Deprecated(
183         "Use DeviceHelpers.assertVisibility directly",
184         ReplaceWith("DeviceHelpers.assertVisibility(selector, visible, timeout, errorProvider)")
185     )
assertVisibilitynull186     fun UiDevice.assertVisibility(
187         selector: BySelector,
188         visible: Boolean = true,
189         timeout: Duration = LONG_WAIT,
190         errorProvider: (() -> String)? = null,
191     ) {
192         DeviceHelpers.assertVisibility(selector, visible, timeout, errorProvider)
193     }
194 
195     /**
196      * Asserts visibility of a [selector], waiting for [timeout] until visibility matches the
197      * expected.
198      *
199      * If [container] is provided, the object is searched only inside of it.
200      */
201     @JvmOverloads
202     @JvmStatic
assertVisibilitynull203     fun assertVisibility(
204         selector: BySelector,
205         visible: Boolean = true,
206         timeout: Duration = LONG_WAIT,
207         errorProvider: (() -> String)? = null,
208     ) {
209         ensureThat("$selector is ${visible.asVisibilityBoolean()}", timeout, errorProvider) {
210             uiDevice.hasObject(selector) == visible
211         }
212     }
213 
Booleannull214     private fun Boolean.asVisibilityBoolean(): String =
215         when (this) {
216             true -> "visible"
217             false -> "invisible"
218         }
219 
220     /**
221      * Asserts visibility of a [selector] inside this [UiObject2], waiting for [timeout] until
222      * visibility matches the expected.
223      */
assertVisibilitynull224     fun UiObject2.assertVisibility(
225         selector: BySelector,
226         visible: Boolean,
227         timeout: Duration = LONG_WAIT,
228         errorProvider: (() -> String)? = null,
229     ) {
230         ensureThat(
231             "$selector is ${visible.asVisibilityBoolean()} inside $this",
232             timeout,
233             errorProvider
234         ) {
235             hasObject(selector) == visible
236         }
237     }
238 
239     /** Asserts that a this selector is visible. Throws otherwise. */
BySelectornull240     fun BySelector.assertVisible(
241         timeout: Duration = LONG_WAIT,
242         errorProvider: (() -> String)? = null
243     ) {
244         uiDevice.assertVisibility(
245             selector = this,
246             visible = true,
247             timeout = timeout,
248             errorProvider = errorProvider
249         )
250     }
251 
252     /** Asserts that a this selector is invisible. Throws otherwise. */
253     @JvmStatic
254     @JvmOverloads
BySelectornull255     fun BySelector.assertInvisible(
256         timeout: Duration = LONG_WAIT,
257         errorProvider: (() -> String)? = null
258     ) {
259         uiDevice.assertVisibility(
260             selector = this,
261             visible = false,
262             timeout = timeout,
263             errorProvider = errorProvider
264         )
265     }
266 
267     /**
268      * Executes a shell command on the device.
269      *
270      * Adds some logging. Throws [RuntimeException] In case of failures.
271      */
272     @Deprecated("Use [DeviceHelpers.shell] directly", ReplaceWith("DeviceHelpers.shell(command)"))
273     @JvmStatic
shellnull274     fun UiDevice.shell(command: String): String = DeviceHelpers.shell(command)
275 
276     /**
277      * Executes a shell command on the device, and return its output one it finishes.
278      *
279      * Adds some logging to [UiDevice.executeShellCommand]. Throws [RuntimeException] In case of
280      * failures. Blocks until the command returns.
281      *
282      * @param command Shell command to execute
283      * @return Standard output of the command.
284      */
285     @JvmStatic
286     fun shell(command: String): String {
287         trace("Executing shell command: $command") {
288             Log.d(TAG, "Executing Shell Command: $command at ${uptimeMillis()}ms")
289             return try {
290                 uiDevice.executeShellCommand(command)
291             } catch (e: IOException) {
292                 Log.e(TAG, "IOException Occurred.", e)
293                 throw RuntimeException(e)
294             }
295         }
296     }
297 
298     /** Perform double tap at specified x and y position */
299     @JvmStatic
UiDevicenull300     fun UiDevice.doubleTapAt(x: Int, y: Int) {
301         click(x, y)
302         Thread.sleep(DOUBLE_TAP_INTERVAL.toMillis())
303         click(x, y)
304     }
305 
306     /**
307      * Aims at replacing [UiDevice.swipe].
308      *
309      * This should be used instead of [UiDevice.swipe] as it causes less flakiness. See
310      * [BetterSwipe].
311      */
312     @JvmStatic
313     @Deprecated(
314         "Use DeviceHelpers.betterSwipe directly",
315         ReplaceWith("DeviceHelpers.betterSwipe(startX, startY, endX, endY, interpolator)")
316     )
UiDevicenull317     fun UiDevice.betterSwipe(
318         startX: Int,
319         startY: Int,
320         endX: Int,
321         endY: Int,
322         interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR
323     ) {
324         DeviceHelpers.betterSwipe(startX, startY, endX, endY, interpolator)
325     }
326 
327     /**
328      * Aims at replacing [UiDevice.swipe].
329      *
330      * This should be used instead of [UiDevice.swipe] as it causes less flakiness. See
331      * [BetterSwipe].
332      */
333     @JvmStatic
betterSwipenull334     fun betterSwipe(
335         startX: Int,
336         startY: Int,
337         endX: Int,
338         endY: Int,
339         interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR
340     ) {
341         trace("Swiping ($startX,$startY) -> ($endX,$endY)") {
342             BetterSwipe.from(PointF(startX.toFloat(), startY.toFloat()))
343                     .to(PointF(endX.toFloat(), endY.toFloat()), interpolator = interpolator)
344                     .release()
345         }
346     }
347 
348     /** [message] will be visible to the terminal when using `am instrument`. */
printInstrumentationStatusnull349     fun printInstrumentationStatus(tag: String, message: String) {
350         val result =
351             Bundle().apply {
352                 putString(Instrumentation.REPORT_KEY_STREAMRESULT, "[$tag]: $message")
353             }
354         instrumentationRegistry.sendStatus(/* resultCode= */ 0, result)
355     }
356 
357     /**
358      * Returns whether the screen is on.
359      *
360      * As this uses [waitForValueToSettle], it is resilient to fast screen on/off happening.
361      */
362     @JvmStatic
363     val UiDevice.isScreenOnSettled: Boolean
<lambda>null364         get() = waitForValueToSettle("Screen on") { isScreenOn }
365 }
366