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