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 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 BetterSwipe.swipe(centerPoint) { points.forEach { to(it) } } 89 } 90 toPointnull91 private fun Pair<Int, Int>.toPoint() = Point(first, second) 92 93 /** 94 * Enter the Lockscreen code in the enter lockscreen text box. 95 * 96 * @param lockscreenType type of lockscreen set 97 * @param lockCode code to unlock the lockscreen 98 */ 99 fun unlockViaCode(lockscreenType: LockscreenType, lockCode: String) { 100 enterCodeOnBouncer(lockscreenType, lockCode) 101 LockscreenUtils.checkDeviceLock(false) 102 By.res(PAGE_TITLE_SELECTOR_PATTERN).assertInvisible() 103 notification?.verifyStartedApp() 104 } 105 106 /** 107 * Enter invalid Lockscreen code in the enter lockscreen text box and fail to unlock. 108 * 109 * @param lockscreenType type of lockscreen set 110 * @param invalidCode invalid code to unlock the lockscreen 111 */ failedUnlockViaCodenull112 private fun failedUnlockViaCode(lockscreenType: LockscreenType, invalidCode: String) { 113 enterCodeOnBouncer(lockscreenType, invalidCode) 114 115 // Making sure device is still locked. The action happens really fast. Making sure 116 // previous action got completed 117 sleep((Constants.SHORT_WAIT_TIME_IN_SECONDS * 1000).toLong()) 118 LockscreenUtils.checkDeviceLock(true) 119 } 120 121 /** 122 * Enter invalid Lockscreen pin in the enter lockscreen text box and fail to unlock. 123 * 124 * @param invalidPin invalid pin to unlock the lockscreen 125 */ failedUnlockViaPinnull126 fun failedUnlockViaPin(invalidPin: String) { 127 failedUnlockViaCode(LockscreenType.PIN, invalidPin) 128 } 129 130 /** 131 * Enter invalid Lockscreen password in the enter lockscreen text box and fail to unlock. 132 * 133 * @param invalidPassword invalid password to unlock the lockscreen 134 */ failedUnlockViaPasswordnull135 fun failedUnlockViaPassword(invalidPassword: String) { 136 failedUnlockViaCode(LockscreenType.PASSWORD, invalidPassword) 137 } 138 139 /** Check bouncer input UI is on the left side of the screen */ assertOnTheLeftSidenull140 fun assertOnTheLeftSide(lockscreenType: LockscreenType) { 141 getInputUI(lockscreenType).assertOnTheLeftSide() 142 } 143 144 /** Check bouncer is on the right side of the screen */ assertOnTheRightSidenull145 fun assertOnTheRightSide(lockscreenType: LockscreenType) { 146 getInputUI(lockscreenType).assertOnTheRightSide() 147 } 148 getInputUInull149 private fun getInputUI(lockscreenType: LockscreenType): UiObject2 { 150 return when (lockscreenType) { 151 LockscreenType.PIN -> waitForObj(KEYPAD_SELECTOR) 152 LockscreenType.PATTERN -> waitForObj(PATTERN_SELECTOR) 153 LockscreenType.PASSWORD, 154 LockscreenType.SWIPE, 155 LockscreenType.NONE -> throw NotImplementedError("Not supported for these auth types") 156 } 157 } 158 159 /** Double-taps on the left side of the screen. */ doubleTapOnTheLeftSidenull160 fun doubleTapOnTheLeftSide() { 161 doubleTapAtXPosition(uiDevice.displayWidth / 4) 162 } 163 164 /** Double-taps on the right side of the screen. */ doubleTapOnTheRightSidenull165 fun doubleTapOnTheRightSide() { 166 doubleTapAtXPosition(uiDevice.displayWidth * 3 / 4) 167 } 168 doubleTapAtXPositionnull169 private fun doubleTapAtXPosition(touchX: Int) { 170 val touchY = uiDevice.displayHeight / 2 171 uiDevice.doubleTapAt(touchX, touchY) 172 } 173 174 /** https://hsv.googleplex.com/5840630509993984?node=26 */ 175 val pinContainerRect: Rect? 176 get() { 177 return waitForFirstObj(*PIN_CONTAINER_SELECTOR).first.visibleBounds 178 } 179 180 /** https://hsv.googleplex.com/5550967647895552?node=25 */ 181 val pinBouncerContainerRect: Rect 182 get() { 183 return waitForObj(sysuiResSelector("keyguard_pin_view")).stableBounds 184 } 185 186 /** https://hsv.googleplex.com/6358737448075264?node=25 */ 187 val patternBouncerContainerRect: Rect 188 get() { 189 return waitForObj(sysuiResSelector("keyguard_pattern_view")).stableBounds 190 } 191 192 /** https://hsv.googleplex.com/4951362564521984?node=25 */ 193 val passwordBouncerContainerRect: Rect 194 get() { 195 return waitForObj(sysuiResSelector("keyguard_password_view")).stableBounds 196 } 197 198 /** Checks whether the delete button exists or not. */ assertDeleteButtonVisibilitynull199 fun assertDeleteButtonVisibility(visible: Boolean) { 200 assertVisibility(PIN_BOUNCER_DELETE_BUTTON, visible) 201 } 202 203 /** Checks whether the enter button exists or not. */ assertEnterButtonVisibilitynull204 fun assertEnterButtonVisibility(visible: Boolean) { 205 assertVisibility(PIN_BOUNCER_ENTER_BUTTON, visible) 206 } 207 208 /** Inputs key on the bouncer. */ inputKeynull209 fun inputKey(key: String) { 210 shell("input keyboard text $key") 211 } 212 213 companion object { 214 // Default wait used by waitForObj. waitForFirstObj uses a shorter wait. 215 private val LONG_WAIT = Duration.ofSeconds(10) 216 217 private val IS_COMPOSE_BOUNCER_ENABLED = 218 com.android.systemui.Flags.composeBouncer() || 219 com.android.systemui.Flags.sceneContainer() 220 /** 221 * Possible selectors for container holding security view like pin, bouncer etc HSV: 222 * https://hsv.googleplex.com/5452172222267392?node=22 223 * 224 * It can be one of these three selectors depending on the flags that are active. 225 */ 226 private val BOUNCER_SELECTORS = 227 arrayOf( 228 sysuiResSelector("bouncer_root"), 229 By.res("element:BouncerContent"), 230 sysuiResSelector("view_flipper"), 231 ) 232 233 private val LOCKSCREEN_TEXT_BOX_SELECTOR = 234 if (IS_COMPOSE_BOUNCER_ENABLED) { 235 sysuiResSelector("bouncer_text_entry") 236 } else { 237 By.res(Pattern.compile(SYSUI_PACKAGE + ":id/(pinEntry|passwordEntry)")) 238 .focused(true) 239 } 240 241 /** The compose bouncer_text_entry isn't the same as pin_container, but close enough */ 242 private val PIN_CONTAINER_SELECTOR = 243 arrayOf(sysuiResSelector("bouncer_text_entry"), sysuiResSelector("pin_container")) 244 /** https://hsv.googleplex.com/5225465733185536?node=54 */ 245 private val PIN_BOUNCER_DELETE_BUTTON = sysuiResSelector("delete_button") 246 /** https://hsv.googleplex.com/5554629610831872?node=52 */ 247 private val PIN_BOUNCER_ENTER_BUTTON = sysuiResSelector("key_enter") 248 249 // https://hsv.googleplex.com/5130837462876160?node=117 250 private val PAGE_TITLE_SELECTOR_PATTERN = 251 Pattern.compile(String.format("%s:id/%s", SYSUI_PACKAGE, "keyguard_indication_area")) 252 253 public val PATTERN_SELECTOR = 254 if (IS_COMPOSE_BOUNCER_ENABLED) { 255 sysuiResSelector("bouncer_pattern_root") 256 } else { 257 sysuiResSelector("lockPatternView") 258 } 259 260 public val PASSWORD_SELECTOR = 261 if (IS_COMPOSE_BOUNCER_ENABLED) { 262 sysuiResSelector("bouncer_text_entry") 263 } else { 264 sysuiResSelector("passwordEntry") 265 } 266 267 public val KEYPAD_SELECTOR = 268 if (IS_COMPOSE_BOUNCER_ENABLED) { 269 sysuiResSelector("pin_pad_grid") 270 } else { 271 sysuiResSelector("flow1") 272 } 273 274 const val VALID_PIN = "1234" 275 276 const val VALID_PASSWORD = "abcd" 277 278 const val DEFAULT_PATTERN = "5624" 279 280 public val USER_ICON_SELECTOR = sysuiResSelector("user_icon") 281 } 282 } 283