Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 71d51cd9 authored by Grace Cheng's avatar Grace Cheng
Browse files

Implement face auth success confirm button

Adds confirmation button after successful face auth on the bouncer for
secure lock device, and triggers all other disable secure lock device
logic. All device unlock and device entry logic and UI is also updated
to reflect the pending confirmation state

Flag: android.security.secure_lock_device
Fixes: 405120698
Fixes: 417071875
Bug: 401645997
Test: atest SecureLockDeviceServiceTest
Test: atest AuthenticationPolicyServiceTest
Test: atest SecureLockDeviceInteractorTest
Test: atest SceneContainerStartableTest
Test: atest DeviceEntryFaceAuthInteractorTest
Test: atest DeviceUnlockedInteractorTest
Test: atest SecureLockDeviceBiometricAuthContentViewModelTest
Test: atest BouncerOverlayContentViewModelTest
Test: atest CtsSecurityTestCases:android.security.cts.authenticationpolicy.AuthenticationPolicyManagerTest
Change-Id: I06abe274d300702161b3fe78b4dbcd48a54e24ec
parent ef6c53ba
Loading
Loading
Loading
Loading
+41 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.bouncer.domain.interactor

import android.platform.test.annotations.EnableFlags
import android.security.Flags.FLAG_SECURE_LOCK_DEVICE
import android.telecom.TelecomManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -27,7 +29,10 @@ import com.android.internal.util.emergencyAffordanceManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
import com.android.systemui.bouncer.shared.model.SecureLockDeviceBouncerActionButtonModel
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
@@ -35,6 +40,8 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.securelockdevice.data.repository.fakeSecureLockDeviceRepository
import com.android.systemui.securelockdevice.ui.viewmodel.secureLockDeviceBiometricAuthContentViewModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
@@ -44,6 +51,7 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.whenever
import com.android.telecom.telecomManager
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -222,6 +230,39 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() {
            assertThat(actionButton).isNull()
        }

    @EnableFlags(FLAG_SECURE_LOCK_DEVICE)
    @Test
    fun showsConfirmButtonAfterFaceAuthSuccessInSecureLockDevice() =
        testScope.runTest {
            val underTest = kosmos.bouncerActionButtonInteractor
            val secureLockDeviceActionButton by
                collectLastValue(underTest.secureLockDeviceActionButton)
            val activateViewModelJob = launch {
                kosmos.secureLockDeviceBiometricAuthContentViewModel.activate()
            }
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
            kosmos.fakeSecureLockDeviceRepository.onSecureLockDeviceEnabled()
            runCurrent()

            // After PIN auth
            kosmos.fakeSecureLockDeviceRepository.onSuccessfulPrimaryAuth()
            runCurrent()

            // After face auth success
            kosmos.secureLockDeviceBiometricAuthContentViewModel.showAuthenticated(
                modality = BiometricModality.Face
            )
            runCurrent()

            assertThat(
                    secureLockDeviceActionButton
                        is
                        SecureLockDeviceBouncerActionButtonModel.ConfirmStrongBiometricAuthButtonModel
                )
                .isTrue()
            activateViewModelJob.cancel()
        }

    companion object {
        private const val MESSAGE_EMERGENCY_CALL = "Emergency"
        private const val MESSAGE_RETURN_TO_CALL = "Return to call"
+12 −0
Original line number Diff line number Diff line
@@ -905,6 +905,18 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
            assertThat(faceAuthRepository.runningAuthRequest.value).isNull()
        }

    @EnableFlags(FLAG_SECURE_LOCK_DEVICE)
    @Test
    fun faceAuthIsNotRequestedWhenPendingConfirmation_inSecureLockDeviceMode() =
        kosmos.runTest {
            underTest.onSecureLockDeviceConfirmButtonShowingChanged(true)
            underTest.start()
            underTest.onSwipeUpOnBouncer()

            runCurrent()
            assertThat(faceAuthRepository.runningAuthRequest.value).isNull()
        }

    @Test
    fun lockedOut_providesSameValueFromRepository() =
        kosmos.runTest {
+100 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.deviceentry.domain.interactor

import android.content.pm.UserInfo
import android.hardware.face.FaceManager
import android.os.PowerManager
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
@@ -32,6 +33,7 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryBypassRepository
import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
import com.android.systemui.flags.fakeSystemPropertiesHelper
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
@@ -63,6 +65,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -770,6 +773,103 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() {
            assertThat(isSecureLockDeviceEnabled).isFalse()
        }

    @Test
    @EnableFlags(FLAG_SECURE_LOCK_DEVICE)
    fun deviceUnlockStatus_updatesAcrossTwoFactorBouncerUnlock_whenSecureLockDeviceEnabled_face() =
        testScope.runTest {
            val deviceEntryRestrictionReason by
                collectLastValue(underTest.deviceEntryRestrictionReason)
            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
            val requiresPrimaryAuthForSecureLockDevice by
                collectLastValue(
                    kosmos.secureLockDeviceInteractor.requiresPrimaryAuthForSecureLockDevice
                )
            val requiresStrongBiometricAuthForSecureLockDevice by
                collectLastValue(
                    kosmos.secureLockDeviceInteractor.requiresStrongBiometricAuthForSecureLockDevice
                )
            val isSecureLockDeviceEnabled by collectLastValue(underTest.isSecureLockDeviceEnabled)

            // Enroll face, configure PIN as primary auth method
            kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
            authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)

            // Mock secure lock device enabled, both StrongAuthFlags set
            kosmos.fakeSecureLockDeviceRepository.onSecureLockDeviceEnabled()
            kosmos.biometricSettingsRepository.setAuthenticationFlags(
                AuthenticationFlags(
                    userId = primaryUserId,
                    flag =
                        LockPatternUtils.StrongAuthTracker
                            .PRIMARY_AUTH_REQUIRED_FOR_SECURE_LOCK_DEVICE or
                            LockPatternUtils.StrongAuthTracker
                                .STRONG_BIOMETRIC_AUTH_REQUIRED_FOR_SECURE_LOCK_DEVICE,
                )
            )
            runCurrent()

            // Assert device is in secure lock device and sets device entry restriction reason,
            // requires bouncer unlock for lockdown
            assertThat(deviceEntryRestrictionReason)
                .isEqualTo(DeviceEntryRestrictionReason.SecureLockDevicePrimaryAuth)
            assertThat(requiresPrimaryAuthForSecureLockDevice).isTrue()
            assertThat(requiresStrongBiometricAuthForSecureLockDevice).isFalse()
            assertThat(isSecureLockDeviceEnabled).isTrue()

            // Assert device is locked, null deviceUnlockSource
            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
            assertThat(deviceUnlockStatus?.deviceUnlockSource).isNull()

            // Mock primary auth on bouncer
            kosmos.fakeSecureLockDeviceRepository.onSuccessfulPrimaryAuth()
            authenticationRepository.reportAuthenticationAttempt(true)

            // Mock primary auth secure lock device flag cleared
            kosmos.fakeSecureLockDeviceRepository.onSuccessfulPrimaryAuth()
            kosmos.biometricSettingsRepository.setAuthenticationFlags(
                AuthenticationFlags(
                    userId = primaryUserId,
                    flag =
                        LockPatternUtils.StrongAuthTracker
                            .STRONG_BIOMETRIC_AUTH_REQUIRED_FOR_SECURE_LOCK_DEVICE,
                )
            )
            runCurrent()

            // Assert device is in secure lock device and updates device entry restriction reason,
            // no longer requires primary auth on bouncer
            assertThat(deviceEntryRestrictionReason)
                .isEqualTo(DeviceEntryRestrictionReason.SecureLockDeviceStrongBiometricOnlyAuth)
            assertThat(requiresPrimaryAuthForSecureLockDevice).isFalse()
            assertThat(requiresStrongBiometricAuthForSecureLockDevice).isTrue()
            assertThat(isSecureLockDeviceEnabled).isTrue()

            // Assert device is still locked, deviceUnlockSource does not update
            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
            assertThat(deviceUnlockStatus?.deviceUnlockSource).isNull()

            // Mock successful strong face auth
            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
                SuccessFaceAuthenticationStatus(
                    successResult = mock(FaceManager.AuthenticationResult::class.java)
                )
            )
            kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true
            runCurrent()

            // Assert device is still locked while pending confirmation
            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
            assertThat(deviceUnlockStatus?.deviceUnlockSource).isNull()

            // Face auth confirmed, pending -> confirmed animation played
            kosmos.secureLockDeviceInteractor.onReadyToDismissBiometricAuth()

            // Assert device is now unlocked, deviceUnlockSource updates to face
            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
            assertThat(deviceUnlockStatus?.deviceUnlockSource)
                .isEqualTo(DeviceUnlockSource.FaceWithoutBypass)
        }

    private fun TestScope.unlockDevice() {
        val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)

