/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.keyguard import android.app.admin.DevicePolicyManager import android.content.res.Configuration import android.media.AudioManager import android.platform.test.annotations.EnableFlags import android.security.Flags.FLAG_SECURE_LOCK_DEVICE import android.telephony.TelephonyManager import android.testing.TestableLooper.RunWithLooper import android.testing.TestableResources import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewTreeObserver import android.view.ViewTreeObserver.OnPreDrawListener import android.view.WindowInsetsController import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.internal.widget.LockPatternUtils import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback import com.android.keyguard.KeyguardSecurityModel.SecurityMode import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants import com.android.systemui.classifier.FalsingA11yDelegate import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardDismissTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.log.SessionTracker import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.FakeSceneDataSource import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.securelockdevice.data.repository.fakeSecureLockDeviceRepository import com.android.systemui.securelockdevice.domain.interactor.secureLockDeviceInteractor import com.android.systemui.shade.domain.interactor.enableSingleShade import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.kotlin.javaAdapter import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argThat import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.wrapper.LockPatternCheckerWrapper import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import junit.framework.Assert import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var view: KeyguardSecurityContainer @Mock private lateinit var adminSecondaryLockScreenControllerFactory: AdminSecondaryLockScreenController.Factory @Mock private lateinit var adminSecondaryLockScreenController: AdminSecondaryLockScreenController @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var inputViewController: KeyguardInputViewController @Mock private lateinit var windowInsetsController: WindowInsetsController @Mock private lateinit var securityViewFlipper: KeyguardSecurityViewFlipper @Mock private lateinit var viewFlipperController: KeyguardSecurityViewFlipperController @Mock private lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory @Mock private lateinit var keyguardMessageAreaController: KeyguardMessageAreaController<*> @Mock private lateinit var keyguardMessageArea: BouncerKeyguardMessageArea @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var emergencyButtonController: EmergencyButtonController @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var globalSettings: GlobalSettings @Mock private lateinit var userSwitcherController: UserSwitcherController @Mock private lateinit var sessionTracker: SessionTracker @Mock private lateinit var keyguardViewController: KeyguardViewController @Mock private lateinit var keyguardPasswordViewControllerMock: KeyguardPasswordViewController @Mock private lateinit var falsingA11yDelegate: FalsingA11yDelegate @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback @Mock private lateinit var audioManager: AudioManager @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var postureController: DevicePostureController @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier @Mock private lateinit var bouncerInteractor: BouncerInteractor @Mock private lateinit var lockPatternChecker: LockPatternCheckerWrapper @Captor private lateinit var keyguardUpdateMonitorCallbackCaptor: ArgumentCaptor @Captor private lateinit var swipeListenerArgumentCaptor: ArgumentCaptor @Captor private lateinit var onViewInflatedCallbackArgumentCaptor: ArgumentCaptor private lateinit var featureFlags: FakeFeatureFlags private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController private lateinit var keyguardPasswordView: KeyguardPasswordView private lateinit var testableResources: TestableResources private lateinit var kosmos: Kosmos private lateinit var sceneInteractor: SceneInteractor private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor private lateinit var deviceEntryInteractor: DeviceEntryInteractor @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor private lateinit var sceneTransitionStateFlow: MutableStateFlow private lateinit var fakeSceneDataSource: FakeSceneDataSource private val executor = FakeExecutor(FakeSystemClock()) private lateinit var underTest: KeyguardSecurityContainerController @Before fun setUp() { MockitoAnnotations.initMocks(this) testableResources = mContext.getOrCreateTestableResources() testableResources.resources.configuration.orientation = Configuration.ORIENTATION_UNDEFINED whenever(view.context).thenReturn(mContext) whenever(view.resources).thenReturn(testableResources.resources) val lp = FrameLayout.LayoutParams(/* width= */ 0, /* height= */ 0) lp.gravity = 0 whenever(view.layoutParams).thenReturn(lp) whenever(adminSecondaryLockScreenControllerFactory.create(any())) .thenReturn(adminSecondaryLockScreenController) whenever(securityViewFlipper.windowInsetsController).thenReturn(windowInsetsController) keyguardPasswordView = spy( LayoutInflater.from(mContext).inflate(R.layout.keyguard_password_view, null) as KeyguardPasswordView ) whenever(keyguardPasswordView.rootView).thenReturn(securityViewFlipper) whenever(keyguardPasswordView.requireViewById(R.id.bouncer_message_area)) .thenReturn(keyguardMessageArea) whenever(messageAreaControllerFactory.create(any())) .thenReturn(keyguardMessageAreaController) whenever(keyguardPasswordView.windowInsetsController).thenReturn(windowInsetsController) whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(SecurityMode.PIN) whenever(keyguardStateController.canDismissLockScreen()).thenReturn(true) whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(true) val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository()) featureFlags = FakeFeatureFlags() featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) mSetFlagsRule.enableFlags(AConfigFlags.FLAG_REVAMPED_BOUNCER_MESSAGES) if (!SceneContainerFlag.isEnabled) { mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_WM_STATE_REFACTOR) } keyguardPasswordViewController = KeyguardPasswordViewController( keyguardPasswordView, keyguardUpdateMonitor, SecurityMode.Password, lockPatternUtils, null, messageAreaControllerFactory, null, null, emergencyButtonController, null, mock(), null, keyguardViewController, postureController, featureFlags, mSelectedUserInteractor, keyguardKeyboardInteractor, null, mUserActivityNotifier, lockPatternChecker, ) kosmos = testKosmos() sceneInteractor = kosmos.sceneInteractor keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor sceneTransitionStateFlow = MutableStateFlow(ObservableTransitionState.Idle(Scenes.Lockscreen)) sceneInteractor.setTransitionState(sceneTransitionStateFlow) deviceEntryInteractor = kosmos.deviceEntryInteractor fakeSceneDataSource = kosmos.fakeSceneDataSource underTest = KeyguardSecurityContainerController( view, adminSecondaryLockScreenControllerFactory, lockPatternUtils, keyguardUpdateMonitor, keyguardSecurityModel, metricsLogger, uiEventLogger, keyguardStateController, viewFlipperController, configurationController, falsingCollector, falsingManager, userSwitcherController, featureFlags, globalSettings, sessionTracker, falsingA11yDelegate, telephonyManager, viewMediatorCallback, audioManager, faceAuthInteractor, mock(), { kosmos.javaAdapter }, mSelectedUserInteractor, deviceProvisionedController, faceAuthAccessibilityDelegate, devicePolicyManager, kosmos.keyguardDismissTransitionInteractor, { primaryBouncerInteractor }, executor, { deviceEntryInteractor }, { kosmos.windowRootViewBlurInteractor }, { bouncerInteractor }, { kosmos.secureLockDeviceInteractor }, ) } @Test @DisableSceneContainer fun onInitConfiguresViewMode() { underTest.onInit() verify(view) .initMode( eq(KeyguardSecurityContainer.MODE_DEFAULT), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) } @Test @DisableSceneContainer fun setAccessibilityDelegate() { verify(view).accessibilityDelegate = eq(faceAuthAccessibilityDelegate) } @Test @DisableSceneContainer fun showSecurityScreen_canInflateAllModes() { val modes = SecurityMode.values() for (mode in modes) { whenever(inputViewController.securityMode).thenReturn(mode) underTest.showSecurityScreen(mode) if (mode == SecurityMode.Invalid) { verify(viewFlipperController, never()).getSecurityView(any(), any(), any()) } else { verify(viewFlipperController).getSecurityView(eq(mode), any(), any()) } } } @Test fun showSecurityScreen_previousBiometricSecondFactorPin_clearsHardwareAuthTokens() { underTest.showSecurityScreen(SecurityMode.BiometricSecondFactorPin) underTest.showSecurityScreen(SecurityMode.PIN) verify(keyguardUpdateMonitor).clearFingerprintRecognized() } @Test @DisableSceneContainer fun onResourcesUpdate_callsThroughOnRotationChange() { clearInvocations(view) // Rotation is the same, shouldn't cause an update underTest.updateResources() verify(view, never()) .initMode( eq(KeyguardSecurityContainer.MODE_DEFAULT), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) // Update rotation. Should trigger update testableResources.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE underTest.updateResources() verify(view) .initMode( eq(KeyguardSecurityContainer.MODE_DEFAULT), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) } private fun touchDown() { underTest.mGlobalTouchListener.onTouchEvent( MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, MotionEvent.ACTION_DOWN, /* x= */ 0f, /* y= */ 0f, /* metaState= */ 0, ) ) } @Test @DisableSceneContainer fun onInterceptTap_inhibitsFalsingInSidedSecurityMode() { whenever(view.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(false) touchDown() verify(falsingCollector, never()).avoidGesture() whenever(view.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(true) touchDown() verify(falsingCollector).avoidGesture() } @Test @DisableSceneContainer fun showSecurityScreen_oneHandedMode_flagDisabled_noOneHandedMode() { testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false) setupGetSecurityView(SecurityMode.Pattern) underTest.showSecurityScreen(SecurityMode.Pattern) verify(view) .initMode( eq(KeyguardSecurityContainer.MODE_DEFAULT), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) } @Test @DisableSceneContainer fun showSecurityScreen_oneHandedMode_flagEnabled_oneHandedMode() { testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) setupGetSecurityView(SecurityMode.Pattern) verify(view) .initMode( eq(KeyguardSecurityContainer.MODE_ONE_HANDED), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) } @Test @DisableSceneContainer fun showSecurityScreen_oneHandedMode_flagEnabled_oneHandedMode_simpin() { testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) setupGetSecurityView(SecurityMode.SimPin) verify(view) .initMode( eq(KeyguardSecurityContainer.MODE_ONE_HANDED), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) } @Test @DisableSceneContainer fun showSecurityScreen_oneHandedMode_flagEnabled_oneHandedMode_simpuk() { testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) setupGetSecurityView(SecurityMode.SimPuk) verify(view) .initMode( eq(KeyguardSecurityContainer.MODE_ONE_HANDED), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) } @Test @DisableSceneContainer fun showSecurityScreen_twoHandedMode_flagEnabled_noOneHandedMode() { testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) setupGetSecurityView(SecurityMode.Password) verify(view) .initMode( eq(KeyguardSecurityContainer.MODE_DEFAULT), eq(globalSettings), eq(falsingManager), eq(userSwitcherController), any(), eq(falsingA11yDelegate), any(), ) } @Test @DisableSceneContainer fun addUserSwitcherCallback() { val captor = ArgumentCaptor.forClass(UserSwitcherCallback::class.java) setupGetSecurityView(SecurityMode.Password) verify(view) .initMode( anyInt(), any(), any(), any(), captor.capture(), eq(falsingA11yDelegate), any(), ) captor.value.showUnlockToContinueMessage() viewControllerImmediately verify(keyguardPasswordViewControllerMock) .showMessage( /* message= */ context.getString(R.string.keyguard_unlock_to_continue), /* colorState= */ null, /* animated= */ true, ) } @Test @DisableSceneContainer fun addUserSwitchCallback() { underTest.onViewAttached() verify(userSwitcherController).addUserSwitchCallback(any()) underTest.onViewDetached() verify(userSwitcherController).removeUserSwitchCallback(any()) } @Test @DisableSceneContainer fun onBouncerVisibilityChanged_resetsScale() { underTest.onBouncerVisibilityChanged(false) verify(view).resetScale() } @Test @DisableSceneContainer fun showNextSecurityScreenOrFinish_DeviceNotSecure() { // GIVEN the current security method is SimPin whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) .thenReturn(false) underTest.showSecurityScreen(SecurityMode.SimPin) // WHEN a request is made from the SimPin screens to show the next security method whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) .thenReturn(SecurityMode.None) whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true) underTest.showNextSecurityScreenOrFinish( /* authenticated= */ true, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.SimPin, ) // THEN the next security method of None will dismiss keyguard. verify(viewMediatorCallback).keyguardDone(anyInt()) } @Test @DisableSceneContainer fun showNextSecurityScreenOrFinish_ignoresCallWhenSecurityMethodHasChanged() { // GIVEN current security mode has been set to PIN underTest.showSecurityScreen(SecurityMode.PIN) // WHEN a request comes from SimPin to dismiss the security screens val keyguardDone = underTest.showNextSecurityScreenOrFinish( /* authenticated= */ true, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.SimPin, ) // THEN no action has happened, which will not dismiss the security screens Truth.assertThat(keyguardDone).isEqualTo(false) verify(keyguardUpdateMonitor, never()).getUserHasTrust(anyInt()) } @Test @DisableSceneContainer fun showNextSecurityScreenOrFinish_SimPin_Swipe() { // GIVEN the current security method is SimPin whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) .thenReturn(false) underTest.showSecurityScreen(SecurityMode.SimPin) // WHEN a request is made from the SimPin screens to show the next security method whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) .thenReturn(SecurityMode.None) // WHEN security method is SWIPE whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false) underTest.showNextSecurityScreenOrFinish( /* authenticated= */ true, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.SimPin, ) // THEN the next security method of None will dismiss keyguard. verify(viewMediatorCallback, never()).keyguardDone(anyInt()) } @Test @DisableSceneContainer fun showNextSecurityScreenOrFinish_SimPin_Swipe_userNotSetup() { // GIVEN the current security method is SimPin whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) .thenReturn(false) underTest.showSecurityScreen(SecurityMode.SimPin) // WHEN a request is made from the SimPin screens to show the next security method whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) .thenReturn(SecurityMode.None) // WHEN security method is SWIPE whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false) whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(false) underTest.showNextSecurityScreenOrFinish( /* authenticated= */ true, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.SimPin, ) // THEN the next security method of None will dismiss keyguard. verify(viewMediatorCallback).keyguardDone(anyInt()) } @Test @DisableSceneContainer fun showNextSecurityScreenOrFinish_SimPin_Password() { // GIVEN the current security method is SimPin whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) .thenReturn(false) underTest.showSecurityScreen(SecurityMode.SimPin) // WHEN a request is made from the SimPin screens to show the next security method whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) .thenReturn(SecurityMode.Password) // WHEN security method is SWIPE whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false) whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(false) underTest.showNextSecurityScreenOrFinish( /* authenticated= */ true, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.SimPin, ) // THEN we will not show the password screen. verify(viewFlipperController, never()) .getSecurityView(eq(SecurityMode.Password), any(), any()) } @Test @DisableSceneContainer fun showNextSecurityScreenOrFinish_SimPin_SimPin() { // GIVEN the current security method is SimPin whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) .thenReturn(false) underTest.showSecurityScreen(SecurityMode.SimPin) // WHEN a request is made from the SimPin screens to show the next security method whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) .thenReturn(SecurityMode.SimPin) // WHEN security method is SWIPE whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false) whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(false) underTest.showNextSecurityScreenOrFinish( /* authenticated= */ true, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.SimPin, ) // THEN we will not show the password screen. verify(viewFlipperController).getSecurityView(eq(SecurityMode.SimPin), any(), any()) } @Test @DisableSceneContainer fun showNextSecurityScreenOrFinish_calledWithNoAuthentication_butRequiresSimPin() { // GIVEN trust is true (extended unlock) whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true) // WHEN SIMPIN is the next required screen whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) .thenReturn(SecurityMode.SimPin) // WHEN a request is made to dismiss underTest.dismiss(TARGET_USER_ID) // THEN we will show the SIM PIN screen verify(viewFlipperController).getSecurityView(eq(SecurityMode.SimPin), any(), any()) } @Test fun showNextSecurityScreenOrFinish_BiometricUnlockedWithSecondFactorEnabled_DoesNotFinish() { whenever(keyguardUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked()).thenReturn(false) whenever(keyguardUpdateMonitor.getUserHasTrust(TARGET_USER_ID)).thenReturn(false) whenever(lockPatternUtils.isBiometricSecondFactorEnabled(TARGET_USER_ID)).thenReturn(true) underTest.showSecurityScreen(SecurityMode.Password) val finish = underTest.showNextSecurityScreenOrFinish( /* authenticated= */ false, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.Password ) Assert.assertFalse(finish) } @Test @DisableSceneContainer @EnableFlags(FLAG_SECURE_LOCK_DEVICE) fun keyguardDoesNotDismiss_onPrimaryAuthSuccess_ifSecureLockDeviceEnabled() = kosmos.testScope.runTest { val isSecureLockDeviceEnabled by collectLastValue(kosmos.secureLockDeviceInteractor.isSecureLockDeviceEnabled) kosmos.fakeSecureLockDeviceRepository.onSecureLockDeviceEnabled() runCurrent() assertThat(isSecureLockDeviceEnabled).isTrue() // GIVEN current security mode has been set to PIN underTest.showSecurityScreen(SecurityMode.PIN) // WHEN a primary auth success requests to dismiss the security screen kosmos.fakeSecureLockDeviceRepository.onSuccessfulPrimaryAuth() val keyguardDone = underTest.showNextSecurityScreenOrFinish( /* authenticated= */ true, TARGET_USER_ID, /* bypassSecondaryLockScreen= */ true, SecurityMode.PIN, ) runCurrent() // THEN no action has happened, which will not dismiss the security screens assertThat(keyguardDone).isEqualTo(false) } @Test @DisableSceneContainer fun onSwipeUp_forwardsItToFaceAuthInteractor() { val registeredSwipeListener = registeredSwipeListener setupGetSecurityView(SecurityMode.Password) registeredSwipeListener.onSwipeUp() verify(faceAuthInteractor).onSwipeUpOnBouncer() } @Test @DisableSceneContainer fun onDensityOrFontScaleChanged() { val configurationListenerArgumentCaptor = ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) underTest.onViewAttached() verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) clearInvocations(viewFlipperController) configurationListenerArgumentCaptor.value.onDensityOrFontScaleChanged() verify(view).onDensityOrFontScaleChanged() } @Test @DisableSceneContainer fun onThemeChanged() { val configurationListenerArgumentCaptor = ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) underTest.onViewAttached() verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) configurationListenerArgumentCaptor.value.onThemeChanged() verify(view).reloadColors() } @Test @DisableSceneContainer fun onUiModeChanged() { val configurationListenerArgumentCaptor = ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) underTest.onViewAttached() verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) configurationListenerArgumentCaptor.value.onUiModeChanged() verify(view).reloadColors() } @Test @DisableSceneContainer fun hasDismissActions() { Assert.assertFalse("Action not set yet", underTest.hasDismissActions()) underTest.setOnDismissAction(mock(), null /* cancelAction */) Assert.assertTrue("Action should exist", underTest.hasDismissActions()) } @Test @DisableSceneContainer fun willRunDismissFromKeyguardIsTrue() { val action: OnDismissAction = mock() whenever(action.willRunAnimationOnKeyguard()).thenReturn(true) underTest.setOnDismissAction(action, null /* cancelAction */) underTest.finish(0 /* currentUser */) Truth.assertThat(underTest.willRunDismissFromKeyguard()).isTrue() } @Test @DisableSceneContainer fun willRunDismissFromKeyguardIsFalse() { val action: OnDismissAction = mock() whenever(action.willRunAnimationOnKeyguard()).thenReturn(false) underTest.setOnDismissAction(action, null /* cancelAction */) underTest.finish(0 /* currentUser */) Truth.assertThat(underTest.willRunDismissFromKeyguard()).isFalse() } @Test @DisableSceneContainer fun willRunDismissFromKeyguardIsFalseWhenNoDismissActionSet() { underTest.setOnDismissAction(null /* action */, null /* cancelAction */) underTest.finish(0 /* currentUser */) Truth.assertThat(underTest.willRunDismissFromKeyguard()).isFalse() } @Test @DisableSceneContainer fun onStartingToHide() { underTest.onStartingToHide() verify(viewFlipperController) .getSecurityView(any(), any(), onViewInflatedCallbackArgumentCaptor.capture()) onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) verify(inputViewController).onStartingToHide() } @Test @DisableSceneContainer fun startAppearAnimation_ifDelayed() { val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java) whenever(view.isAppearAnimationDelayed).thenReturn(true) val viewTreeObserver: ViewTreeObserver = mock() whenever(view.viewTreeObserver).thenReturn(viewTreeObserver) underTest.startAppearAnimationIfDelayed() verify(view).alpha = 1f verify(viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture()) argumentCaptor.value.onPreDraw() verify(view).startAppearAnimation(any(SecurityMode::class.java)) verify(view).setIsAppearAnimationDelayed(false) } @Test @DisableSceneContainer fun appearAnimation_willNotStart_ifNotDelayed() { whenever(view.isAppearAnimationDelayed).thenReturn(false) val viewTreeObserver: ViewTreeObserver = mock() whenever(view.viewTreeObserver).thenReturn(viewTreeObserver) underTest.startAppearAnimationIfDelayed() verify(view, never()).alpha verify(viewTreeObserver, never()).addOnPreDrawListener(any()) verify(view, never()).startAppearAnimation(any(SecurityMode::class.java)) } @Test @DisableSceneContainer fun gravityReappliedOnConfigurationChange() { // Set initial gravity testableResources.addOverride(R.integer.keyguard_host_view_gravity, Gravity.CENTER) testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false) // Kick off the initial pass... underTest.onInit() verify(view).layoutParams = any() clearInvocations(view) // Now simulate a config change testableResources.addOverride( R.integer.keyguard_host_view_gravity, Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, ) underTest.updateResources() verify(view).layoutParams = any() } @Test @DisableSceneContainer fun gravityUsesOneHandGravityWhenApplicable() { testableResources.addOverride(R.integer.keyguard_host_view_gravity, Gravity.CENTER) testableResources.addOverride( R.integer.keyguard_host_view_one_handed_gravity, Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, ) // Start disabled. testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false) underTest.onInit() verify(view).layoutParams = argThat( ArgumentMatcher { argument: FrameLayout.LayoutParams -> argument.gravity == Gravity.CENTER } as ArgumentMatcher ) clearInvocations(view) // And enable testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) underTest.updateResources() verify(view).layoutParams = argThat( ArgumentMatcher { argument: FrameLayout.LayoutParams -> argument.gravity == Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM } as ArgumentMatcher ) } @Test @DisableSceneContainer fun updateKeyguardPositionDelegatesToSecurityContainer() { underTest.updateKeyguardPosition(1.0f) verify(view).updatePositionByTouchX(1.0f) } @Test @DisableSceneContainer fun reinflateViewFlipper() { val onViewInflatedCallback = KeyguardSecurityViewFlipperController.OnViewInflatedCallback {} underTest.reinflateViewFlipper(onViewInflatedCallback) verify(viewFlipperController).clearViews() verify(viewFlipperController) .getSecurityView(any(), any(), onViewInflatedCallbackArgumentCaptor.capture()) onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) verify(view).updateSecurityViewFlipper() } @Test @EnableFlags(FLAG_SECURE_LOCK_DEVICE) @DisableSceneContainer fun reinflateViewFlipper_onSecureLockDeviceBiometricAuthViewShownOrInterrupted() { // On shown val onViewInflatedCallback = KeyguardSecurityViewFlipperController.OnViewInflatedCallback {} underTest.showSecureLockDeviceView(onViewInflatedCallback) verify(viewFlipperController).clearViews() verify(viewFlipperController) .getSecurityView( eq(SecurityMode.SecureLockDeviceBiometricAuth), any(), onViewInflatedCallbackArgumentCaptor.capture(), ) onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) verify(view).updateSecurityViewFlipper() clearInvocations(view) clearInvocations(viewFlipperController) // On hidden (e.g. back gesture, dozing, biometric lockout, etc) kosmos.fakeSecureLockDeviceRepository.setRequiresPrimaryAuthForSecureLockDevice(true) underTest.onSecureLockDeviceBiometricAuthInterrupted(onViewInflatedCallback) verify(viewFlipperController).clearViews() verify(viewFlipperController) .getSecurityView(any(), any(), onViewInflatedCallbackArgumentCaptor.capture()) onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) verify(view).updateSecurityViewFlipper() verify(viewMediatorCallback).resetKeyguard() } @Test @DisableSceneContainer fun setExpansion_setsAlpha() { underTest.setExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE) verify(view).alpha = 1f verify(view).translationY = 0f } @Test @EnableSceneContainer fun dismissesKeyguard_whenSceneChangesToGone() = kosmos.testScope.runTest { kosmos.enableSingleShade() runCurrent() // Collect sceneInteractor.currentOverlays so that show/hideOverlay receive updated // overlay state during validation val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) // Upon init, we have never dismisses the keyguard. underTest.onInit() runCurrent() verify(primaryBouncerInteractor, never()) .notifyKeyguardAuthenticatedPrimaryAuth(anyInt()) // Once the view is attached, we start listening but simply going to the bouncer scene // is not enough to trigger a dismissal of the keyguard. underTest.onViewAttached() fakeSceneDataSource.pause() sceneInteractor.changeScene(Scenes.Lockscreen, "reason") sceneInteractor.showOverlay(Overlays.Bouncer, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition.showOverlay( overlay = Overlays.Bouncer, fromScene = Scenes.Lockscreen, currentOverlays = flowOf(setOf(Overlays.Bouncer)), progress = flowOf(.5f), isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), ) runCurrent() fakeSceneDataSource.unpause(expectedOverlays = setOf(Overlays.Bouncer)) sceneTransitionStateFlow.value = ObservableTransitionState.Idle( currentScene = Scenes.Lockscreen, currentOverlays = setOf(Overlays.Bouncer), ) runCurrent() verify(primaryBouncerInteractor, never()) .notifyKeyguardAuthenticatedPrimaryAuth(anyInt()) // While listening, going from the bouncer scene to the gone scene, does dismiss the // keyguard. kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) ) runCurrent() fakeSceneDataSource.pause() sceneInteractor.changeScene(Scenes.Gone, "reason") sceneInteractor.hideOverlay(Overlays.Bouncer, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition.hideOverlay( overlay = Overlays.Bouncer, toScene = Scenes.Gone, currentOverlays = flowOf(emptySet()), progress = flowOf(.5f), isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), ) runCurrent() fakeSceneDataSource.unpause(expectedScene = Scenes.Gone, expectedOverlays = emptySet()) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) runCurrent() verify(primaryBouncerInteractor).notifyKeyguardAuthenticatedPrimaryAuth(anyInt()) // While listening, moving back to the bouncer scene does not dismiss the keyguard // again. clearInvocations(primaryBouncerInteractor) // switch to a different non-keyguard scene since showing overlay over Gone is // prohibited sceneInteractor.snapToScene(Scenes.Shade, "reason") runCurrent() fakeSceneDataSource.pause() sceneInteractor.showOverlay(Overlays.Bouncer, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition.showOverlay( overlay = Overlays.Bouncer, fromScene = Scenes.Shade, currentOverlays = flowOf(setOf(Overlays.Bouncer)), progress = flowOf(.5f), isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), ) runCurrent() fakeSceneDataSource.unpause(expectedOverlays = setOf(Overlays.Bouncer)) sceneTransitionStateFlow.value = ObservableTransitionState.Idle( currentScene = Scenes.Lockscreen, currentOverlays = setOf(Overlays.Bouncer), ) runCurrent() verify(primaryBouncerInteractor, never()) .notifyKeyguardAuthenticatedPrimaryAuth(anyInt()) // Detaching the view stops listening, so moving from the bouncer scene to the gone // scene // does not dismiss the keyguard while we're not listening. underTest.onViewDetached() fakeSceneDataSource.pause() sceneInteractor.changeScene(Scenes.Gone, "reason") sceneInteractor.hideOverlay(Overlays.Bouncer, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition.hideOverlay( overlay = Overlays.Bouncer, toScene = Scenes.Gone, currentOverlays = flowOf(emptySet()), progress = flowOf(.5f), isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), ) runCurrent() fakeSceneDataSource.unpause(expectedScene = Scenes.Gone, expectedOverlays = emptySet()) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) runCurrent() verify(primaryBouncerInteractor, never()) .notifyKeyguardAuthenticatedPrimaryAuth(anyInt()) // While not listening, moving to the lockscreen does not dismiss the keyguard. fakeSceneDataSource.pause() sceneInteractor.changeScene(Scenes.Lockscreen, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( Scenes.Gone, Scenes.Lockscreen, flowOf(Scenes.Lockscreen), flowOf(.5f), false, isUserInputOngoing = flowOf(false), ) runCurrent() fakeSceneDataSource.unpause(expectedScene = Scenes.Lockscreen) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Lockscreen) runCurrent() verify(primaryBouncerInteractor, never()) .notifyKeyguardAuthenticatedPrimaryAuth(anyInt()) // Reattaching the view starts listening again so moving from the bouncer scene to the // gone scene now does dismiss the keyguard again, this time from lockscreen. underTest.onViewAttached() fakeSceneDataSource.pause() sceneInteractor.changeScene(Scenes.Gone, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( Scenes.Lockscreen, Scenes.Gone, flowOf(Scenes.Gone), flowOf(.5f), false, isUserInputOngoing = flowOf(false), ) runCurrent() fakeSceneDataSource.unpause(expectedScene = Scenes.Gone) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) runCurrent() verify(primaryBouncerInteractor).notifyKeyguardAuthenticatedPrimaryAuth(anyInt()) } @Test @DisableSceneContainer fun testResetUserSwitcher() { val userSwitcher = mock(View::class.java) whenever(view.findViewById(R.id.keyguard_bouncer_user_switcher)) .thenReturn(userSwitcher) underTest.prepareToShow() verify(userSwitcher).setAlpha(0f) } @Test @DisableSceneContainer fun testOnUserSwitched() { val userSwitchCallbackArgumentCaptor = argumentCaptor() underTest.onViewAttached() verify(userSwitcherController) .addUserSwitchCallback(capture(userSwitchCallbackArgumentCaptor)) reset(primaryBouncerInteractor) userSwitchCallbackArgumentCaptor.value.onUserSwitched() verify(primaryBouncerInteractor).setLastShownPrimarySecurityScreen(any()) } @Test @DisableSceneContainer fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() { val mainUserId = 10 underTest.showMessageForFailedUnlockAttempt( /* userId = */ mainUserId, /* expiringUserId = */ mainUserId, /* mainUserId = */ mainUserId, /* remainingBeforeWipe = */ 1, /* failedAttempts = */ 1, ) verify(view) .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY)) } @Test @DisableSceneContainer fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() { val secondaryUserId = 10 val mainUserId = 0 underTest.showMessageForFailedUnlockAttempt( /* userId = */ secondaryUserId, /* expiringUserId = */ secondaryUserId, /* mainUserId = */ mainUserId, /* remainingBeforeWipe = */ 1, /* failedAttempts = */ 1, ) verify(view) .showAlmostAtWipeDialog( any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER), ) } private val registeredKeyguardUpdateMonitorCallback: KeyguardUpdateMonitorCallback get() { underTest.onViewAttached() verify(keyguardUpdateMonitor) .registerCallback(keyguardUpdateMonitorCallbackCaptor.capture()) return keyguardUpdateMonitorCallbackCaptor.value } private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener get() { underTest.onViewAttached() verify(view).setSwipeListener(swipeListenerArgumentCaptor.capture()) return swipeListenerArgumentCaptor.value } private fun setupGetSecurityView(securityMode: SecurityMode) { underTest.showSecurityScreen(securityMode) viewControllerImmediately } private val viewControllerImmediately: Unit get() { verify(viewFlipperController, atLeastOnce()) .getSecurityView(any(), any(), onViewInflatedCallbackArgumentCaptor.capture()) @Suppress("UNCHECKED_CAST") onViewInflatedCallbackArgumentCaptor.value.onViewInflated( keyguardPasswordViewControllerMock as KeyguardInputViewController ) } companion object { private const val TARGET_USER_ID = 100 } }