Merge "Make KeyguardFaceAuthManager check all the gating conditions before running face auth" into udc-dev
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -87,6 +87,13 @@ interface BiometricSettingsRepository {
|
||||
*/
|
||||
val isStrongBiometricAllowed: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* 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<Boolean>
|
||||
|
||||
/** Whether fingerprint feature is enabled for the current user by the DevicePolicy */
|
||||
val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean>
|
||||
|
||||
@@ -276,6 +283,16 @@ constructor(
|
||||
)
|
||||
)
|
||||
|
||||
override val isNonStrongBiometricAllowed: StateFlow<Boolean> =
|
||||
strongAuthTracker.isNonStrongBiometricAllowed.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
strongAuthTracker.isBiometricAllowedForUser(
|
||||
false,
|
||||
userRepository.getSelectedUserInfo().id
|
||||
)
|
||||
)
|
||||
|
||||
override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> =
|
||||
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<StrongAuthenticationFlags> =
|
||||
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<Boolean> =
|
||||
currentUserAuthFlags.map { isBiometricAllowedForUser(true, it.userId) }
|
||||
|
||||
/** isNonStrongBiometricAllowed for the current user. */
|
||||
val isNonStrongBiometricAllowed: Flow<Boolean> =
|
||||
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 =
|
||||
|
||||
@@ -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<BiometricType?>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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<BiometricType?>
|
||||
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<Boolean> =
|
||||
conflatedCallbackFlow {
|
||||
|
||||
@@ -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<Boolean>
|
||||
|
||||
/**
|
||||
* 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<Boolean>
|
||||
|
||||
/** Provide the current status of face authentication. */
|
||||
val authenticationStatus: Flow<AuthenticationStatus>
|
||||
@@ -91,8 +92,19 @@ interface KeyguardFaceAuthManager {
|
||||
/** Current state of whether face authentication is running. */
|
||||
val isAuthRunning: Flow<Boolean>
|
||||
|
||||
/** 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<Int>
|
||||
|
||||
private var cancelNotReceivedHandlerJob: Job? = null
|
||||
|
||||
private val _authenticationStatus: MutableStateFlow<AuthenticationStatus?> =
|
||||
MutableStateFlow(null)
|
||||
override val authenticationStatus: Flow<AuthenticationStatus>
|
||||
get() = _authenticationStatus.filterNotNull()
|
||||
|
||||
private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null)
|
||||
override val detectionStatus: Flow<DetectionStatus>
|
||||
get() = _detectionStatus.filterNotNull()
|
||||
|
||||
private val _isLockedOut = MutableStateFlow(false)
|
||||
override val isLockedOut: Flow<Boolean> = _isLockedOut
|
||||
|
||||
val isDetectionSupported =
|
||||
faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false
|
||||
|
||||
private val _isAuthRunning = MutableStateFlow(false)
|
||||
override val isAuthRunning: Flow<Boolean>
|
||||
get() = _isAuthRunning
|
||||
|
||||
private val keyguardSessionId: InstanceId?
|
||||
get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD)
|
||||
|
||||
private val _canRunFaceAuth = MutableStateFlow(true)
|
||||
override val canRunFaceAuth: StateFlow<Boolean>
|
||||
get() = _canRunFaceAuth
|
||||
|
||||
private val canRunDetection = MutableStateFlow(false)
|
||||
|
||||
private val _isAuthenticated = MutableStateFlow(false)
|
||||
override val isAuthenticated: Flow<Boolean>
|
||||
get() = _isAuthenticated
|
||||
|
||||
private val bypassEnabled: Flow<Boolean> =
|
||||
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<Boolean> {
|
||||
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<AuthenticationStatus?> =
|
||||
MutableStateFlow(null)
|
||||
override val authenticationStatus: Flow<AuthenticationStatus>
|
||||
get() = _authenticationStatus.filterNotNull()
|
||||
|
||||
private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null)
|
||||
override val detectionStatus: Flow<DetectionStatus>
|
||||
get() = _detectionStatus.filterNotNull()
|
||||
|
||||
private val _isLockedOut = MutableStateFlow(false)
|
||||
override val isLockedOut: Flow<Boolean> = _isLockedOut
|
||||
|
||||
override val isDetectionSupported =
|
||||
faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false
|
||||
|
||||
private val _isAuthRunning = MutableStateFlow(false)
|
||||
override val isAuthRunning: Flow<Boolean>
|
||||
get() = _isAuthRunning
|
||||
|
||||
private val keyguardSessionId: InstanceId?
|
||||
get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD)
|
||||
private fun logAndObserve(cond: Flow<Boolean>, loggingContext: String): Flow<Boolean> {
|
||||
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<Boolean>, anotherFlow: Flow<Boolean>) =
|
||||
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<Boolean>.isFalse(): Flow<Boolean> {
|
||||
return this.map { !it }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FaceManager.AuthenticationCallback>
|
||||
|
||||
@Captor
|
||||
private lateinit var detectionCallback: ArgumentCaptor<FaceManager.FaceDetectionCallback>
|
||||
@Captor private lateinit var cancellationSignal: ArgumentCaptor<CancellationSignal>
|
||||
|
||||
private lateinit var bypassStateChangedListener:
|
||||
KotlinArgumentCaptor<KeyguardBypassController.OnBypassStateChangedListener>
|
||||
|
||||
@Captor
|
||||
private lateinit var faceLockoutResetCallback: ArgumentCaptor<FaceManager.LockoutResetCallback>
|
||||
private lateinit var testDispatcher: TestDispatcher
|
||||
@@ -100,17 +121,60 @@ class KeyguardFaceAuthManagerTest : SysuiTestCase() {
|
||||
private lateinit var detectStatus: FlowValue<DetectionStatus?>
|
||||
private lateinit var authRunning: FlowValue<Boolean?>
|
||||
private lateinit var lockedOut: FlowValue<Boolean?>
|
||||
private lateinit var canFaceAuthRun: FlowValue<Boolean?>
|
||||
private lateinit var authenticated: FlowValue<Boolean?>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Callbacks>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<Boolean>
|
||||
get() = _isNonStrongBiometricAllowed
|
||||
|
||||
private val _isFingerprintEnabledByDevicePolicy = MutableStateFlow(false)
|
||||
override val isFingerprintEnabledByDevicePolicy =
|
||||
_isFingerprintEnabledByDevicePolicy.asStateFlow()
|
||||
|
||||
private val _isFaceAuthSupportedInCurrentPosture = MutableStateFlow(false)
|
||||
override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
|
||||
get() = flowOf(true)
|
||||
get() = _isFaceAuthSupportedInCurrentPosture
|
||||
|
||||
private val _isCurrentUserInLockdown = MutableStateFlow(false)
|
||||
override val isCurrentUserInLockdown: Flow<Boolean>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Callbacks>()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -30,10 +30,19 @@ class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepos
|
||||
override val isRunning: Flow<Boolean>
|
||||
get() = _isRunning
|
||||
|
||||
override val availableFpSensorType: BiometricType?
|
||||
get() = null
|
||||
private var fpSensorType = MutableStateFlow<BiometricType?>(null)
|
||||
override val availableFpSensorType: Flow<BiometricType?>
|
||||
get() = fpSensorType
|
||||
|
||||
fun setLockedOut(lockedOut: Boolean) {
|
||||
_isLockedOut.value = lockedOut
|
||||
}
|
||||
|
||||
fun setIsRunning(value: Boolean) {
|
||||
_isRunning.value = value
|
||||
}
|
||||
|
||||
fun setAvailableFpSensorType(value: BiometricType?) {
|
||||
fpSensorType.value = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,10 @@ class FakeKeyguardRepository : KeyguardRepository {
|
||||
_isKeyguardShowing.value = isShowing
|
||||
}
|
||||
|
||||
fun setKeyguardGoingAway(isGoingAway: Boolean) {
|
||||
_isKeyguardGoingAway.value = isGoingAway
|
||||
}
|
||||
|
||||
fun setKeyguardOccluded(isOccluded: Boolean) {
|
||||
_isKeyguardOccluded.value = isOccluded
|
||||
}
|
||||
|
||||
@@ -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<Boolean>
|
||||
get() = _isCurrentUserTrusted
|
||||
|
||||
fun setCurrentUserTrusted(trust: Boolean) {
|
||||
_isCurrentUserTrusted.value = trust
|
||||
}
|
||||
}
|
||||
@@ -100,4 +100,8 @@ class FakeUserRepository : UserRepository {
|
||||
fun setGuestUserAutoCreated(value: Boolean) {
|
||||
_isGuestUserAutoCreated = value
|
||||
}
|
||||
|
||||
fun setUserSwitching(value: Boolean) {
|
||||
_userSwitchingInProgress.value = value
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user