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