1 /* 2 * Copyright (C) 2024 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.systemui_tapl.ui 18 19 import android.graphics.Point 20 import android.graphics.Rect 21 import android.os.SystemClock.sleep 22 import android.platform.helpers.Constants 23 import android.platform.helpers.LockscreenUtils 24 import android.platform.helpers.LockscreenUtils.LockscreenType 25 import android.platform.systemui_tapl.utils.DeviceUtils.sysuiResSelector 26 import android.platform.systemui_tapl.utils.SYSUI_PACKAGE 27 import android.platform.uiautomatorhelpers.BetterSwipe.from 28 import android.platform.uiautomatorhelpers.DeviceHelpers.assertInvisible 29 import android.platform.uiautomatorhelpers.DeviceHelpers.assertVisibility 30 import android.platform.uiautomatorhelpers.DeviceHelpers.assertVisible 31 import android.platform.uiautomatorhelpers.DeviceHelpers.click 32 import android.platform.uiautomatorhelpers.DeviceHelpers.doubleTapAt 33 import android.platform.uiautomatorhelpers.DeviceHelpers.shell 34 import android.platform.uiautomatorhelpers.DeviceHelpers.uiDevice 35 import android.platform.uiautomatorhelpers.DeviceHelpers.waitForFirstObj 36 import android.platform.uiautomatorhelpers.DeviceHelpers.waitForObj 37 import android.platform.uiautomatorhelpers.assertOnTheLeftSide 38 import android.platform.uiautomatorhelpers.assertOnTheRightSide 39 import android.platform.uiautomatorhelpers.stableBounds 40 import androidx.test.uiautomator.By 41 import androidx.test.uiautomator.UiObject2 42 import com.google.common.truth.Truth 43 import java.time.Duration 44 import java.util.regex.Pattern 45 46 /** System UI test automation object representing the lockscreen bouncer. */ 47 class Bouncer internal constructor(private val notification: Notification?) { 48 private val uiObject: UiObject2 = waitForFirstObj(*BOUNCER_SELECTORS, timeout = LONG_WAIT).first 49 enterCodeOnBouncernull50 private fun enterCodeOnBouncer(lockscreenType: LockscreenType, lockCode: String) { 51 LOCKSCREEN_TEXT_BOX_SELECTOR.assertVisible { "Lockscreen text box is not visible" } 52 LOCKSCREEN_TEXT_BOX_SELECTOR.click() 53 LockscreenUtils.enterCodeOnLockscreen(lockscreenType, lockCode) 54 } 55 56 /** 57 * Enters pattern based on a string which contains digits between 1-9 to represent a 3x3 grid. 58 * 59 * The constraint here is that the pattern must start with 5 (the center) as we use the center 60 * of the lock pattern view as a reference on where to swipe. 61 */ enterPatternnull62 fun enterPattern(pattern: String) { 63 Truth.assertWithMessage("#enterPattern argument does not start with 5") 64 .that(pattern.isNotEmpty() && pattern[0] == '5') 65 .isTrue() 66 val lockPatternView = waitForObj(PATTERN_SELECTOR) 67 val visibleCenter = lockPatternView.visibleCenter 68 val visibleBounds = lockPatternView.visibleBounds 69 val points = mutableListOf<Point>() 70 val centerPoint = Point(visibleCenter.x, visibleCenter.y) 71 for (c in pattern.substring(1).toCharArray()) { 72 points.add(centerPoint) 73 points.add( 74 when (c) { 75 '1' -> visibleBounds.left to visibleBounds.top 76 '2' -> visibleCenter.x to visibleBounds.top 77 '3' -> visibleBounds.right to visibleBounds.top 78 '4' -> visibleBounds.left to visibleCenter.y 79 '5' -> visibleCenter.x to visibleCenter.y 80 '6' -> visibleBounds.right to visibleCenter.y 81 '7' -> visibleBounds.left to visibleBounds.bottom 82 '8' -> visibleCenter.x to visibleBounds.bottom 83 '9' -> visibleBounds.right to visibleBounds.bottom 84 else -> error("Entering invalid digit: $c") 85 }.toPoint() 86 ) 87 } 88 val swipe = from(centerPoint) 89 points.forEach { swipe.to(it) } 90 swipe.release() 91 } 92 toPointnull93 private fun Pair<Int, Int>.toPoint() = Point(first, second) 94 95 /** 96 * Enter the Lockscreen code in the enter lockscreen text box. 97 * 98 * @param lockscreenType type of lockscreen set 99 * @param lockCode code to unlock the lockscreen 100 */ 101 fun unlockViaCode(lockscreenType: LockscreenType, lockCode: String) { 102 enterCodeOnBouncer(lockscreenType, lockCode) 103 LockscreenUtils.checkDeviceLock(false) 104 By.res(PAGE_TITLE_SELECTOR_PATTERN).assertInvisible() 105 notification?.verifyStartedApp() 106 } 107 108 /** 109 * Enter invalid Lockscreen code in the enter lockscreen text box and fail to unlock. 110 * 111 * @param lockscreenType type of lockscreen set 112 * @param invalidCode invalid code to unlock the lockscreen 113 */ failedUnlockViaCodenull114 private fun failedUnlockViaCode(lockscreenType: LockscreenType, invalidCode: String) { 115 enterCodeOnBouncer(lockscreenType, invalidCode) 116 117 // Making sure device is still locked. The action happens really fast. Making sure 118 // previous action got completed 119 sleep((Constants.SHORT_WAIT_TIME_IN_SECONDS * 1000).toLong()) 120 LockscreenUtils.checkDeviceLock(true) 121 } 122 123 /** 124 * Enter invalid Lockscreen pin in the enter lockscreen text box and fail to unlock. 125 * 126 * @param invalidPin invalid pin to unlock the lockscreen 127 */ failedUnlockViaPinnull128 fun failedUnlockViaPin(invalidPin: String) { 129 failedUnlockViaCode(LockscreenType.PIN, invalidPin) 130 } 131 132 /** 133 * Enter invalid Lockscreen password in the enter lockscreen text box and fail to unlock. 134 * 135 * @param invalidPassword invalid password to unlock the lockscreen 136 */ failedUnlockViaPasswordnull137 fun failedUnlockViaPassword(invalidPassword: String) { 138 failedUnlockViaCode(LockscreenType.PASSWORD, invalidPassword) 139 } 140 141 /** Check bouncer input UI is on the left side of the screen */ assertOnTheLeftSidenull142 fun assertOnTheLeftSide(lockscreenType: LockscreenType) { 143 getInputUI(lockscreenType).assertOnTheLeftSide() 144 } 145 146 /** Check bouncer is on the right side of the screen */ assertOnTheRightSidenull147 fun assertOnTheRightSide(lockscreenType: LockscreenType) { 148 getInputUI(lockscreenType).assertOnTheRightSide() 149 } 150 getInputUInull151 private fun getInputUI(lockscreenType: LockscreenType): UiObject2 { 152 return when (lockscreenType) { 153 LockscreenType.PIN -> waitForObj(KEYPAD_SELECTOR) 154 LockscreenType.PATTERN -> waitForObj(PATTERN_SELECTOR) 155 LockscreenType.PASSWORD, 156 LockscreenType.SWIPE, 157 LockscreenType.NONE -> throw NotImplementedError("Not supported for these auth types") 158 } 159 } 160 161 /** Double-taps on the left side of the screen. */ doubleTapOnTheLeftSidenull162 fun doubleTapOnTheLeftSide() { 163 doubleTapAtXPosition(uiDevice.displayWidth / 4) 164 } 165 166 /** Double-taps on the right side of the screen. */ doubleTapOnTheRightSidenull167 fun doubleTapOnTheRightSide() { 168 doubleTapAtXPosition(uiDevice.displayWidth * 3 / 4) 169 } 170 doubleTapAtXPositionnull171 private fun doubleTapAtXPosition(touchX: Int) { 172 val touchY = uiDevice.displayHeight / 2 173 uiDevice.doubleTapAt(touchX, touchY) 174 } 175 176 /** https://hsv.googleplex.com/5840630509993984?node=26 */ 177 val pinContainerRect: Rect? 178 get() { 179 return waitForFirstObj(*PIN_CONTAINER_SELECTOR).first.visibleBounds 180 } 181 182 /** https://hsv.googleplex.com/5550967647895552?node=25 */ 183 val pinBouncerContainerRect: Rect 184 get() { 185 return waitForObj(sysuiResSelector("keyguard_pin_view")).stableBounds 186 } 187 188 /** https://hsv.googleplex.com/6358737448075264?node=25 */ 189 val patternBouncerContainerRect: Rect 190 get() { 191 return waitForObj(sysuiResSelector("keyguard_pattern_view")).stableBounds 192 } 193 194 /** https://hsv.googleplex.com/4951362564521984?node=25 */ 195 val passwordBouncerContainerRect: Rect 196 get() { 197 return waitForObj(sysuiResSelector("keyguard_password_view")).stableBounds 198 } 199 200 /** Checks whether the delete button exists or not. */ assertDeleteButtonVisibilitynull201 fun assertDeleteButtonVisibility(visible: Boolean) { 202 assertVisibility(PIN_BOUNCER_DELETE_BUTTON, visible) 203 } 204 205 /** Checks whether the enter button exists or not. */ assertEnterButtonVisibilitynull206 fun assertEnterButtonVisibility(visible: Boolean) { 207 assertVisibility(PIN_BOUNCER_ENTER_BUTTON, visible) 208 } 209 210 /** Inputs key on the bouncer. */ inputKeynull211 fun inputKey(key: String) { 212 shell("input keyboard text $key") 213 } 214 215 companion object { 216 // Default wait used by waitForObj. waitForFirstObj uses a shorter wait. 217 private val LONG_WAIT = Duration.ofSeconds(10) 218 219 private val IS_COMPOSE_BOUNCER_ENABLED = 220 com.android.systemui.Flags.composeBouncer() || 221 com.android.systemui.Flags.sceneContainer() 222 /** 223 * Possible selectors for container holding security view like pin, bouncer etc HSV: 224 * https://hsv.googleplex.com/5452172222267392?node=22 225 * 226 * It can be one of these three selectors depending on the flags that are active. 227 */ 228 private val BOUNCER_SELECTORS = 229 arrayOf( 230 sysuiResSelector("bouncer_root"), 231 By.res("element:BouncerContent"), 232 sysuiResSelector("view_flipper"), 233 ) 234 235 private val LOCKSCREEN_TEXT_BOX_SELECTOR = 236 if (IS_COMPOSE_BOUNCER_ENABLED) { 237 sysuiResSelector("bouncer_text_entry") 238 } else { 239 By.res(Pattern.compile(SYSUI_PACKAGE + ":id/(pinEntry|passwordEntry)")) 240 .focused(true) 241 } 242 243 /** The compose bouncer_text_entry isn't the same as pin_container, but close enough */ 244 private val PIN_CONTAINER_SELECTOR = 245 arrayOf( 246 sysuiResSelector("bouncer_text_entry"), 247 sysuiResSelector("pin_container"), 248 ) 249 /** https://hsv.googleplex.com/5225465733185536?node=54 */ 250 private val PIN_BOUNCER_DELETE_BUTTON = sysuiResSelector("delete_button") 251 /** https://hsv.googleplex.com/5554629610831872?node=52 */ 252 private val PIN_BOUNCER_ENTER_BUTTON = sysuiResSelector("key_enter") 253 254 // https://hsv.googleplex.com/5130837462876160?node=117 255 private val PAGE_TITLE_SELECTOR_PATTERN = 256 Pattern.compile(String.format("%s:id/%s", SYSUI_PACKAGE, "keyguard_clock_container")) 257 258 public val PATTERN_SELECTOR = 259 if (IS_COMPOSE_BOUNCER_ENABLED) { 260 sysuiResSelector("bouncer_pattern_root") 261 } else { 262 sysuiResSelector("lockPatternView") 263 } 264 265 public val PASSWORD_SELECTOR = 266 if (IS_COMPOSE_BOUNCER_ENABLED) { 267 sysuiResSelector("bouncer_text_entry") 268 } else { 269 sysuiResSelector("passwordEntry") 270 } 271 272 public val KEYPAD_SELECTOR = 273 if (IS_COMPOSE_BOUNCER_ENABLED) { 274 sysuiResSelector("pin_pad_grid") 275 } else { 276 sysuiResSelector("flow1") 277 } 278 279 const val VALID_PIN = "1234" 280 281 const val VALID_PASSWORD = "abcd" 282 283 const val DEFAULT_PATTERN = "5624" 284 285 public val USER_ICON_SELECTOR = sysuiResSelector("user_icon") 286 } 287 } 288