Merge "Create KeyguardFaceAuthManager to provide API for running face auth & face detection from Keyguard" into tm-qpr-dev am: e9797ad610

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20789601

Change-Id: I773e6320f506fa92de0d78724b3116c41039321c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
TreeHugger Robot
2023-02-15 22:55:23 +00:00
committed by Automerger Merge Worker
6 changed files with 1021 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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