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