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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -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