diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index cb891063385ff..6ac51cd52b495 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule; +import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; @@ -66,8 +67,6 @@ import java.util.concurrent.Executor; import dagger.Lazy; import dagger.Module; import dagger.Provides; -import kotlinx.coroutines.CoroutineDispatcher; -import kotlinx.coroutines.CoroutineScope; /** * Dagger Module providing keyguard. @@ -82,6 +81,7 @@ import kotlinx.coroutines.CoroutineScope; KeyguardDataQuickAffordanceModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, + KeyguardFaceAuthModule.class, StartKeyguardTransitionModule.class, }) public class KeyguardModule { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index d5129a612b044..09002fded4b83 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -87,6 +87,13 @@ interface BiometricSettingsRepository { */ val isStrongBiometricAllowed: StateFlow + /** + * Whether the current user is allowed to use a convenience biometric for device entry based on + * Android Security policies. If false, the user may be able to use strong biometric or primary + * authentication for device entry. + */ + val isNonStrongBiometricAllowed: StateFlow + /** Whether fingerprint feature is enabled for the current user by the DevicePolicy */ val isFingerprintEnabledByDevicePolicy: StateFlow @@ -276,6 +283,16 @@ constructor( ) ) + override val isNonStrongBiometricAllowed: StateFlow = + strongAuthTracker.isNonStrongBiometricAllowed.stateIn( + scope, + SharingStarted.Eagerly, + strongAuthTracker.isBiometricAllowedForUser( + false, + userRepository.getSelectedUserInfo().id + ) + ) + override val isFingerprintEnabledByDevicePolicy: StateFlow = selectedUserId .flatMapLatest { userId -> @@ -297,40 +314,62 @@ constructor( private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) : LockPatternUtils.StrongAuthTracker(context) { - private val _authFlags = + // Backing field for onStrongAuthRequiredChanged + private val _strongAuthFlags = MutableStateFlow( StrongAuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)) ) + // Backing field for onIsNonStrongBiometricAllowedChanged + private val _nonStrongBiometricAllowed = + MutableStateFlow( + Pair(currentUserId, isNonStrongBiometricAllowedAfterIdleTimeout(currentUserId)) + ) + val currentUserAuthFlags: Flow = userRepository.selectedUserInfo .map { it.id } .distinctUntilChanged() - .flatMapLatest { currUserId -> - _authFlags - .filter { it.userId == currUserId } + .flatMapLatest { userId -> + _strongAuthFlags + .filter { it.userId == userId } .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") } .onStart { - emit( - StrongAuthenticationFlags( - currentUserId, - getStrongAuthForUser(currentUserId) - ) - ) + emit(StrongAuthenticationFlags(userId, getStrongAuthForUser(userId))) } } + /** isStrongBiometricAllowed for the current user. */ val isStrongBiometricAllowed: Flow = currentUserAuthFlags.map { isBiometricAllowedForUser(true, it.userId) } + /** isNonStrongBiometricAllowed for the current user. */ + val isNonStrongBiometricAllowed: Flow = + userRepository.selectedUserInfo + .map { it.id } + .distinctUntilChanged() + .flatMapLatest { userId -> + _nonStrongBiometricAllowed + .filter { it.first == userId } + .map { it.second } + .onEach { Log.d(TAG, "isNonStrongBiometricAllowed changed for current user") } + .onStart { emit(isNonStrongBiometricAllowedAfterIdleTimeout(userId)) } + } + private val currentUserId get() = userRepository.getSelectedUserInfo().id override fun onStrongAuthRequiredChanged(userId: Int) { val newFlags = getStrongAuthForUser(userId) - _authFlags.value = StrongAuthenticationFlags(userId, newFlags) + _strongAuthFlags.value = StrongAuthenticationFlags(userId, newFlags) Log.d(TAG, "onStrongAuthRequiredChanged for userId: $userId, flag value: $newFlags") } + + override fun onIsNonStrongBiometricAllowedChanged(userId: Int) { + val allowed = isNonStrongBiometricAllowedAfterIdleTimeout(userId) + _nonStrongBiometricAllowed.value = Pair(userId, allowed) + Log.d(TAG, "onIsNonStrongBiometricAllowedChanged for userId: $userId, $allowed") + } } private fun DevicePolicyManager.isFaceDisabled(userId: Int): Boolean = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index 4fa56ee8e4d29..52234b32b83a5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.data.repository +import android.hardware.biometrics.BiometricAuthenticator +import android.hardware.biometrics.BiometricAuthenticator.Modality import android.hardware.biometrics.BiometricSourceType import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback @@ -33,6 +35,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn /** Encapsulates state about device entry fingerprint auth mechanism. */ @@ -49,7 +52,7 @@ interface DeviceEntryFingerprintAuthRepository { /** * Fingerprint sensor type present on the device, null if fingerprint sensor is not available. */ - val availableFpSensorType: BiometricType? + val availableFpSensorType: Flow } /** @@ -77,11 +80,39 @@ constructor( pw.println("isLockedOut=${isLockedOut.value}") } - override val availableFpSensorType: BiometricType? - get() = - if (authController.isUdfpsSupported) BiometricType.UNDER_DISPLAY_FINGERPRINT - else if (authController.isSfpsSupported) BiometricType.SIDE_FINGERPRINT - else if (authController.isRearFpsSupported) BiometricType.REAR_FINGERPRINT else null + override val availableFpSensorType: Flow + get() { + return if (authController.areAllFingerprintAuthenticatorsRegistered()) { + flowOf(getFpSensorType()) + } else { + conflatedCallbackFlow { + val callback = + object : AuthController.Callback { + override fun onAllAuthenticatorsRegistered(@Modality modality: Int) { + if (modality == BiometricAuthenticator.TYPE_FINGERPRINT) + trySendWithFailureLogging( + getFpSensorType(), + TAG, + "onAllAuthenticatorsRegistered, emitting fpSensorType" + ) + } + } + authController.addCallback(callback) + trySendWithFailureLogging( + getFpSensorType(), + TAG, + "initial value for fpSensorType" + ) + awaitClose { authController.removeCallback(callback) } + } + } + } + + private fun getFpSensorType(): BiometricType? { + return if (authController.isUdfpsSupported) BiometricType.UNDER_DISPLAY_FINGERPRINT + else if (authController.isSfpsSupported) BiometricType.SIDE_FINGERPRINT + else if (authController.isRearFpsSupported) BiometricType.REAR_FINGERPRINT else null + } override val isLockedOut: StateFlow = conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt index a3268405a830e..88c340bfb31c7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt @@ -26,10 +26,14 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.FaceAuthUiEvent import com.android.systemui.Dumpable import com.android.systemui.R +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus import com.android.systemui.keyguard.shared.model.AuthenticationStatus import com.android.systemui.keyguard.shared.model.DetectionStatus @@ -37,6 +41,7 @@ import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus +import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.log.SessionTracker import com.android.systemui.statusbar.phone.KeyguardBypassController @@ -48,10 +53,19 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -60,24 +74,11 @@ import kotlinx.coroutines.withContext * biometric prompt). */ interface KeyguardFaceAuthManager { - /** - * Trigger face authentication. - * - * [uiEvent] provided should be logged whenever face authentication runs. Invocation should be - * ignored if face authentication is already running. Results should be propagated through - * [authenticationStatus] - */ - suspend fun authenticate(uiEvent: FaceAuthUiEvent) + /** Provide the current face authentication state for device entry. */ + val isAuthenticated: Flow - /** - * Trigger face detection. - * - * Invocation should be ignored if face authentication is currently running. - */ - suspend fun detect() - - /** Stop currently running face authentication or detection. */ - fun cancel() + /** Whether face auth can run at this point. */ + val canRunFaceAuth: Flow /** Provide the current status of face authentication. */ val authenticationStatus: Flow @@ -91,8 +92,19 @@ interface KeyguardFaceAuthManager { /** Current state of whether face authentication is running. */ val isAuthRunning: Flow - /** Is face detection supported. */ - val isDetectionSupported: Boolean + /** + * Trigger face authentication. + * + * [uiEvent] provided should be logged whenever face authentication runs. Invocation should be + * ignored if face authentication is already running. Results should be propagated through + * [authenticationStatus] + * + * Run only face detection when [fallbackToDetection] is true and [canRunFaceAuth] is false. + */ + suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean = false) + + /** Stop currently running face authentication or detection. */ + fun cancel() } @SysUISingleton @@ -108,13 +120,68 @@ constructor( private val sessionTracker: SessionTracker, private val uiEventsLogger: UiEventLogger, private val faceAuthLogger: FaceAuthenticationLogger, + private val biometricSettingsRepository: BiometricSettingsRepository, + private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, + private val trustRepository: TrustRepository, + private val keyguardRepository: KeyguardRepository, + private val keyguardInteractor: KeyguardInteractor, + private val alternateBouncerInteractor: AlternateBouncerInteractor, dumpManager: DumpManager, ) : KeyguardFaceAuthManager, Dumpable { - private var cancellationSignal: CancellationSignal? = null - private val lockscreenBypassEnabled: Boolean - get() = keyguardBypassController?.bypassEnabled ?: false + private var authCancellationSignal: CancellationSignal? = null + private var detectCancellationSignal: CancellationSignal? = null private var faceAcquiredInfoIgnoreList: Set + private var cancelNotReceivedHandlerJob: Job? = null + + private val _authenticationStatus: MutableStateFlow = + MutableStateFlow(null) + override val authenticationStatus: Flow + get() = _authenticationStatus.filterNotNull() + + private val _detectionStatus = MutableStateFlow(null) + override val detectionStatus: Flow + get() = _detectionStatus.filterNotNull() + + private val _isLockedOut = MutableStateFlow(false) + override val isLockedOut: Flow = _isLockedOut + + val isDetectionSupported = + faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false + + private val _isAuthRunning = MutableStateFlow(false) + override val isAuthRunning: Flow + get() = _isAuthRunning + + private val keyguardSessionId: InstanceId? + get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD) + + private val _canRunFaceAuth = MutableStateFlow(true) + override val canRunFaceAuth: StateFlow + get() = _canRunFaceAuth + + private val canRunDetection = MutableStateFlow(false) + + private val _isAuthenticated = MutableStateFlow(false) + override val isAuthenticated: Flow + get() = _isAuthenticated + + private val bypassEnabled: Flow = + keyguardBypassController?.let { + conflatedCallbackFlow { + val callback = + object : KeyguardBypassController.OnBypassStateChangedListener { + override fun onBypassStateChanged(isEnabled: Boolean) { + trySendWithFailureLogging(isEnabled, TAG, "BypassStateChanged") + } + } + it.registerOnBypassStateChangedListener(callback) + trySendWithFailureLogging(it.bypassEnabled, TAG, "BypassStateChanged") + awaitClose { it.unregisterOnBypassStateChangedListener(callback) } + } + } + ?: flowOf(false) + private val faceLockoutResetCallback = object : FaceManager.LockoutResetCallback() { override fun onLockoutReset(sensorId: Int) { @@ -133,12 +200,137 @@ constructor( .boxed() .collect(Collectors.toSet()) dumpManager.registerCriticalDumpable("KeyguardFaceAuthManagerImpl", this) + + observeFaceAuthGatingChecks() + observeFaceDetectGatingChecks() + observeFaceAuthResettingConditions() + } + + private fun observeFaceAuthResettingConditions() { + // Clear auth status when keyguard is going away or when the user is switching. + merge(keyguardRepository.isKeyguardGoingAway, userRepository.userSwitchingInProgress) + .onEach { goingAwayOrUserSwitchingInProgress -> + if (goingAwayOrUserSwitchingInProgress) { + _isAuthenticated.value = false + } + } + .launchIn(applicationScope) + } + + private fun observeFaceDetectGatingChecks() { + // Face detection can run only when lockscreen bypass is enabled + // & detection is supported & biometric unlock is not allowed. + listOf( + canFaceAuthOrDetectRun(), + logAndObserve(bypassEnabled, "bypassEnabled"), + logAndObserve( + biometricSettingsRepository.isNonStrongBiometricAllowed.isFalse(), + "nonStrongBiometricIsNotAllowed" + ), + // We don't want to run face detect if it's not possible to authenticate with FP + // from the bouncer. UDFPS is the only fp sensor type that won't support this. + logAndObserve( + and(isUdfps(), deviceEntryFingerprintAuthRepository.isRunning).isFalse(), + "udfpsAuthIsNotPossibleAnymore" + ) + ) + .reduce(::and) + .distinctUntilChanged() + .onEach { + faceAuthLogger.canRunDetectionChanged(it) + canRunDetection.value = it + if (!it) { + cancelDetection() + } + } + .launchIn(applicationScope) + } + + private fun isUdfps() = + deviceEntryFingerprintAuthRepository.availableFpSensorType.map { + it == BiometricType.UNDER_DISPLAY_FINGERPRINT + } + + private fun canFaceAuthOrDetectRun(): Flow { + return listOf( + logAndObserve(biometricSettingsRepository.isFaceEnrolled, "isFaceEnrolled"), + logAndObserve( + biometricSettingsRepository.isFaceAuthenticationEnabled, + "isFaceAuthenticationEnabled" + ), + logAndObserve( + userRepository.userSwitchingInProgress.isFalse(), + "userSwitchingNotInProgress" + ), + logAndObserve( + keyguardRepository.isKeyguardGoingAway.isFalse(), + "keyguardNotGoingAway" + ), + logAndObserve( + keyguardRepository.wakefulness + .map { WakefulnessModel.isSleepingOrStartingToSleep(it) } + .isFalse(), + "deviceNotSleepingOrNotStartingToSleep" + ), + logAndObserve( + combine( + keyguardInteractor.isSecureCameraActive, + alternateBouncerInteractor.isVisible, + ) { a, b -> + !a || b + }, + "secureCameraNotActiveOrAltBouncerIsShowing" + ), + logAndObserve( + biometricSettingsRepository.isFaceAuthSupportedInCurrentPosture, + "isFaceAuthSupportedInCurrentPosture" + ), + logAndObserve( + biometricSettingsRepository.isCurrentUserInLockdown.isFalse(), + "userHasNotLockedDownDevice" + ) + ) + .reduce(::and) + } + + private fun observeFaceAuthGatingChecks() { + // Face auth can run only if all of the gating conditions are true. + listOf( + canFaceAuthOrDetectRun(), + logAndObserve(isLockedOut.isFalse(), "isNotLocked"), + logAndObserve( + deviceEntryFingerprintAuthRepository.isLockedOut.isFalse(), + "fpLockedOut" + ), + logAndObserve(trustRepository.isCurrentUserTrusted.isFalse(), "currentUserTrusted"), + logAndObserve( + biometricSettingsRepository.isNonStrongBiometricAllowed, + "nonStrongBiometricIsAllowed" + ), + logAndObserve( + userRepository.selectedUserInfo.map { it.isPrimary }, + "userIsPrimaryUser" + ), + ) + .reduce(::and) + .distinctUntilChanged() + .onEach { + faceAuthLogger.canFaceAuthRunChanged(it) + _canRunFaceAuth.value = it + if (!it) { + // Cancel currently running auth if any of the gating checks are false. + faceAuthLogger.cancellingFaceAuth() + cancel() + } + } + .launchIn(applicationScope) } private val faceAuthCallback = object : FaceManager.AuthenticationCallback() { override fun onAuthenticationFailed() { _authenticationStatus.value = FailedAuthenticationStatus + _isAuthenticated.value = false faceAuthLogger.authenticationFailed() onFaceAuthRequestCompleted() } @@ -154,6 +346,7 @@ constructor( _isLockedOut.value = true } _authenticationStatus.value = errorStatus + _isAuthenticated.value = false if (errorStatus.isCancellationError()) { cancelNotReceivedHandlerJob?.cancel() applicationScope.launch { @@ -182,6 +375,7 @@ constructor( override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) { _authenticationStatus.value = SuccessAuthenticationStatus(result) + _isAuthenticated.value = true faceAuthLogger.faceAuthSuccess(result) onFaceAuthRequestCompleted() } @@ -190,7 +384,7 @@ constructor( private fun onFaceAuthRequestCompleted() { cancellationInProgress = false _isAuthRunning.value = false - cancellationSignal = null + authCancellationSignal = null } private val detectionCallback = @@ -202,7 +396,7 @@ constructor( private var cancellationInProgress = false private var faceAuthRequestedWhileCancellation: FaceAuthUiEvent? = null - override suspend fun authenticate(uiEvent: FaceAuthUiEvent) { + override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) { if (_isAuthRunning.value) { faceAuthLogger.ignoredFaceAuthTrigger(uiEvent) return @@ -219,44 +413,48 @@ constructor( faceAuthRequestedWhileCancellation = null } - withContext(mainDispatcher) { - // We always want to invoke face auth in the main thread. - cancellationSignal = CancellationSignal() - _isAuthRunning.value = true - uiEventsLogger.logWithInstanceIdAndPosition( - uiEvent, - 0, - null, - keyguardSessionId, - uiEvent.extraInfo - ) - faceAuthLogger.authenticating(uiEvent) - faceManager?.authenticate( - null, - cancellationSignal, - faceAuthCallback, - null, - FaceAuthenticateOptions.Builder().setUserId(currentUserId).build() - ) + if (canRunFaceAuth.value) { + withContext(mainDispatcher) { + // We always want to invoke face auth in the main thread. + authCancellationSignal = CancellationSignal() + _isAuthRunning.value = true + uiEventsLogger.logWithInstanceIdAndPosition( + uiEvent, + 0, + null, + keyguardSessionId, + uiEvent.extraInfo + ) + faceAuthLogger.authenticating(uiEvent) + faceManager?.authenticate( + null, + authCancellationSignal, + faceAuthCallback, + null, + FaceAuthenticateOptions.Builder().setUserId(currentUserId).build() + ) + } + } else if (fallbackToDetection && canRunDetection.value) { + detect() } } - override suspend fun detect() { + suspend fun detect() { if (!isDetectionSupported) { faceAuthLogger.detectionNotSupported(faceManager, faceManager?.sensorPropertiesInternal) return } - if (_isAuthRunning.value) { - faceAuthLogger.skippingBecauseAlreadyRunning("detection") + if (_isAuthRunning.value || detectCancellationSignal != null) { + faceAuthLogger.skippingDetection(_isAuthRunning.value, detectCancellationSignal != null) return } - cancellationSignal = CancellationSignal() + detectCancellationSignal = CancellationSignal() withContext(mainDispatcher) { // We always want to invoke face detect in the main thread. faceAuthLogger.faceDetectionStarted() faceManager?.detectFace( - cancellationSignal, + detectCancellationSignal, detectionCallback, FaceAuthenticateOptions.Builder().setUserId(currentUserId).build() ) @@ -266,10 +464,15 @@ constructor( private val currentUserId: Int get() = userRepository.getSelectedUserInfo().id - override fun cancel() { - if (cancellationSignal == null) return + fun cancelDetection() { + detectCancellationSignal?.cancel() + detectCancellationSignal = null + } - cancellationSignal?.cancel() + override fun cancel() { + if (authCancellationSignal == null) return + + authCancellationSignal?.cancel() cancelNotReceivedHandlerJob = applicationScope.launch { delay(DEFAULT_CANCEL_SIGNAL_TIMEOUT) @@ -285,29 +488,11 @@ constructor( _isAuthRunning.value = false } - private var cancelNotReceivedHandlerJob: Job? = null - - private val _authenticationStatus: MutableStateFlow = - MutableStateFlow(null) - override val authenticationStatus: Flow - get() = _authenticationStatus.filterNotNull() - - private val _detectionStatus = MutableStateFlow(null) - override val detectionStatus: Flow - get() = _detectionStatus.filterNotNull() - - private val _isLockedOut = MutableStateFlow(false) - override val isLockedOut: Flow = _isLockedOut - - override val isDetectionSupported = - faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false - - private val _isAuthRunning = MutableStateFlow(false) - override val isAuthRunning: Flow - get() = _isAuthRunning - - private val keyguardSessionId: InstanceId? - get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD) + private fun logAndObserve(cond: Flow, loggingContext: String): Flow { + return cond.distinctUntilChanged().onEach { + faceAuthLogger.observedConditionChanged(it, loggingContext) + } + } companion object { const val TAG = "KeyguardFaceAuthManager" @@ -335,12 +520,21 @@ constructor( pw.println( " faceAuthRequestedWhileCancellation: ${faceAuthRequestedWhileCancellation?.reason}" ) - pw.println(" cancellationSignal: $cancellationSignal") + pw.println(" authCancellationSignal: $authCancellationSignal") + pw.println(" detectCancellationSignal: $detectCancellationSignal") pw.println(" faceAcquiredInfoIgnoreList: $faceAcquiredInfoIgnoreList") pw.println(" _authenticationStatus: ${_authenticationStatus.value}") pw.println(" _detectionStatus: ${_detectionStatus.value}") pw.println(" currentUserId: $currentUserId") pw.println(" keyguardSessionId: $keyguardSessionId") - pw.println(" lockscreenBypassEnabled: $lockscreenBypassEnabled") + pw.println(" lockscreenBypassEnabled: ${keyguardBypassController?.bypassEnabled ?: false}") } } +/** Combine two boolean flows by and-ing both of them */ +private fun and(flow: Flow, anotherFlow: Flow) = + flow.combine(anotherFlow) { a, b -> a && b } + +/** "Not" the given flow. The return [Flow] will be true when [this] flow is false. */ +private fun Flow.isFalse(): Flow { + return this.map { !it } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt new file mode 100644 index 0000000000000..b426e54f0ac23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt @@ -0,0 +1,28 @@ +/* + * 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.systemui.keyguard.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface KeyguardFaceAuthModule { + @Binds fun faceAuthManager(impl: KeyguardFaceAuthManagerImpl): KeyguardFaceAuthManager + + @Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt index 647e3a15ba2f0..1c1755a6bd37c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt @@ -7,13 +7,13 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.dagger.FaceAuthLog import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel.DEBUG -import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject private const val TAG = "KeyguardFaceAuthManagerLog" /** - * Helper class for logging for [com.android.keyguard.faceauth.KeyguardFaceAuthManager] + * Helper class for logging for + * [com.android.systemui.keyguard.data.repository.KeyguardFaceAuthManager] * * To enable logcat echoing for an entire buffer: * ``` @@ -82,8 +82,19 @@ constructor( ) } - fun skippingBecauseAlreadyRunning(@CompileTimeConstant operation: String) { - logBuffer.log(TAG, DEBUG, "isAuthRunning is true, skipping $operation") + fun skippingDetection(isAuthRunning: Boolean, detectCancellationNotNull: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { + bool1 = isAuthRunning + bool2 = detectCancellationNotNull + }, + { + "Skipping running detection: isAuthRunning: $bool1, " + + "detectCancellationNotNull: $bool2" + } + ) } fun faceDetectionStarted() { @@ -177,4 +188,33 @@ constructor( { "Face authenticated successfully: userId: $int1, isStrongBiometric: $bool1" } ) } + + fun observedConditionChanged(newValue: Boolean, context: String) { + logBuffer.log( + TAG, + DEBUG, + { + bool1 = newValue + str1 = context + }, + { "Observed condition changed: $str1, new value: $bool1" } + ) + } + + fun canFaceAuthRunChanged(canRun: Boolean) { + logBuffer.log(TAG, DEBUG, { bool1 = canRun }, { "canFaceAuthRun value changed to $bool1" }) + } + + fun canRunDetectionChanged(canRunDetection: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { bool1 = canRunDetection }, + { "canRunDetection value changed to $bool1" } + ) + } + + fun cancellingFaceAuth() { + logBuffer.log(TAG, DEBUG, "cancelling face auth because a gating condition became false") + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index fb7d379c06270..5d83f561fdc26 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -157,11 +157,34 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(strongBiometricAllowed()).isFalse() } + @Test + fun convenienceBiometricAllowedChange() = + testScope.runTest { + createBiometricSettingsRepository() + val convenienceBiometricAllowed = + collectLastValue(underTest.isNonStrongBiometricAllowed) + runCurrent() + + onNonStrongAuthChanged(true, PRIMARY_USER_ID) + assertThat(convenienceBiometricAllowed()).isTrue() + + onNonStrongAuthChanged(false, ANOTHER_USER_ID) + assertThat(convenienceBiometricAllowed()).isTrue() + + onNonStrongAuthChanged(false, PRIMARY_USER_ID) + assertThat(convenienceBiometricAllowed()).isFalse() + } + private fun onStrongAuthChanged(flags: Int, userId: Int) { strongAuthTracker.value.stub.onStrongAuthRequiredChanged(flags, userId) testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper } + private fun onNonStrongAuthChanged(allowed: Boolean, userId: Int) { + strongAuthTracker.value.stub.onIsNonStrongBiometricAllowedChanged(allowed, userId) + testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + } + @Test fun fingerprintDisabledByDpmChange() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt index 70f766f719e95..e57b04495e40a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.data.repository +import android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT import android.hardware.biometrics.BiometricSourceType import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor @@ -30,7 +31,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -70,11 +70,6 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { ) } - @After - fun tearDown() { - // verify(keyguardUpdateMonitor).removeCallback(updateMonitorCallback.value) - } - @Test fun isLockedOut_whenFingerprintLockoutStateChanges_emitsNewValue() = testScope.runTest { @@ -129,29 +124,55 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { } @Test - fun enabledFingerprintTypeProvidesTheCorrectOutput() = + fun enabledFingerprintTypeProvidesTheCorrectOutputForSpfs() = testScope.runTest { whenever(authController.isSfpsSupported).thenReturn(true) whenever(authController.isUdfpsSupported).thenReturn(false) whenever(authController.isRearFpsSupported).thenReturn(false) - assertThat(underTest.availableFpSensorType).isEqualTo(BiometricType.SIDE_FINGERPRINT) + val availableFpSensorType = collectLastValue(underTest.availableFpSensorType) + assertThat(availableFpSensorType()).isEqualTo(BiometricType.SIDE_FINGERPRINT) + } + @Test + fun enabledFingerprintTypeProvidesTheCorrectOutputForUdfps() = + testScope.runTest { whenever(authController.isSfpsSupported).thenReturn(false) whenever(authController.isUdfpsSupported).thenReturn(true) whenever(authController.isRearFpsSupported).thenReturn(false) + val availableFpSensorType = collectLastValue(underTest.availableFpSensorType) + assertThat(availableFpSensorType()).isEqualTo(BiometricType.UNDER_DISPLAY_FINGERPRINT) + } - assertThat(underTest.availableFpSensorType) - .isEqualTo(BiometricType.UNDER_DISPLAY_FINGERPRINT) - + @Test + fun enabledFingerprintTypeProvidesTheCorrectOutputForRearFps() = + testScope.runTest { whenever(authController.isSfpsSupported).thenReturn(false) whenever(authController.isUdfpsSupported).thenReturn(false) whenever(authController.isRearFpsSupported).thenReturn(true) - assertThat(underTest.availableFpSensorType).isEqualTo(BiometricType.REAR_FINGERPRINT) + val availableFpSensorType = collectLastValue(underTest.availableFpSensorType) + assertThat(availableFpSensorType()).isEqualTo(BiometricType.REAR_FINGERPRINT) + } + + @Test + fun enabledFingerprintTypeProvidesTheCorrectOutputAfterAllAuthenticatorsAreRegistered() = + testScope.runTest { + whenever(authController.isSfpsSupported).thenReturn(false) + whenever(authController.isUdfpsSupported).thenReturn(false) whenever(authController.isRearFpsSupported).thenReturn(false) + whenever(authController.areAllFingerprintAuthenticatorsRegistered()).thenReturn(false) - assertThat(underTest.availableFpSensorType).isNull() + val availableFpSensorType = collectLastValue(underTest.availableFpSensorType) + runCurrent() + + val callback = ArgumentCaptor.forClass(AuthController.Callback::class.java) + verify(authController).addCallback(callback.capture()) + assertThat(availableFpSensorType()).isNull() + + whenever(authController.isUdfpsSupported).thenReturn(true) + callback.value.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT) + assertThat(availableFpSensorType()).isEqualTo(BiometricType.UNDER_DISPLAY_FINGERPRINT) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt index d55370b20d093..8349b0b2d9727 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt @@ -16,8 +16,10 @@ package com.android.systemui.keyguard.data.repository +import android.app.StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP import android.app.StatusBarManager.SESSION_KEYGUARD import android.content.pm.UserInfo +import android.content.pm.UserInfo.FLAG_PRIMARY import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_CANCELED import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT import android.hardware.biometrics.ComponentInfoInternal @@ -38,24 +40,37 @@ import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager import com.android.systemui.dump.logcatLogBuffer +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.AuthenticationStatus import com.android.systemui.keyguard.shared.model.DetectionStatus import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.log.SessionTracker +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.phone.FakeKeyguardStateController import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.SystemClock import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -68,6 +83,7 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.isNull +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -87,9 +103,14 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { @Captor private lateinit var authenticationCallback: ArgumentCaptor + @Captor private lateinit var detectionCallback: ArgumentCaptor @Captor private lateinit var cancellationSignal: ArgumentCaptor + + private lateinit var bypassStateChangedListener: + KotlinArgumentCaptor + @Captor private lateinit var faceLockoutResetCallback: ArgumentCaptor private lateinit var testDispatcher: TestDispatcher @@ -100,17 +121,60 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { private lateinit var detectStatus: FlowValue private lateinit var authRunning: FlowValue private lateinit var lockedOut: FlowValue + private lateinit var canFaceAuthRun: FlowValue + private lateinit var authenticated: FlowValue + private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository + private lateinit var deviceEntryFingerprintAuthRepository: + FakeDeviceEntryFingerprintAuthRepository + private lateinit var trustRepository: FakeTrustRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var keyguardInteractor: KeyguardInteractor + private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor + private lateinit var bouncerRepository: FakeKeyguardBouncerRepository + private lateinit var fakeCommandQueue: FakeCommandQueue + private lateinit var featureFlags: FakeFeatureFlags + + private var wasAuthCancelled = false + private var wasDetectCancelled = false @Before fun setup() { MockitoAnnotations.initMocks(this) fakeUserRepository = FakeUserRepository() - fakeUserRepository.setUserInfos(listOf(currentUser)) + fakeUserRepository.setUserInfos(listOf(primaryUser, secondaryUser)) testDispatcher = StandardTestDispatcher() + biometricSettingsRepository = FakeBiometricSettingsRepository() + deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() + trustRepository = FakeTrustRepository() + keyguardRepository = FakeKeyguardRepository() + bouncerRepository = FakeKeyguardBouncerRepository() + featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) } + fakeCommandQueue = FakeCommandQueue() + keyguardInteractor = + KeyguardInteractor( + keyguardRepository, + fakeCommandQueue, + featureFlags, + bouncerRepository + ) + alternateBouncerInteractor = + AlternateBouncerInteractor( + bouncerRepository = bouncerRepository, + biometricSettingsRepository = biometricSettingsRepository, + deviceEntryFingerprintAuthRepository = deviceEntryFingerprintAuthRepository, + systemClock = mock(SystemClock::class.java), + keyguardStateController = FakeKeyguardStateController(), + statusBarStateController = mock(StatusBarStateController::class.java), + ) + + bypassStateChangedListener = + KotlinArgumentCaptor(KeyguardBypassController.OnBypassStateChangedListener::class.java) testScope = TestScope(testDispatcher) whenever(sessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(keyguardSessionId) + whenever(faceManager.sensorPropertiesInternal) + .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true))) whenever(bypassController.bypassEnabled).thenReturn(true) - underTest = createFaceAuthManagerImpl(faceManager) + underTest = createFaceAuthManagerImpl(faceManager, bypassController) } private fun createFaceAuthManagerImpl( @@ -127,13 +191,20 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { sessionTracker, uiEventLogger, FaceAuthenticationLogger(logcatLogBuffer("KeyguardFaceAuthManagerLog")), + biometricSettingsRepository, + deviceEntryFingerprintAuthRepository, + trustRepository, + keyguardRepository, + keyguardInteractor, + alternateBouncerInteractor, dumpManager, ) @Test fun faceAuthRunsAndProvidesAuthStatusUpdates() = testScope.runTest { - testSetup(this) + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER.extraInfo = 10 underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) @@ -146,7 +217,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { authenticationCallback.value.onAuthenticationSucceeded(successResult) assertThat(authStatus()).isEqualTo(SuccessAuthenticationStatus(successResult)) - + assertThat(authenticated()).isTrue() assertThat(authRunning()).isFalse() } @@ -164,7 +235,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { @Test fun faceAuthDoesNotRunWhileItIsAlreadyRunning() = testScope.runTest { - testSetup(this) + initCollectors() underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) faceAuthenticateIsCalled() @@ -179,7 +250,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { @Test fun faceLockoutStatusIsPropagated() = testScope.runTest { - testSetup(this) + initCollectors() verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture()) underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) @@ -233,7 +304,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { @Test fun cancelStopsFaceAuthentication() = testScope.runTest { - testSetup(this) + initCollectors() underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) faceAuthenticateIsCalled() @@ -255,7 +326,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { whenever(faceManager.sensorPropertiesInternal) .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true))) underTest = createFaceAuthManagerImpl() - testSetup(this) + initCollectors() underTest.detect() faceDetectIsCalled() @@ -271,7 +342,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { whenever(faceManager.sensorPropertiesInternal) .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = false))) underTest = createFaceAuthManagerImpl() - testSetup(this) + initCollectors() clearInvocations(faceManager) underTest.detect() @@ -282,7 +353,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { @Test fun faceAuthShouldWaitAndRunIfTriggeredWhileCancelling() = testScope.runTest { - testSetup(this) + initCollectors() underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) faceAuthenticateIsCalled() @@ -316,7 +387,8 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { @Test fun faceAuthAutoCancelsAfterDefaultCancellationTimeout() = testScope.runTest { - testSetup(this) + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) faceAuthenticateIsCalled() @@ -337,7 +409,7 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { intArrayOf(10, 11) ) underTest = createFaceAuthManagerImpl() - testSetup(this) + initCollectors() underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) faceAuthenticateIsCalled() @@ -352,34 +424,430 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { @Test fun dumpDoesNotErrorOutWhenFaceManagerOrBypassControllerIsNull() = testScope.runTest { - fakeUserRepository.setSelectedUserInfo(currentUser) + fakeUserRepository.setSelectedUserInfo(primaryUser) underTest.dump(PrintWriter(StringWriter()), emptyArray()) underTest = createFaceAuthManagerImpl(fmOverride = null, bypassControllerOverride = null) - fakeUserRepository.setSelectedUserInfo(currentUser) + fakeUserRepository.setSelectedUserInfo(primaryUser) underTest.dump(PrintWriter(StringWriter()), emptyArray()) } - private suspend fun testSetup(testScope: TestScope) { - with(testScope) { - authStatus = collectLastValue(underTest.authenticationStatus) - detectStatus = collectLastValue(underTest.detectionStatus) - authRunning = collectLastValue(underTest.isAuthRunning) - lockedOut = collectLastValue(underTest.isLockedOut) - fakeUserRepository.setSelectedUserInfo(currentUser) + @Test + fun authenticateDoesNotRunIfFaceIsNotEnrolled() = + testScope.runTest { + testGatingCheckForFaceAuth { biometricSettingsRepository.setFaceEnrolled(false) } } + + @Test + fun authenticateDoesNotRunIfFaceIsNotEnabled() = + testScope.runTest { + testGatingCheckForFaceAuth { biometricSettingsRepository.setIsFaceAuthEnabled(false) } + } + + @Test + fun authenticateDoesNotRunIfUserIsInLockdown() = + testScope.runTest { + testGatingCheckForFaceAuth { biometricSettingsRepository.setIsUserInLockdown(true) } + } + + @Test + fun authenticateDoesNotRunIfUserIsCurrentlySwitching() = + testScope.runTest { + testGatingCheckForFaceAuth { fakeUserRepository.setUserSwitching(true) } + } + + @Test + fun authenticateDoesNotRunWhenFpIsLockedOut() = + testScope.runTest { + testGatingCheckForFaceAuth { deviceEntryFingerprintAuthRepository.setLockedOut(true) } + } + + @Test + fun authenticateDoesNotRunWhenUserIsCurrentlyTrusted() = + testScope.runTest { + testGatingCheckForFaceAuth { trustRepository.setCurrentUserTrusted(true) } + } + + @Test + fun authenticateDoesNotRunWhenKeyguardIsGoingAway() = + testScope.runTest { + testGatingCheckForFaceAuth { keyguardRepository.setKeyguardGoingAway(true) } + } + + @Test + fun authenticateDoesNotRunWhenDeviceIsGoingToSleep() = + testScope.runTest { + testGatingCheckForFaceAuth { + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + state = WakefulnessState.STARTING_TO_SLEEP, + isWakingUpOrAwake = false, + lastWakeReason = WakeSleepReason.OTHER, + lastSleepReason = WakeSleepReason.OTHER, + ) + ) + } + } + + @Test + fun authenticateDoesNotRunWhenDeviceIsSleeping() = + testScope.runTest { + testGatingCheckForFaceAuth { + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + state = WakefulnessState.ASLEEP, + isWakingUpOrAwake = false, + lastWakeReason = WakeSleepReason.OTHER, + lastSleepReason = WakeSleepReason.OTHER, + ) + ) + } + } + + @Test + fun authenticateDoesNotRunWhenNonStrongBiometricIsNotAllowed() = + testScope.runTest { + testGatingCheckForFaceAuth { + biometricSettingsRepository.setIsNonStrongBiometricAllowed(false) + } + } + + @Test + fun authenticateDoesNotRunWhenCurrentUserIsNotPrimary() = + testScope.runTest { + testGatingCheckForFaceAuth { + launch { fakeUserRepository.setSelectedUserInfo(secondaryUser) } + } + } + + @Test + fun authenticateDoesNotRunWhenSecureCameraIsActive() = + testScope.runTest { + testGatingCheckForFaceAuth { + bouncerRepository.setAlternateVisible(false) + fakeCommandQueue.doForEachCallback { + it.onCameraLaunchGestureDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) + } + } + } + + @Test + fun authenticateDoesNotRunOnUnsupportedPosture() = + testScope.runTest { + testGatingCheckForFaceAuth { + biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(false) + } + } + + @Test + fun authenticateFallbacksToDetectionWhenItCannotRun() = + testScope.runTest { + whenever(faceManager.sensorPropertiesInternal) + .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true))) + whenever(bypassController.bypassEnabled).thenReturn(true) + underTest = createFaceAuthManagerImpl() + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() + + // Flip one precondition to false. + biometricSettingsRepository.setIsNonStrongBiometricAllowed(false) + assertThat(canFaceAuthRun()).isFalse() + underTest.authenticate( + FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, + fallbackToDetection = true + ) + faceAuthenticateIsNotCalled() + + faceDetectIsCalled() + } + + @Test + fun everythingWorksWithFaceAuthRefactorFlagDisabled() = + testScope.runTest { + featureFlags.set(FACE_AUTH_REFACTOR, false) + + underTest = createFaceAuthManagerImpl() + initCollectors() + + // Collecting any flows exposed in the public API doesn't throw any error + authStatus() + detectStatus() + authRunning() + lockedOut() + canFaceAuthRun() + authenticated() + } + + @Test + fun isAuthenticatedIsFalseWhenFaceAuthFails() = + testScope.runTest { + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() + + triggerFaceAuth(false) + + authenticationCallback.value.onAuthenticationFailed() + + assertThat(authenticated()).isFalse() + } + + @Test + fun isAuthenticatedIsFalseWhenFaceAuthErrorsOut() = + testScope.runTest { + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() + + triggerFaceAuth(false) + + authenticationCallback.value.onAuthenticationError(-1, "some error") + + assertThat(authenticated()).isFalse() + } + + @Test + fun isAuthenticatedIsResetToFalseWhenKeyguardIsGoingAway() = + testScope.runTest { + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() + + triggerFaceAuth(false) + + authenticationCallback.value.onAuthenticationSucceeded( + mock(FaceManager.AuthenticationResult::class.java) + ) + + assertThat(authenticated()).isTrue() + + keyguardRepository.setKeyguardGoingAway(true) + + assertThat(authenticated()).isFalse() + } + + @Test + fun isAuthenticatedIsResetToFalseWhenUserIsSwitching() = + testScope.runTest { + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() + + triggerFaceAuth(false) + + authenticationCallback.value.onAuthenticationSucceeded( + mock(FaceManager.AuthenticationResult::class.java) + ) + + assertThat(authenticated()).isTrue() + + fakeUserRepository.setUserSwitching(true) + + assertThat(authenticated()).isFalse() + } + + @Test + fun detectDoesNotRunWhenFaceIsNotEnrolled() = + testScope.runTest { + testGatingCheckForDetect { biometricSettingsRepository.setFaceEnrolled(false) } + } + + @Test + fun detectDoesNotRunWhenFaceIsNotEnabled() = + testScope.runTest { + testGatingCheckForDetect { biometricSettingsRepository.setIsFaceAuthEnabled(false) } + } + + @Test + fun detectDoesNotRunWhenUserSwitchingInProgress() = + testScope.runTest { testGatingCheckForDetect { fakeUserRepository.setUserSwitching(true) } } + + @Test + fun detectDoesNotRunWhenKeyguardGoingAway() = + testScope.runTest { + testGatingCheckForDetect { keyguardRepository.setKeyguardGoingAway(true) } + } + + @Test + fun detectDoesNotRunWhenDeviceSleepingStartingToSleep() = + testScope.runTest { + testGatingCheckForDetect { + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + state = WakefulnessState.STARTING_TO_SLEEP, + isWakingUpOrAwake = false, + lastWakeReason = WakeSleepReason.OTHER, + lastSleepReason = WakeSleepReason.OTHER, + ) + ) + } + } + + @Test + fun detectDoesNotRunWhenSecureCameraIsActive() = + testScope.runTest { + testGatingCheckForDetect { + bouncerRepository.setAlternateVisible(false) + fakeCommandQueue.doForEachCallback { + it.onCameraLaunchGestureDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) + } + } + } + + @Test + fun detectDoesNotRunWhenFaceAuthNotSupportedInCurrentPosture() = + testScope.runTest { + testGatingCheckForDetect { + biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(false) + } + } + + @Test + fun detectDoesNotRunWhenCurrentUserInLockdown() = + testScope.runTest { + testGatingCheckForDetect { biometricSettingsRepository.setIsUserInLockdown(true) } + } + + @Test + fun detectDoesNotRunWhenBypassIsNotEnabled() = + testScope.runTest { + runCurrent() + verify(bypassController) + .registerOnBypassStateChangedListener(bypassStateChangedListener.capture()) + + testGatingCheckForDetect { + bypassStateChangedListener.value.onBypassStateChanged(false) + } + } + + @Test + fun detectDoesNotRunWhenNonStrongBiometricIsAllowed() = + testScope.runTest { + testGatingCheckForDetect { + biometricSettingsRepository.setIsNonStrongBiometricAllowed(true) + } + } + + @Test + fun detectDoesNotRunIfUdfpsIsRunning() = + testScope.runTest { + testGatingCheckForDetect { + deviceEntryFingerprintAuthRepository.setAvailableFpSensorType( + BiometricType.UNDER_DISPLAY_FINGERPRINT + ) + deviceEntryFingerprintAuthRepository.setIsRunning(true) + } + } + + private suspend fun TestScope.testGatingCheckForFaceAuth(gatingCheckModifier: () -> Unit) { + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() + + gatingCheckModifier() + runCurrent() + + // gating check doesn't allow face auth to run. + assertThat(underTest.canRunFaceAuth.value).isFalse() + + // flip the gating check back on. + allPreconditionsToRunFaceAuthAreTrue() + + triggerFaceAuth(false) + + // Flip gating check off + gatingCheckModifier() + runCurrent() + + // Stops currently running auth + assertThat(wasAuthCancelled).isTrue() + clearInvocations(faceManager) + + // Try auth again + underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) + + // Auth can't run again + faceAuthenticateIsNotCalled() } - private fun successResult() = FaceManager.AuthenticationResult(null, null, currentUserId, false) + private suspend fun TestScope.testGatingCheckForDetect(gatingCheckModifier: () -> Unit) { + initCollectors() + allPreconditionsToRunFaceAuthAreTrue() + + // This will stop face auth from running but is required to be false for detect. + biometricSettingsRepository.setIsNonStrongBiometricAllowed(false) + runCurrent() + + assertThat(canFaceAuthRun()).isFalse() + + // Trigger authenticate with detection fallback + underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, fallbackToDetection = true) + + faceAuthenticateIsNotCalled() + faceDetectIsCalled() + cancellationSignal.value.setOnCancelListener { wasDetectCancelled = true } + + // Flip gating check + gatingCheckModifier() + runCurrent() + + // Stops currently running detect + assertThat(wasDetectCancelled).isTrue() + clearInvocations(faceManager) + + // Try to run detect again + underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, fallbackToDetection = true) + + // Detect won't run because preconditions are not true anymore. + faceDetectIsNotCalled() + } + + private suspend fun triggerFaceAuth(fallbackToDetect: Boolean) { + assertThat(canFaceAuthRun()).isTrue() + underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, fallbackToDetect) + faceAuthenticateIsCalled() + assertThat(authRunning()).isTrue() + cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true } + } + + private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() { + biometricSettingsRepository.setFaceEnrolled(true) + biometricSettingsRepository.setIsFaceAuthEnabled(true) + fakeUserRepository.setUserSwitching(false) + deviceEntryFingerprintAuthRepository.setLockedOut(false) + trustRepository.setCurrentUserTrusted(false) + keyguardRepository.setKeyguardGoingAway(false) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.STARTING_TO_WAKE, + true, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + biometricSettingsRepository.setIsNonStrongBiometricAllowed(true) + biometricSettingsRepository.setIsUserInLockdown(false) + fakeUserRepository.setSelectedUserInfo(primaryUser) + biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true) + bouncerRepository.setAlternateVisible(true) + runCurrent() + } + + private suspend fun TestScope.initCollectors() { + authStatus = collectLastValue(underTest.authenticationStatus) + detectStatus = collectLastValue(underTest.detectionStatus) + authRunning = collectLastValue(underTest.isAuthRunning) + lockedOut = collectLastValue(underTest.isLockedOut) + canFaceAuthRun = collectLastValue(underTest.canRunFaceAuth) + authenticated = collectLastValue(underTest.isAuthenticated) + fakeUserRepository.setSelectedUserInfo(primaryUser) + } + + private fun successResult() = FaceManager.AuthenticationResult(null, null, primaryUserId, false) private fun faceDetectIsCalled() { verify(faceManager) .detectFace( cancellationSignal.capture(), detectionCallback.capture(), - eq(FaceAuthenticateOptions.Builder().setUserId(currentUserId).build()) + eq(FaceAuthenticateOptions.Builder().setUserId(primaryUserId).build()) ) } @@ -390,10 +858,26 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { cancellationSignal.capture(), authenticationCallback.capture(), isNull(), - eq(FaceAuthenticateOptions.Builder().setUserId(currentUserId).build()) + eq(FaceAuthenticateOptions.Builder().setUserId(primaryUserId).build()) ) } + private fun faceAuthenticateIsNotCalled() { + verify(faceManager, never()) + .authenticate( + isNull(), + any(), + any(), + isNull(), + any(FaceAuthenticateOptions::class.java) + ) + } + + private fun faceDetectIsNotCalled() { + verify(faceManager, never()) + .detectFace(any(), any(), any(FaceAuthenticateOptions::class.java)) + } + private fun createFaceSensorProperties( supportsFaceDetection: Boolean ): FaceSensorPropertiesInternal { @@ -420,8 +904,10 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() { } companion object { - const val currentUserId = 1 + const val primaryUserId = 1 val keyguardSessionId = fakeInstanceId(10)!! - val currentUser = UserInfo(currentUserId, "test user", 0) + val primaryUser = UserInfo(primaryUserId, "test user", FLAG_PRIMARY) + + val secondaryUser = UserInfo(2, "secondary user", 0) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index 7f3016270def9..68d694aaf20f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -18,18 +18,16 @@ package com.android.systemui.keyguard.domain.interactor import android.app.StatusBarManager -import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR +import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel -import com.android.systemui.settings.DisplayTracker -import com.android.systemui.statusbar.CommandQueue import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.test.TestScope @@ -38,7 +36,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations @SmallTest @@ -56,7 +53,7 @@ class KeyguardInteractorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) } - commandQueue = FakeCommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java)) + commandQueue = FakeCommandQueue() testScope = TestScope() repository = FakeKeyguardRepository() bouncerRepository = FakeKeyguardBouncerRepository() @@ -174,22 +171,3 @@ class KeyguardInteractorTest : SysuiTestCase() { assertThat(secureCameraActive()).isFalse() } } - -class FakeCommandQueue(val context: Context, val displayTracker: DisplayTracker) : - CommandQueue(context, displayTracker) { - private val callbacks = mutableListOf() - - override fun addCallback(callback: Callbacks) { - callbacks.add(callback) - } - - override fun removeCallback(callback: Callbacks) { - callbacks.remove(callback) - } - - fun doForEachCallback(func: (callback: Callbacks) -> Unit) { - callbacks.forEach { func(it) } - } - - fun callbackCount(): Int = callbacks.size -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt index d8b3270d3aff4..65735f028c41c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.flowOf class FakeBiometricSettingsRepository : BiometricSettingsRepository { @@ -39,12 +38,17 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { private val _isStrongBiometricAllowed = MutableStateFlow(false) override val isStrongBiometricAllowed = _isStrongBiometricAllowed.asStateFlow() + private val _isNonStrongBiometricAllowed = MutableStateFlow(false) + override val isNonStrongBiometricAllowed: StateFlow + get() = _isNonStrongBiometricAllowed + private val _isFingerprintEnabledByDevicePolicy = MutableStateFlow(false) override val isFingerprintEnabledByDevicePolicy = _isFingerprintEnabledByDevicePolicy.asStateFlow() + private val _isFaceAuthSupportedInCurrentPosture = MutableStateFlow(false) override val isFaceAuthSupportedInCurrentPosture: Flow - get() = flowOf(true) + get() = _isFaceAuthSupportedInCurrentPosture private val _isCurrentUserInLockdown = MutableStateFlow(false) override val isCurrentUserInLockdown: Flow @@ -66,7 +70,19 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { _isFaceEnrolled.value = isFaceEnrolled } + fun setIsFaceAuthSupportedInCurrentPosture(value: Boolean) { + _isFaceAuthSupportedInCurrentPosture.value = value + } + fun setIsFaceAuthEnabled(enabled: Boolean) { _isFaceAuthEnabled.value = enabled } + + fun setIsUserInLockdown(value: Boolean) { + _isCurrentUserInLockdown.value = value + } + + fun setIsNonStrongBiometricAllowed(value: Boolean) { + _isNonStrongBiometricAllowed.value = value + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt new file mode 100644 index 0000000000000..fe941179830ae --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 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.systemui.keyguard.data.repository + +import android.content.Context +import com.android.systemui.settings.DisplayTracker +import com.android.systemui.statusbar.CommandQueue +import org.mockito.Mockito.mock + +class FakeCommandQueue : CommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java)) { + private val callbacks = mutableListOf() + + override fun addCallback(callback: Callbacks) { + callbacks.add(callback) + } + + override fun removeCallback(callback: Callbacks) { + callbacks.remove(callback) + } + + fun doForEachCallback(func: (callback: Callbacks) -> Unit) { + callbacks.forEach { func(it) } + } + + fun callbackCount(): Int = callbacks.size +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt index 00b1a401ac797..4bfd3d64c98e2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt @@ -30,10 +30,19 @@ class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepos override val isRunning: Flow get() = _isRunning - override val availableFpSensorType: BiometricType? - get() = null + private var fpSensorType = MutableStateFlow(null) + override val availableFpSensorType: Flow + get() = fpSensorType fun setLockedOut(lockedOut: Boolean) { _isLockedOut.value = lockedOut } + + fun setIsRunning(value: Boolean) { + _isRunning.value = value + } + + fun setAvailableFpSensorType(value: BiometricType?) { + fpSensorType.value = value + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 194ed02712b20..d4115900850fe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -129,6 +129,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isKeyguardShowing.value = isShowing } + fun setKeyguardGoingAway(isGoingAway: Boolean) { + _isKeyguardGoingAway.value = isGoingAway + } + fun setKeyguardOccluded(isOccluded: Boolean) { _isKeyguardOccluded.value = isOccluded } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt new file mode 100644 index 0000000000000..6690de87d0a29 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 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.systemui.keyguard.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeTrustRepository : TrustRepository { + private val _isCurrentUserTrusted = MutableStateFlow(false) + override val isCurrentUserTrusted: Flow + get() = _isCurrentUserTrusted + + fun setCurrentUserTrusted(trust: Boolean) { + _isCurrentUserTrusted.value = trust + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 53bb340fc167f..fbc2381c30311 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -100,4 +100,8 @@ class FakeUserRepository : UserRepository { fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } + + fun setUserSwitching(value: Boolean) { + _userSwitchingInProgress.value = value + } }