diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt index 6fd3e21b25a4d..30f8f3edfa8fb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt @@ -109,6 +109,18 @@ interface DeviceEntryFaceAuthRepository { /** Set whether face authentication should be locked out or not */ fun lockoutFaceAuth() + /** + * Cancel current face authentication and prevent it from running until [resumeFaceAuth] is + * invoked. + */ + fun pauseFaceAuth() + + /** + * Allow face auth paused using [pauseFaceAuth] to run again. The next invocation to + * [authenticate] will run as long as other gating conditions don't stop it from running. + */ + fun resumeFaceAuth() + /** * Trigger face authentication. * @@ -186,6 +198,15 @@ constructor( override val isAuthRunning: StateFlow get() = _isAuthRunning + private val faceAuthPaused = MutableStateFlow(false) + override fun pauseFaceAuth() { + faceAuthPaused.value = true + } + + override fun resumeFaceAuth() { + faceAuthPaused.value = false + } + private val keyguardSessionId: InstanceId? get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD) @@ -329,11 +350,7 @@ constructor( "isFaceAuthenticationEnabled", tableLogBuffer ), - logAndObserve( - userRepository.userSwitchingInProgress.isFalse(), - "userSwitchingNotInProgress", - tableLogBuffer - ), + logAndObserve(faceAuthPaused.isFalse(), "faceAuthIsNotPaused", tableLogBuffer), logAndObserve( keyguardRepository.isKeyguardGoingAway.isFalse(), "keyguardNotGoingAway", @@ -454,7 +471,6 @@ constructor( } private fun handleFaceCancellationError() { - cancelNotReceivedHandlerJob?.cancel() applicationScope.launch { faceAuthRequestedWhileCancellation?.let { faceAuthLogger.launchingQueuedFaceAuthRequest(it) @@ -483,6 +499,7 @@ constructor( } private fun onFaceAuthRequestCompleted() { + cancelNotReceivedHandlerJob?.cancel() cancellationInProgress = false _isAuthRunning.value = false authCancellationSignal = null diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt index 5ef9a9e0482cf..e4e6a6dae6b0e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt @@ -56,6 +56,9 @@ class NoopDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceA get() = emptyFlow() override fun lockoutFaceAuth() = Unit + override fun pauseFaceAuth() = Unit + + override fun resumeFaceAuth() = Unit /** * Trigger face authentication. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt index 8f4776fa2ed3b..2a3f8520a63c9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt @@ -35,6 +35,7 @@ import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger +import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Encapsulates business logic related face authentication being triggered for device entry from @@ -69,6 +71,7 @@ constructor( private val faceAuthenticationLogger: FaceAuthenticationLogger, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, + private val userRepository: UserRepository, ) : CoreStartable, KeyguardFaceAuthInteractor { private val listeners: MutableList = mutableListOf() @@ -128,6 +131,23 @@ constructor( } } .launchIn(applicationScope) + + // User switching should stop face auth and then when it is complete we should trigger face + // auth so that the switched user can unlock the device with face auth. + userRepository.userSwitchingInProgress + .pairwise(false) + .onEach { (wasSwitching, isSwitching) -> + if (!wasSwitching && isSwitching) { + repository.pauseFaceAuth() + } else if (wasSwitching && !isSwitching) { + repository.resumeFaceAuth() + runFaceAuth( + FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING, + fallbackToDetect = true + ) + } + } + .launchIn(applicationScope) } override fun onSwipeUpOnBouncer() { @@ -199,8 +219,10 @@ constructor( } else { faceAuthenticationStatusOverride.value = null applicationScope.launch { - faceAuthenticationLogger.authRequested(uiEvent) - repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect) + withContext(mainDispatcher) { + faceAuthenticationLogger.authRequested(uiEvent) + repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect) + } } } } else { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 01a6c64a6898e..85ee0e4d6ec36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -541,10 +541,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun authenticateDoesNotRunIfUserIsCurrentlySwitching() = - testScope.runTest { - testGatingCheckForFaceAuth { fakeUserRepository.setUserSwitching(true) } - } + fun authenticateDoesNotRunIfFaceAuthIsCurrentlyPaused() = + testScope.runTest { testGatingCheckForFaceAuth { underTest.pauseFaceAuth() } } @Test fun authenticateDoesNotRunIfKeyguardIsNotShowing() = @@ -840,7 +838,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { @Test fun detectDoesNotRunWhenUserSwitchingInProgress() = - testScope.runTest { testGatingCheckForDetect { fakeUserRepository.setUserSwitching(true) } } + testScope.runTest { testGatingCheckForDetect { underTest.pauseFaceAuth() } } @Test fun detectDoesNotRunWhenKeyguardGoingAway() = @@ -1130,7 +1128,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { .addLockoutResetCallback(faceLockoutResetCallback.capture()) biometricSettingsRepository.setFaceEnrolled(true) biometricSettingsRepository.setIsFaceAuthEnabled(true) - fakeUserRepository.setUserSwitching(false) + underTest.resumeFaceAuth() trustRepository.setCurrentUserTrusted(false) keyguardRepository.setKeyguardGoingAway(false) keyguardRepository.setWakefulnessModel( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt index 8636dd8df3b03..93f208ee14f5f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -74,6 +75,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository + private lateinit var fakeUserRepository: FakeUserRepository private lateinit var fakeDeviceEntryFingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository @@ -98,6 +100,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { .keyguardTransitionInteractor fakeDeviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() + fakeUserRepository = FakeUserRepository() underTest = SystemUIKeyguardFaceAuthInteractor( mContext, @@ -131,7 +134,8 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { featureFlags, FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")), keyguardUpdateMonitor, - fakeDeviceEntryFingerprintAuthRepository + fakeDeviceEntryFingerprintAuthRepository, + fakeUserRepository, ) } @@ -211,6 +215,38 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { ) } + @Test + fun faceAuthIsPausedWhenUserSwitchingIsInProgress() = + testScope.runTest { + underTest.start() + + fakeUserRepository.setUserSwitching(false) + runCurrent() + fakeUserRepository.setUserSwitching(true) + runCurrent() + + assertThat(faceAuthRepository.isFaceAuthPaused()).isTrue() + } + + @Test + fun faceAuthIsUnpausedWhenUserSwitchingIsInComplete() = + testScope.runTest { + underTest.start() + + // previously running + fakeUserRepository.setUserSwitching(true) + runCurrent() + fakeUserRepository.setUserSwitching(false) + runCurrent() + + assertThat(faceAuthRepository.isFaceAuthPaused()).isFalse() + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value!!.first) + .isEqualTo(FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING) + assertThat(faceAuthRepository.runningAuthRequest.value!!.second).isEqualTo(true) + } + @Test fun faceAuthIsRequestedWhenPrimaryBouncerIsVisible() = testScope.runTest { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt index f4c2db1b944e0..1e1dc4fc59b17 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt @@ -61,6 +61,19 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { _wasDisabled = true } + private val faceAuthPaused = MutableStateFlow(false) + override fun pauseFaceAuth() { + faceAuthPaused.value = true + } + + override fun resumeFaceAuth() { + faceAuthPaused.value = false + } + + fun isFaceAuthPaused(): Boolean { + return faceAuthPaused.value + } + override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) { _runningAuthRequest.value = uiEvent to fallbackToDetection _isAuthRunning.value = true