Merge "Create KeyguardFaceAuthManager to provide API for running face auth & face detection from Keyguard" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
e9797ad610
@@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.app.StatusBarManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.hardware.face.FaceManager
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import com.android.internal.logging.InstanceId
|
||||||
|
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.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.shared.model.AcquiredAuthenticationStatus
|
||||||
|
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.FailedAuthenticationStatus
|
||||||
|
import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus
|
||||||
|
import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus
|
||||||
|
import com.android.systemui.log.FaceAuthenticationLogger
|
||||||
|
import com.android.systemui.log.SessionTracker
|
||||||
|
import com.android.systemui.statusbar.phone.KeyguardBypassController
|
||||||
|
import com.android.systemui.user.data.repository.UserRepository
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.util.Arrays
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API to run face authentication and detection for device entry / on keyguard (as opposed to the
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
|
||||||
|
/** Provide the current status of face authentication. */
|
||||||
|
val authenticationStatus: Flow<AuthenticationStatus>
|
||||||
|
|
||||||
|
/** Provide the current status of face detection. */
|
||||||
|
val detectionStatus: Flow<DetectionStatus>
|
||||||
|
|
||||||
|
/** Current state of whether face authentication is locked out or not. */
|
||||||
|
val isLockedOut: Flow<Boolean>
|
||||||
|
|
||||||
|
/** Current state of whether face authentication is running. */
|
||||||
|
val isAuthRunning: Flow<Boolean>
|
||||||
|
|
||||||
|
/** Is face detection supported. */
|
||||||
|
val isDetectionSupported: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@SysUISingleton
|
||||||
|
class KeyguardFaceAuthManagerImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
private val faceManager: FaceManager? = null,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val keyguardBypassController: KeyguardBypassController? = null,
|
||||||
|
@Application private val applicationScope: CoroutineScope,
|
||||||
|
@Main private val mainDispatcher: CoroutineDispatcher,
|
||||||
|
private val sessionTracker: SessionTracker,
|
||||||
|
private val uiEventsLogger: UiEventLogger,
|
||||||
|
private val faceAuthLogger: FaceAuthenticationLogger,
|
||||||
|
dumpManager: DumpManager,
|
||||||
|
) : KeyguardFaceAuthManager, Dumpable {
|
||||||
|
private var cancellationSignal: CancellationSignal? = null
|
||||||
|
private val lockscreenBypassEnabled: Boolean
|
||||||
|
get() = keyguardBypassController?.bypassEnabled ?: false
|
||||||
|
private var faceAcquiredInfoIgnoreList: Set<Int>
|
||||||
|
|
||||||
|
private val faceLockoutResetCallback =
|
||||||
|
object : FaceManager.LockoutResetCallback() {
|
||||||
|
override fun onLockoutReset(sensorId: Int) {
|
||||||
|
_isLockedOut.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
|
||||||
|
faceAcquiredInfoIgnoreList =
|
||||||
|
Arrays.stream(
|
||||||
|
context.resources.getIntArray(
|
||||||
|
R.array.config_face_acquire_device_entry_ignorelist
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
dumpManager.registerCriticalDumpable("KeyguardFaceAuthManagerImpl", this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val faceAuthCallback =
|
||||||
|
object : FaceManager.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
_authenticationStatus.value = FailedAuthenticationStatus
|
||||||
|
faceAuthLogger.authenticationFailed()
|
||||||
|
onFaceAuthRequestCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationAcquired(acquireInfo: Int) {
|
||||||
|
_authenticationStatus.value = AcquiredAuthenticationStatus(acquireInfo)
|
||||||
|
faceAuthLogger.authenticationAcquired(acquireInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
|
||||||
|
val errorStatus = ErrorAuthenticationStatus(errorCode, errString.toString())
|
||||||
|
if (errorStatus.isLockoutError()) {
|
||||||
|
_isLockedOut.value = true
|
||||||
|
}
|
||||||
|
_authenticationStatus.value = errorStatus
|
||||||
|
if (errorStatus.isCancellationError()) {
|
||||||
|
cancelNotReceivedHandlerJob?.cancel()
|
||||||
|
applicationScope.launch {
|
||||||
|
faceAuthLogger.launchingQueuedFaceAuthRequest(
|
||||||
|
faceAuthRequestedWhileCancellation
|
||||||
|
)
|
||||||
|
faceAuthRequestedWhileCancellation?.let { authenticate(it) }
|
||||||
|
faceAuthRequestedWhileCancellation = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
faceAuthLogger.authenticationError(
|
||||||
|
errorCode,
|
||||||
|
errString,
|
||||||
|
errorStatus.isLockoutError(),
|
||||||
|
errorStatus.isCancellationError()
|
||||||
|
)
|
||||||
|
onFaceAuthRequestCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationHelp(code: Int, helpStr: CharSequence?) {
|
||||||
|
if (faceAcquiredInfoIgnoreList.contains(code)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_authenticationStatus.value = HelpAuthenticationStatus(code, helpStr.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) {
|
||||||
|
_authenticationStatus.value = SuccessAuthenticationStatus(result)
|
||||||
|
faceAuthLogger.faceAuthSuccess(result)
|
||||||
|
onFaceAuthRequestCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFaceAuthRequestCompleted() {
|
||||||
|
cancellationInProgress = false
|
||||||
|
_isAuthRunning.value = false
|
||||||
|
cancellationSignal = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val detectionCallback =
|
||||||
|
FaceManager.FaceDetectionCallback { sensorId, userId, isStrong ->
|
||||||
|
faceAuthLogger.faceDetected()
|
||||||
|
_detectionStatus.value = DetectionStatus(sensorId, userId, isStrong)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cancellationInProgress = false
|
||||||
|
private var faceAuthRequestedWhileCancellation: FaceAuthUiEvent? = null
|
||||||
|
|
||||||
|
override suspend fun authenticate(uiEvent: FaceAuthUiEvent) {
|
||||||
|
if (_isAuthRunning.value) {
|
||||||
|
faceAuthLogger.ignoredFaceAuthTrigger(uiEvent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancellationInProgress) {
|
||||||
|
faceAuthLogger.queuingRequestWhileCancelling(
|
||||||
|
faceAuthRequestedWhileCancellation,
|
||||||
|
uiEvent
|
||||||
|
)
|
||||||
|
faceAuthRequestedWhileCancellation = uiEvent
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
|
currentUserId,
|
||||||
|
lockscreenBypassEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun detect() {
|
||||||
|
if (!isDetectionSupported) {
|
||||||
|
faceAuthLogger.detectionNotSupported(faceManager, faceManager?.sensorPropertiesInternal)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (_isAuthRunning.value) {
|
||||||
|
faceAuthLogger.skippingBecauseAlreadyRunning("detection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationSignal = CancellationSignal()
|
||||||
|
withContext(mainDispatcher) {
|
||||||
|
// We always want to invoke face detect in the main thread.
|
||||||
|
faceAuthLogger.faceDetectionStarted()
|
||||||
|
faceManager?.detectFace(cancellationSignal, detectionCallback, currentUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentUserId: Int
|
||||||
|
get() = userRepository.getSelectedUserInfo().id
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
if (cancellationSignal == null) return
|
||||||
|
|
||||||
|
cancellationSignal?.cancel()
|
||||||
|
cancelNotReceivedHandlerJob =
|
||||||
|
applicationScope.launch {
|
||||||
|
delay(DEFAULT_CANCEL_SIGNAL_TIMEOUT)
|
||||||
|
faceAuthLogger.cancelSignalNotReceived(
|
||||||
|
_isAuthRunning.value,
|
||||||
|
_isLockedOut.value,
|
||||||
|
cancellationInProgress,
|
||||||
|
faceAuthRequestedWhileCancellation
|
||||||
|
)
|
||||||
|
onFaceAuthRequestCompleted()
|
||||||
|
}
|
||||||
|
cancellationInProgress = true
|
||||||
|
_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)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "KeyguardFaceAuthManager"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no cancel signal has been received after this amount of time, assume that it is
|
||||||
|
* cancelled.
|
||||||
|
*/
|
||||||
|
const val DEFAULT_CANCEL_SIGNAL_TIMEOUT = 3000L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dump(pw: PrintWriter, args: Array<out String>) {
|
||||||
|
pw.println("KeyguardFaceAuthManagerImpl state:")
|
||||||
|
pw.println(" cancellationInProgress: $cancellationInProgress")
|
||||||
|
pw.println(" _isLockedOut.value: ${_isLockedOut.value}")
|
||||||
|
pw.println(" _isAuthRunning.value: ${_isAuthRunning.value}")
|
||||||
|
pw.println(" isDetectionSupported: $isDetectionSupported")
|
||||||
|
pw.println(" FaceManager state:")
|
||||||
|
pw.println(" faceManager: $faceManager")
|
||||||
|
pw.println(" sensorPropertiesInternal: ${faceManager?.sensorPropertiesInternal}")
|
||||||
|
pw.println(
|
||||||
|
" supportsFaceDetection: " +
|
||||||
|
"${faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection}"
|
||||||
|
)
|
||||||
|
pw.println(
|
||||||
|
" faceAuthRequestedWhileCancellation: ${faceAuthRequestedWhileCancellation?.reason}"
|
||||||
|
)
|
||||||
|
pw.println(" cancellationSignal: $cancellationSignal")
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.shared.model
|
||||||
|
|
||||||
|
import android.hardware.face.FaceManager
|
||||||
|
|
||||||
|
/** Authentication status provided by [com.android.keyguard.faceauth.KeyguardFaceAuthManager] */
|
||||||
|
sealed class AuthenticationStatus
|
||||||
|
|
||||||
|
/** Success authentication status. */
|
||||||
|
data class SuccessAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) :
|
||||||
|
AuthenticationStatus()
|
||||||
|
|
||||||
|
/** Face authentication help message. */
|
||||||
|
data class HelpAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus()
|
||||||
|
|
||||||
|
/** Face acquired message. */
|
||||||
|
data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationStatus()
|
||||||
|
|
||||||
|
/** Face authentication failed message. */
|
||||||
|
object FailedAuthenticationStatus : AuthenticationStatus()
|
||||||
|
|
||||||
|
/** Face authentication error message */
|
||||||
|
data class ErrorAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus() {
|
||||||
|
/**
|
||||||
|
* Method that checks if [msgId] is a lockout error. A lockout error means that face
|
||||||
|
* authentication is locked out.
|
||||||
|
*/
|
||||||
|
fun isLockoutError() = msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that checks if [msgId] is a cancellation error. This means that face authentication
|
||||||
|
* was cancelled before it completed.
|
||||||
|
*/
|
||||||
|
fun isCancellationError() = msgId == FaceManager.FACE_ERROR_CANCELED
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Face detection success message. */
|
||||||
|
data class DetectionStatus(val sensorId: Int, val userId: Int, val isStrongBiometric: Boolean)
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package com.android.systemui.log
|
||||||
|
|
||||||
|
import android.hardware.face.FaceManager
|
||||||
|
import android.hardware.face.FaceSensorPropertiesInternal
|
||||||
|
import com.android.keyguard.FaceAuthUiEvent
|
||||||
|
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]
|
||||||
|
*
|
||||||
|
* To enable logcat echoing for an entire buffer:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* adb shell settings put global systemui/buffer/KeyguardFaceAuthManagerLog <logLevel>
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@SysUISingleton
|
||||||
|
class FaceAuthenticationLogger
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@FaceAuthLog private val logBuffer: LogBuffer,
|
||||||
|
) {
|
||||||
|
fun ignoredFaceAuthTrigger(uiEvent: FaceAuthUiEvent) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{ str1 = uiEvent.reason },
|
||||||
|
{
|
||||||
|
"Ignoring trigger because face auth is currently running. " +
|
||||||
|
"Trigger reason: $str1"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queuingRequestWhileCancelling(
|
||||||
|
alreadyQueuedRequest: FaceAuthUiEvent?,
|
||||||
|
newRequest: FaceAuthUiEvent
|
||||||
|
) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{
|
||||||
|
str1 = alreadyQueuedRequest?.reason
|
||||||
|
str2 = newRequest.reason
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Face auth requested while previous request is being cancelled, " +
|
||||||
|
"already queued request: $str1 queueing the new request: $str2"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authenticating(uiEvent: FaceAuthUiEvent) {
|
||||||
|
logBuffer.log(TAG, DEBUG, { str1 = uiEvent.reason }, { "Running authenticate for $str1" })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detectionNotSupported(
|
||||||
|
faceManager: FaceManager?,
|
||||||
|
sensorPropertiesInternal: MutableList<FaceSensorPropertiesInternal>?
|
||||||
|
) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{
|
||||||
|
bool1 = faceManager == null
|
||||||
|
bool2 = sensorPropertiesInternal.isNullOrEmpty()
|
||||||
|
bool2 = sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skipping detection request because it is not supported, " +
|
||||||
|
"faceManager isNull: $bool1, " +
|
||||||
|
"sensorPropertiesInternal isNullOrEmpty: $bool2, " +
|
||||||
|
"supportsFaceDetection: $bool3"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skippingBecauseAlreadyRunning(@CompileTimeConstant operation: String) {
|
||||||
|
logBuffer.log(TAG, DEBUG, "isAuthRunning is true, skipping $operation")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun faceDetectionStarted() {
|
||||||
|
logBuffer.log(TAG, DEBUG, "Face detection started.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun faceDetected() {
|
||||||
|
logBuffer.log(TAG, DEBUG, "Face detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelSignalNotReceived(
|
||||||
|
isAuthRunning: Boolean,
|
||||||
|
isLockedOut: Boolean,
|
||||||
|
cancellationInProgress: Boolean,
|
||||||
|
faceAuthRequestedWhileCancellation: FaceAuthUiEvent?
|
||||||
|
) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{
|
||||||
|
bool1 = isAuthRunning
|
||||||
|
bool2 = isLockedOut
|
||||||
|
bool3 = cancellationInProgress
|
||||||
|
str1 = "${faceAuthRequestedWhileCancellation?.reason}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Cancel signal was not received, running timeout handler to reset state. " +
|
||||||
|
"State before reset: " +
|
||||||
|
"isAuthRunning: $bool1, " +
|
||||||
|
"isLockedOut: $bool2, " +
|
||||||
|
"cancellationInProgress: $bool3, " +
|
||||||
|
"faceAuthRequestedWhileCancellation: $str1"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authenticationFailed() {
|
||||||
|
logBuffer.log(TAG, DEBUG, "Face authentication failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authenticationAcquired(acquireInfo: Int) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{ int1 = acquireInfo },
|
||||||
|
{ "Face acquired during face authentication: acquireInfo: $int1 " }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authenticationError(
|
||||||
|
errorCode: Int,
|
||||||
|
errString: CharSequence?,
|
||||||
|
lockoutError: Boolean,
|
||||||
|
cancellationError: Boolean
|
||||||
|
) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{
|
||||||
|
int1 = errorCode
|
||||||
|
str1 = "$errString"
|
||||||
|
bool1 = lockoutError
|
||||||
|
bool2 = cancellationError
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Received authentication error: errorCode: $int1, " +
|
||||||
|
"errString: $str1, " +
|
||||||
|
"isLockoutError: $bool1, " +
|
||||||
|
"isCancellationError: $bool2"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchingQueuedFaceAuthRequest(faceAuthRequestedWhileCancellation: FaceAuthUiEvent?) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{ str1 = "${faceAuthRequestedWhileCancellation?.reason}" },
|
||||||
|
{ "Received cancellation error and starting queued face auth request: $str1" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun faceAuthSuccess(result: FaceManager.AuthenticationResult) {
|
||||||
|
logBuffer.log(
|
||||||
|
TAG,
|
||||||
|
DEBUG,
|
||||||
|
{
|
||||||
|
int1 = result.userId
|
||||||
|
bool1 = result.isStrongBiometric
|
||||||
|
},
|
||||||
|
{ "Face authenticated successfully: userId: $int1, isStrongBiometric: $bool1" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.android.systemui.log.dagger
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
/** A [com.android.systemui.log.LogBuffer] for Face authentication triggered by SysUI. */
|
||||||
|
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class FaceAuthLog()
|
||||||
@@ -349,6 +349,17 @@ public class LogModule {
|
|||||||
return factory.create("ScreenDecorationsLog", 200);
|
return factory.create("ScreenDecorationsLog", 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a {@link LogBuffer} for use by
|
||||||
|
* {@link com.android.keyguard.faceauth.KeyguardFaceAuthManagerImpl}.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@SysUISingleton
|
||||||
|
@FaceAuthLog
|
||||||
|
public static LogBuffer provideFaceAuthLog(LogBufferFactory factory) {
|
||||||
|
return factory.create("KeyguardFaceAuthManagerLog", 300);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a {@link LogBuffer} for bluetooth-related logs.
|
* Provides a {@link LogBuffer} for bluetooth-related logs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,428 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.app.StatusBarManager.SESSION_KEYGUARD
|
||||||
|
import android.content.pm.UserInfo
|
||||||
|
import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_CANCELED
|
||||||
|
import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT
|
||||||
|
import android.hardware.biometrics.ComponentInfoInternal
|
||||||
|
import android.hardware.face.FaceManager
|
||||||
|
import android.hardware.face.FaceSensorProperties
|
||||||
|
import android.hardware.face.FaceSensorPropertiesInternal
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.internal.logging.InstanceId.fakeInstanceId
|
||||||
|
import com.android.internal.logging.UiEventLogger
|
||||||
|
import com.android.keyguard.FaceAuthUiEvent
|
||||||
|
import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN
|
||||||
|
import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
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.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.log.FaceAuthenticationLogger
|
||||||
|
import com.android.systemui.log.SessionTracker
|
||||||
|
import com.android.systemui.statusbar.phone.KeyguardBypassController
|
||||||
|
import com.android.systemui.user.data.repository.FakeUserRepository
|
||||||
|
import com.android.systemui.util.mockito.whenever
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.StringWriter
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.ArgumentMatchers.any
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.ArgumentMatchers.eq
|
||||||
|
import org.mockito.Captor
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.clearInvocations
|
||||||
|
import org.mockito.Mockito.isNull
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
class KeyguardFaceAuthManagerTest : SysuiTestCase() {
|
||||||
|
private lateinit var underTest: KeyguardFaceAuthManagerImpl
|
||||||
|
|
||||||
|
@Mock private lateinit var faceManager: FaceManager
|
||||||
|
@Mock private lateinit var bypassController: KeyguardBypassController
|
||||||
|
@Mock private lateinit var sessionTracker: SessionTracker
|
||||||
|
@Mock private lateinit var uiEventLogger: UiEventLogger
|
||||||
|
@Mock private lateinit var dumpManager: DumpManager
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private lateinit var authenticationCallback: ArgumentCaptor<FaceManager.AuthenticationCallback>
|
||||||
|
@Captor
|
||||||
|
private lateinit var detectionCallback: ArgumentCaptor<FaceManager.FaceDetectionCallback>
|
||||||
|
@Captor private lateinit var cancellationSignal: ArgumentCaptor<CancellationSignal>
|
||||||
|
@Captor
|
||||||
|
private lateinit var faceLockoutResetCallback: ArgumentCaptor<FaceManager.LockoutResetCallback>
|
||||||
|
private lateinit var testDispatcher: TestDispatcher
|
||||||
|
|
||||||
|
private lateinit var testScope: TestScope
|
||||||
|
private lateinit var fakeUserRepository: FakeUserRepository
|
||||||
|
private lateinit var authStatus: FlowValue<AuthenticationStatus?>
|
||||||
|
private lateinit var detectStatus: FlowValue<DetectionStatus?>
|
||||||
|
private lateinit var authRunning: FlowValue<Boolean?>
|
||||||
|
private lateinit var lockedOut: FlowValue<Boolean?>
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
fakeUserRepository = FakeUserRepository()
|
||||||
|
fakeUserRepository.setUserInfos(listOf(currentUser))
|
||||||
|
testDispatcher = StandardTestDispatcher()
|
||||||
|
testScope = TestScope(testDispatcher)
|
||||||
|
whenever(sessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(keyguardSessionId)
|
||||||
|
whenever(bypassController.bypassEnabled).thenReturn(true)
|
||||||
|
underTest = createFaceAuthManagerImpl(faceManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFaceAuthManagerImpl(
|
||||||
|
fmOverride: FaceManager? = faceManager,
|
||||||
|
bypassControllerOverride: KeyguardBypassController? = bypassController
|
||||||
|
) =
|
||||||
|
KeyguardFaceAuthManagerImpl(
|
||||||
|
mContext,
|
||||||
|
fmOverride,
|
||||||
|
fakeUserRepository,
|
||||||
|
bypassControllerOverride,
|
||||||
|
testScope.backgroundScope,
|
||||||
|
testDispatcher,
|
||||||
|
sessionTracker,
|
||||||
|
uiEventLogger,
|
||||||
|
FaceAuthenticationLogger(logcatLogBuffer("KeyguardFaceAuthManagerLog")),
|
||||||
|
dumpManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceAuthRunsAndProvidesAuthStatusUpdates() =
|
||||||
|
testScope.runTest {
|
||||||
|
testSetup(this)
|
||||||
|
|
||||||
|
FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER.extraInfo = 10
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
uiEventIsLogged(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
|
||||||
|
assertThat(authRunning()).isTrue()
|
||||||
|
|
||||||
|
val successResult = successResult()
|
||||||
|
authenticationCallback.value.onAuthenticationSucceeded(successResult)
|
||||||
|
|
||||||
|
assertThat(authStatus()).isEqualTo(SuccessAuthenticationStatus(successResult))
|
||||||
|
|
||||||
|
assertThat(authRunning()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uiEventIsLogged(faceAuthUiEvent: FaceAuthUiEvent) {
|
||||||
|
verify(uiEventLogger)
|
||||||
|
.logWithInstanceIdAndPosition(
|
||||||
|
faceAuthUiEvent,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
keyguardSessionId,
|
||||||
|
faceAuthUiEvent.extraInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceAuthDoesNotRunWhileItIsAlreadyRunning() =
|
||||||
|
testScope.runTest {
|
||||||
|
testSetup(this)
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
clearInvocations(faceManager)
|
||||||
|
clearInvocations(uiEventLogger)
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
verifyNoMoreInteractions(faceManager)
|
||||||
|
verifyNoMoreInteractions(uiEventLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceLockoutStatusIsPropagated() =
|
||||||
|
testScope.runTest {
|
||||||
|
testSetup(this)
|
||||||
|
verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture())
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
|
||||||
|
authenticationCallback.value.onAuthenticationError(
|
||||||
|
FACE_ERROR_LOCKOUT_PERMANENT,
|
||||||
|
"face locked out"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(lockedOut()).isTrue()
|
||||||
|
|
||||||
|
faceLockoutResetCallback.value.onLockoutReset(0)
|
||||||
|
assertThat(lockedOut()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceDetectionSupportIsTheCorrectValue() =
|
||||||
|
testScope.runTest {
|
||||||
|
assertThat(createFaceAuthManagerImpl(fmOverride = null).isDetectionSupported).isFalse()
|
||||||
|
|
||||||
|
whenever(faceManager.sensorPropertiesInternal).thenReturn(null)
|
||||||
|
assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
|
||||||
|
|
||||||
|
whenever(faceManager.sensorPropertiesInternal).thenReturn(listOf())
|
||||||
|
assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
|
||||||
|
|
||||||
|
whenever(faceManager.sensorPropertiesInternal)
|
||||||
|
.thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = false)))
|
||||||
|
assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
|
||||||
|
|
||||||
|
whenever(faceManager.sensorPropertiesInternal)
|
||||||
|
.thenReturn(
|
||||||
|
listOf(
|
||||||
|
createFaceSensorProperties(supportsFaceDetection = false),
|
||||||
|
createFaceSensorProperties(supportsFaceDetection = true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
|
||||||
|
|
||||||
|
whenever(faceManager.sensorPropertiesInternal)
|
||||||
|
.thenReturn(
|
||||||
|
listOf(
|
||||||
|
createFaceSensorProperties(supportsFaceDetection = true),
|
||||||
|
createFaceSensorProperties(supportsFaceDetection = false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(createFaceAuthManagerImpl().isDetectionSupported).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelStopsFaceAuthentication() =
|
||||||
|
testScope.runTest {
|
||||||
|
testSetup(this)
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
|
||||||
|
var wasAuthCancelled = false
|
||||||
|
cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true }
|
||||||
|
|
||||||
|
underTest.cancel()
|
||||||
|
assertThat(wasAuthCancelled).isTrue()
|
||||||
|
assertThat(authRunning()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelInvokedWithoutFaceAuthRunningIsANoop() = testScope.runTest { underTest.cancel() }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceDetectionRunsAndPropagatesDetectionStatus() =
|
||||||
|
testScope.runTest {
|
||||||
|
whenever(faceManager.sensorPropertiesInternal)
|
||||||
|
.thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true)))
|
||||||
|
underTest = createFaceAuthManagerImpl()
|
||||||
|
testSetup(this)
|
||||||
|
|
||||||
|
underTest.detect()
|
||||||
|
faceDetectIsCalled()
|
||||||
|
|
||||||
|
detectionCallback.value.onFaceDetected(1, 1, true)
|
||||||
|
|
||||||
|
assertThat(detectStatus()).isEqualTo(DetectionStatus(1, 1, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceDetectDoesNotRunIfDetectionIsNotSupported() =
|
||||||
|
testScope.runTest {
|
||||||
|
whenever(faceManager.sensorPropertiesInternal)
|
||||||
|
.thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = false)))
|
||||||
|
underTest = createFaceAuthManagerImpl()
|
||||||
|
testSetup(this)
|
||||||
|
clearInvocations(faceManager)
|
||||||
|
|
||||||
|
underTest.detect()
|
||||||
|
|
||||||
|
verify(faceManager, never()).detectFace(any(), any(), anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceAuthShouldWaitAndRunIfTriggeredWhileCancelling() =
|
||||||
|
testScope.runTest {
|
||||||
|
testSetup(this)
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
|
||||||
|
// Enter cancelling state
|
||||||
|
underTest.cancel()
|
||||||
|
clearInvocations(faceManager)
|
||||||
|
|
||||||
|
// Auth is while cancelling.
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
|
||||||
|
// Auth is not started
|
||||||
|
verifyNoMoreInteractions(faceManager)
|
||||||
|
|
||||||
|
// Auth is done cancelling.
|
||||||
|
authenticationCallback.value.onAuthenticationError(
|
||||||
|
FACE_ERROR_CANCELED,
|
||||||
|
"First auth attempt cancellation completed"
|
||||||
|
)
|
||||||
|
assertThat(authStatus())
|
||||||
|
.isEqualTo(
|
||||||
|
ErrorAuthenticationStatus(
|
||||||
|
FACE_ERROR_CANCELED,
|
||||||
|
"First auth attempt cancellation completed"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
uiEventIsLogged(FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceAuthAutoCancelsAfterDefaultCancellationTimeout() =
|
||||||
|
testScope.runTest {
|
||||||
|
testSetup(this)
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
|
||||||
|
clearInvocations(faceManager)
|
||||||
|
underTest.cancel()
|
||||||
|
advanceTimeBy(KeyguardFaceAuthManagerImpl.DEFAULT_CANCEL_SIGNAL_TIMEOUT + 1)
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun faceHelpMessagesAreIgnoredBasedOnConfig() =
|
||||||
|
testScope.runTest {
|
||||||
|
overrideResource(
|
||||||
|
R.array.config_face_acquire_device_entry_ignorelist,
|
||||||
|
intArrayOf(10, 11)
|
||||||
|
)
|
||||||
|
underTest = createFaceAuthManagerImpl()
|
||||||
|
testSetup(this)
|
||||||
|
|
||||||
|
underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
|
||||||
|
faceAuthenticateIsCalled()
|
||||||
|
|
||||||
|
authenticationCallback.value.onAuthenticationHelp(9, "help msg")
|
||||||
|
authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
|
||||||
|
authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
|
||||||
|
|
||||||
|
assertThat(authStatus()).isEqualTo(HelpAuthenticationStatus(9, "help msg"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dumpDoesNotErrorOutWhenFaceManagerOrBypassControllerIsNull() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeUserRepository.setSelectedUserInfo(currentUser)
|
||||||
|
underTest.dump(PrintWriter(StringWriter()), emptyArray())
|
||||||
|
|
||||||
|
underTest =
|
||||||
|
createFaceAuthManagerImpl(fmOverride = null, bypassControllerOverride = null)
|
||||||
|
fakeUserRepository.setSelectedUserInfo(currentUser)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun successResult() = FaceManager.AuthenticationResult(null, null, currentUserId, false)
|
||||||
|
|
||||||
|
private fun faceDetectIsCalled() {
|
||||||
|
verify(faceManager)
|
||||||
|
.detectFace(
|
||||||
|
cancellationSignal.capture(),
|
||||||
|
detectionCallback.capture(),
|
||||||
|
eq(currentUserId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun faceAuthenticateIsCalled() {
|
||||||
|
verify(faceManager)
|
||||||
|
.authenticate(
|
||||||
|
isNull(),
|
||||||
|
cancellationSignal.capture(),
|
||||||
|
authenticationCallback.capture(),
|
||||||
|
isNull(),
|
||||||
|
eq(currentUserId),
|
||||||
|
eq(true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFaceSensorProperties(
|
||||||
|
supportsFaceDetection: Boolean
|
||||||
|
): FaceSensorPropertiesInternal {
|
||||||
|
val componentInfo =
|
||||||
|
listOf(
|
||||||
|
ComponentInfoInternal(
|
||||||
|
"faceSensor" /* componentId */,
|
||||||
|
"vendor/model/revision" /* hardwareVersion */,
|
||||||
|
"1.01" /* firmwareVersion */,
|
||||||
|
"00000001" /* serialNumber */,
|
||||||
|
"" /* softwareVersion */
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return FaceSensorPropertiesInternal(
|
||||||
|
0 /* id */,
|
||||||
|
FaceSensorProperties.STRENGTH_STRONG,
|
||||||
|
1 /* maxTemplatesAllowed */,
|
||||||
|
componentInfo,
|
||||||
|
FaceSensorProperties.TYPE_UNKNOWN,
|
||||||
|
supportsFaceDetection /* supportsFaceDetection */,
|
||||||
|
true /* supportsSelfIllumination */,
|
||||||
|
false /* resetLockoutRequiresChallenge */
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val currentUserId = 1
|
||||||
|
val keyguardSessionId = fakeInstanceId(10)!!
|
||||||
|
val currentUser = UserInfo(currentUserId, "test user", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user