Make KeyguardFaceAuthManager check all the gating conditions before running face auth

Bug: 262838215
Test: atest KeyguardFaceAuthManagerTest
Change-Id: Ic3541defddddde063528a4db8a1865a5991f6623
This commit is contained in:
Chandru S
2022-12-22 16:23:12 +05:30
parent fd6a854118
commit 202d0dc71f
16 changed files with 1111 additions and 166 deletions

View File

@@ -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 {

View File

@@ -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 =

View File

@@ -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 {

View File

@@ -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 }
}

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -129,6 +129,10 @@ class FakeKeyguardRepository : KeyguardRepository {
_isKeyguardShowing.value = isShowing
}
fun setKeyguardGoingAway(isGoingAway: Boolean) {
_isKeyguardGoingAway.value = isGoingAway
}
fun setKeyguardOccluded(isOccluded: Boolean) {
_isKeyguardOccluded.value = isOccluded
}

View File

@@ -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
}
}

View File

@@ -100,4 +100,8 @@ class FakeUserRepository : UserRepository {
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}
fun setUserSwitching(value: Boolean) {
_userSwitchingInProgress.value = value
}
}