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>
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt
new file mode 100644
index 0000000..2069891
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt
@@ -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")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
new file mode 100644
index 0000000..b1c5f8f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -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)
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
new file mode 100644
index 0000000..f7349a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -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" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/FaceAuthLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/FaceAuthLog.kt
new file mode 100644
index 0000000..b97e3a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/FaceAuthLog.kt
@@ -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()
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 5341cd5..817de79 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -350,6 +350,17 @@
     }
 
     /**
+     * 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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt
new file mode 100644
index 0000000..7c604f7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt
@@ -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)
+    }
+}