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.waitForValueToSettle 29 import android.util.Log 30 import androidx.test.platform.app.InstrumentationRegistry 31 import androidx.test.uiautomator.BySelector 32 import androidx.test.uiautomator.UiDevice 33 import androidx.test.uiautomator.UiObject2 34 import java.io.IOException 35 import java.time.Duration 36 37 private const val TAG = "DeviceHelpers" 38 39 object DeviceHelpers { 40 private val SHORT_WAIT = Duration.ofMillis(1500) 41 private val LONG_WAIT = Duration.ofSeconds(10) 42 private val DOUBLE_TAP_INTERVAL = Duration.ofMillis(100) 43 44 private val instrumentationRegistry = InstrumentationRegistry.getInstrumentation() 45 46 @JvmStatic 47 val uiDevice: UiDevice 48 get() = UiDevice.getInstance(instrumentationRegistry) 49 50 @JvmStatic 51 val context: Context 52 get() = instrumentationRegistry.targetContext 53 54 /** 55 * Waits for an object to be visible and returns it. 56 * 57 * Throws an error with message provided by [errorProvider] if the object is not found. 58 * 59 * @deprecated Use [DeviceHelpers.waitForObj] instead. 60 */ waitForObjnull61 fun UiDevice.waitForObj( 62 selector: BySelector, 63 timeout: Duration = LONG_WAIT, 64 errorProvider: () -> String = { "Object $selector not found" }, <lambda>null65 ): UiObject2 = waitFor("$selector object", timeout, errorProvider) { findObject(selector) } 66 67 /** 68 * Waits for an object to be visible and returns it. 69 * 70 * Throws an error with message provided by [errorProvider] if the object is not found. 71 */ waitForObjnull72 fun waitForObj( 73 selector: BySelector, 74 timeout: Duration = LONG_WAIT, 75 errorProvider: () -> String = { "Object $selector not found" }, 76 ): UiObject2 = uiDevice.waitForObj(selector, timeout, errorProvider) 77 78 /** 79 * Waits for an object to be visible and returns it. 80 * 81 * Throws an error with message provided by [errorProvider] if the object is not found. 82 */ waitForObjnull83 fun UiObject2.waitForObj( 84 selector: BySelector, 85 timeout: Duration = LONG_WAIT, 86 errorProvider: () -> String = { "Object $selector not found" }, <lambda>null87 ): UiObject2 = waitFor("$selector object", timeout, errorProvider) { findObject(selector) } 88 89 /** 90 * Waits for an object to be visible and returns it. Returns `null` if the object is not found. 91 * 92 * @deprecated use [DeviceHelpers.waitForNullableObj] instead. 93 */ waitForNullableObjnull94 fun UiDevice.waitForNullableObj( 95 selector: BySelector, 96 timeout: Duration = SHORT_WAIT, 97 ): UiObject2? = waitForNullable("nullable $selector objects", timeout) { findObject(selector) } 98 99 /** 100 * Waits for an object to be visible and returns it. Returns `null` if the object is not found. 101 */ waitForNullableObjnull102 fun waitForNullableObj( 103 selector: BySelector, 104 timeout: Duration = SHORT_WAIT, 105 ): UiObject2? = uiDevice.waitForNullableObj(selector, timeout) 106 107 /** 108 * Waits for objects matched by [selector] to be visible and returns them. Returns `null` if no 109 * objects are found 110 */ 111 fun UiDevice.waitForNullableObjects( 112 selector: BySelector, 113 timeout: Duration = SHORT_WAIT, 114 ): List<UiObject2>? = waitForNullable("$selector objects", timeout) { findObjects(selector) } 115 116 /** 117 * Asserts visibility of a [selector], waiting for [timeout] until visibility matches the 118 * expected. 119 * 120 * If [container] is provided, the object is searched only inside of it. 121 */ 122 @JvmOverloads 123 @JvmStatic assertVisibilitynull124 fun UiDevice.assertVisibility( 125 selector: BySelector, 126 visible: Boolean = true, 127 timeout: Duration = LONG_WAIT, 128 errorProvider: (() -> String)? = null, 129 ) { 130 ensureThat("$selector is ${visible.asVisibilityBoolean()}", timeout, errorProvider) { 131 hasObject(selector) == visible 132 } 133 } 134 Booleannull135 private fun Boolean.asVisibilityBoolean(): String = 136 when (this) { 137 true -> "visible" 138 false -> "invisible" 139 } 140 141 /** 142 * Asserts visibility of a [selector] inside this [UiObject2], waiting for [timeout] until 143 * visibility matches the expected. 144 */ assertVisibilitynull145 fun UiObject2.assertVisibility( 146 selector: BySelector, 147 visible: Boolean, 148 timeout: Duration = LONG_WAIT, 149 errorProvider: (() -> String)? = null, 150 ) { 151 ensureThat( 152 "$selector is ${visible.asVisibilityBoolean()} inside $this", 153 timeout, 154 errorProvider 155 ) { 156 hasObject(selector) == visible 157 } 158 } 159 160 /** Asserts that a this selector is visible. Throws otherwise. */ BySelectornull161 fun BySelector.assertVisible( 162 timeout: Duration = LONG_WAIT, 163 errorProvider: (() -> String)? = null 164 ) { 165 uiDevice.assertVisibility( 166 selector = this, 167 visible = true, 168 timeout = timeout, 169 errorProvider = errorProvider 170 ) 171 } 172 /** Asserts that a this selector is invisible. Throws otherwise. */ BySelectornull173 fun BySelector.assertInvisible( 174 timeout: Duration = LONG_WAIT, 175 errorProvider: (() -> String)? = null 176 ) { 177 uiDevice.assertVisibility( 178 selector = this, 179 visible = false, 180 timeout = timeout, 181 errorProvider = errorProvider 182 ) 183 } 184 185 /** 186 * Executes a shell command on the device. 187 * 188 * Adds some logging. Throws [RuntimeException] In case of failures. 189 */ 190 @JvmStatic shellnull191 fun UiDevice.shell(command: String): String = 192 trace("Executing shell command: $command") { 193 Log.d(TAG, "Executing Shell Command: $command") 194 return try { 195 executeShellCommand(command) 196 } catch (e: IOException) { 197 Log.e(TAG, "IOException Occurred.", e) 198 throw RuntimeException(e) 199 } 200 } 201 202 /** Perform double tap at specified x and y position */ 203 @JvmStatic UiDevicenull204 fun UiDevice.doubleTapAt(x: Int, y: Int) { 205 click(x, y) 206 Thread.sleep(DOUBLE_TAP_INTERVAL.toMillis()) 207 click(x, y) 208 } 209 210 /** 211 * Aims at replacing [UiDevice.swipe]. 212 * 213 * This should be used instead of [UiDevice.swipe] as it causes less flakiness. See 214 * [BetterSwipe]. 215 */ 216 @JvmStatic UiDevicenull217 fun UiDevice.betterSwipe( 218 startX: Int, 219 startY: Int, 220 endX: Int, 221 endY: Int, 222 interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR 223 ) { 224 trace("Swiping ($startX,$startY) -> ($endX,$endY)") { 225 BetterSwipe.from(PointF(startX.toFloat(), startY.toFloat())) 226 .to(PointF(endX.toFloat(), endY.toFloat()), interpolator = interpolator) 227 .release() 228 } 229 } 230 231 /** [message] will be visible to the terminal when using `am instrument`. */ printInstrumentationStatusnull232 fun printInstrumentationStatus(tag: String, message: String) { 233 val result = 234 Bundle().apply { 235 putString(Instrumentation.REPORT_KEY_STREAMRESULT, "[$tag]: $message") 236 } 237 instrumentationRegistry.sendStatus(/* resultCode= */ 0, result) 238 } 239 240 /** 241 * Returns whether the screen is on. 242 * 243 * As this uses [waitForValueToSettle], it is resilient to fast screen on/off happening. 244 */ 245 @JvmStatic 246 val UiDevice.isScreenOnSettled: Boolean <lambda>null247 get() = waitForValueToSettle("Screen on") { isScreenOn } 248 } 249