+60 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.os.PowerManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import android.security.Flags.FLAG_SECURE_LOCK_DEVICE
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -59,6 +60,7 @@ import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepositor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
@@ -108,6 +110,8 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
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.data.repository.fakeShadeDisplaysRepository
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.enableSingleShade
@@ -2906,6 +2910,62 @@ class SceneContainerStartableTest : SysuiTestCase() {
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
        }

    @EnableFlags(FLAG_SECURE_LOCK_DEVICE)
    @Test
    fun doesNotUnlock_onFaceAuthSuccess_untilConfirmedAndReadyToDismissInSecureLockDevice() =
        kosmos.runTest {
            val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            val isSecureLockDeviceEnabled by
                collectLastValue(kosmos.secureLockDeviceInteractor.isSecureLockDeviceEnabled)
            val isFullyUnlockedAndReadyToDismiss by
                collectLastValue(kosmos.secureLockDeviceInteractor.isFullyUnlockedAndReadyToDismiss)

            val transitionState =
                prepareState(
                    authenticationMethod = AuthenticationMethodModel.Pin,
                    isDeviceUnlocked = false,
                    initialSceneKey = Scenes.Lockscreen,
                )
            kosmos.fakeSecureLockDeviceRepository.onSecureLockDeviceEnabled()
            runCurrent()

            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            underTest.start()
            runCurrent()

            sceneInteractor.showOverlay(Overlays.Bouncer, "showing bouncer for test")
            transitionState.value =
                ObservableTransitionState.Idle(Scenes.Lockscreen, setOf(Overlays.Bouncer))
            runCurrent()
            assertThat(currentOverlays).contains(Overlays.Bouncer)

            kosmos.fakeSecureLockDeviceRepository.onSuccessfulPrimaryAuth()
            kosmos.secureLockDeviceInteractor.onBiometricAuthRequested()

            updateFaceAuthStatus(isSuccess = true)

            assertThat(isSecureLockDeviceEnabled).isTrue()
            assertThat(isFullyUnlockedAndReadyToDismiss).isFalse()
            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
            assertThat(deviceUnlockStatus?.deviceUnlockSource).isNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays).contains(Overlays.Bouncer)

            // Face auth confirm button clicked, pending -> confirmed auth animation played
            kosmos.secureLockDeviceInteractor.onReadyToDismissBiometricAuth()
            runCurrent()

            assertThat(isSecureLockDeviceEnabled).isTrue()
            assertThat(isFullyUnlockedAndReadyToDismiss).isTrue()
            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
            assertThat(deviceUnlockStatus?.deviceUnlockSource)
                .isEqualTo(DeviceUnlockSource.FaceWithoutBypass)
            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
            assertThat(currentOverlays).doesNotContain(Overlays.Bouncer)
        }

    private fun Kosmos.emulateSceneTransition(
        transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
        toScene: SceneKey,
+54 −0
Original line number Diff line number Diff line
@@ -27,8 +27,10 @@ import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRe
import com.android.systemui.biometrics.faceSensorPropertiesInternal
import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.biometrics.shared.model.toFaceSensorInfo
import com.android.systemui.biometrics.shared.model.toFingerprintSensorInfo
import com.android.systemui.biometrics.ui.viewmodel.PromptAuthState
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.kosmos.testScope
@@ -161,4 +163,56 @@ class SecureLockDeviceInteractorTest : SysuiTestCase() {
            assertThat(suppressBouncerMessageUpdates).isFalse()
        }
    }

    @Test
    fun showsConfirmButtonOnPendingFaceAuth_hidesOnBiometricAuthHidden() {
        testScope.runTest {
            val showingConfirmButton by collectLastValue(underTest.showConfirmBiometricAuthButton)

            underTest.onBiometricAuthRequested()
            runCurrent()
            assertThat(showingConfirmButton).isFalse()

            underTest.onBiometricAuthenticatedStateUpdated(
                PromptAuthState(
                    isAuthenticated = true,
                    authenticatedModality = BiometricModality.Face,
                    needsUserConfirmation = true,
                )
            )
            runCurrent()
            assertThat(showingConfirmButton).isTrue()

            underTest.onBiometricAuthUiHidden()
            runCurrent()
            assertThat(showingConfirmButton).isFalse()
        }
    }

    @Test
    fun stopsListeningForBiometricAuth_whileConfirmButtonIsShown() {
        testScope.runTest {
            val shouldListenForBiometricAuth by
                collectLastValue(underTest.shouldListenForBiometricAuth)
            val showingConfirmButton by collectLastValue(underTest.showConfirmBiometricAuthButton)

            kosmos.fakeSecureLockDeviceRepository.setRequiresStrongBiometricAuthForSecureLockDevice(
                true
            )
            underTest.onBiometricAuthRequested()
            runCurrent()
            assertThat(shouldListenForBiometricAuth).isTrue()

            underTest.onBiometricAuthenticatedStateUpdated(
                PromptAuthState(
                    isAuthenticated = true,
                    authenticatedModality = BiometricModality.Face,
                    needsUserConfirmation = true,
                )
            )
            runCurrent()
            assertThat(showingConfirmButton).isTrue()
            assertThat(shouldListenForBiometricAuth).isFalse()
        }
    }
}
Loading