Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0db1775e authored by Beverly's avatar Beverly
Browse files

Run face detect when face is locked out & show scanning anim

If face-bypass is not enabled, then do NOT run face detect;
instead show a message on the lockscreen that
face is unavailable.

This also updates KUM.isFaceDetectionRunning() to include
both faceAuth running AND faceDetect running. Previously,
it was only checking whether faceAuth was running so
there was no face scanning UI for face detect.

Fixes: 408069688
Flag: EXEMPT bugfix
Test: enable face auth bypass; lockout face (via
fingerprint failures or face failures); trigger
detect by waking up the device to the lockscreen;
observe face scanning animation by camera AND
on face detection, the primary bouncer shows
Test: atest DeviceEntryFaceAuthInteractorTest
Change-Id: I62244d81f3ee1518a10fbb0cd20c3e268089a73d

Change-Id: Id04260f112ae7ea2f5250276c74b761790590840
parent 6129796b
Loading
Loading
Loading
Loading
+36 −0
Original line number Diff line number Diff line
@@ -155,6 +155,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
    private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?>
    private lateinit var detectStatus: FlowValue<FaceDetectionStatus?>
    private lateinit var authRunning: FlowValue<Boolean?>
    private lateinit var detectRunning: FlowValue<Boolean?>
    private lateinit var bypassEnabled: FlowValue<Boolean?>
    private lateinit var lockedOut: FlowValue<Boolean?>
    private lateinit var canFaceAuthRun: FlowValue<Boolean?>
@@ -377,6 +378,38 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
                .isEqualTo(AUTHENTICATE_REASON_NOTIFICATION_PANEL_CLICKED)
        }

    @Test
    fun faceDetectionRunsAndSucceeds_detectRunningStateUpdates() =
        testScope.runTest {
            whenever(faceManager.sensorPropertiesInternal)
                .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true)))
            underTest = createDeviceEntryFaceAuthRepositoryImpl()
            initCollectors()

            underTest.detect(FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED)
            faceDetectIsCalled()
            assertThat(detectRunning()).isTrue()

            detectionCallback.value.onFaceDetected(1, 1, true)
            assertThat(detectRunning()).isFalse()
        }

    @Test
    fun faceDetectionRunsAndCancels_detectRunningStateUpdates() =
        testScope.runTest {
            whenever(faceManager.sensorPropertiesInternal)
                .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true)))
            underTest = createDeviceEntryFaceAuthRepositoryImpl()
            initCollectors()

            underTest.detect(FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED)
            faceDetectIsCalled()
            assertThat(detectRunning()).isTrue()

            underTest.cancel()
            assertThat(detectRunning()).isFalse()
        }

    @Test
    fun faceDetectDoesNotRunIfDetectionIsNotSupported() =
        testScope.runTest {
@@ -390,6 +423,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {

            verify(faceManager, never())
                .detectFace(any(), any(), any(FaceAuthenticateOptions::class.java))
            assertThat(detectRunning()).isFalse()
        }

    @Test
@@ -786,6 +820,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
            assertThat(authStatus()).isNull()
            assertThat(detectStatus()).isNull()
            assertThat(authRunning()).isNotNull()
            assertThat(detectRunning()).isNotNull()
            assertThat(bypassEnabled()).isNotNull()
            assertThat(lockedOut()).isNotNull()
            assertThat(canFaceAuthRun()).isNotNull()
@@ -1388,6 +1423,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
        authStatus = collectLastValue(underTest.authenticationStatus)
        detectStatus = collectLastValue(underTest.detectionStatus)
        authRunning = collectLastValue(underTest.isAuthRunning)
        detectRunning = collectLastValue(underTest.isDetectRunning)
        lockedOut = collectLastValue(underTest.isLockedOut)
        canFaceAuthRun = collectLastValue(underTest.canRunFaceAuth)
        authenticated = collectLastValue(underTest.isAuthenticated)
+15 −1
Original line number Diff line number Diff line
@@ -189,11 +189,12 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
        }

    @Test
    fun whenFaceIsLockedOutAnyAttemptsToTriggerFaceAuthMustProvideLockoutError() =
    fun whenFaceIsLockedOutAndNonBypassAnyAttemptsToTriggerFaceAuthMustProvideLockoutError() =
        testScope.runTest {
            underTest.start()
            val authenticationStatus = collectLastValue(underTest.authenticationStatus)
            faceAuthRepository.setLockedOut(true)
            kosmos.fakeDeviceEntryFaceAuthRepository.isBypassEnabled.value = false

            underTest.onDeviceLifted()

@@ -204,6 +205,19 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
            assertThat(faceAuthRepository.runningAuthRequest.value).isNull()
        }

    @Test
    fun whenFaceIsLockedOutAndBypass_DetectRuns() =
        testScope.runTest {
            underTest.start()
            val authenticationStatus = collectLastValue(underTest.authenticationStatus)
            faceAuthRepository.setLockedOut(true)
            kosmos.fakeDeviceEntryFaceAuthRepository.isBypassEnabled.value = true

            underTest.onDeviceLifted()

            assertThat(faceAuthRepository.runningAuthRequest.value).isNotNull()
        }

    @Test
    fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromAodState() =
        testScope.runTest {
+3 −1
Original line number Diff line number Diff line
@@ -1310,7 +1310,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, CoreSt
     */
    @Deprecated
    public boolean isFaceDetectionRunning() {
        return getFaceAuthInteractor() != null && getFaceAuthInteractor().isRunning();
        return getFaceAuthInteractor() != null
                && (getFaceAuthInteractor().isAuthRunning()
                || getFaceAuthInteractor().isDetectRunning());
    }

    private @Nullable DeviceEntryFaceAuthInteractor getFaceAuthInteractor() {
+39 −23
Original line number Diff line number Diff line
@@ -113,8 +113,11 @@ interface DeviceEntryFaceAuthRepository {
    /** Current state of whether face authentication is running. */
    val isAuthRunning: StateFlow<Boolean>

    /** Current state of whether face detection is running. */
    val isDetectRunning: StateFlow<Boolean>

    /** Whether bypass is currently enabled */
    val isBypassEnabled: Flow<Boolean>
    val isBypassEnabled: StateFlow<Boolean>

    /** Set whether face authentication should be locked out or not */
    fun setLockedOut(isLockedOut: Boolean)
@@ -197,6 +200,9 @@ constructor(
    override val isAuthRunning: StateFlow<Boolean>
        get() = _isAuthRunning

    private val _isDetectRunning = MutableStateFlow(false)
    override val isDetectRunning: StateFlow<Boolean> = _isDetectRunning

    private val keyguardSessionId: InstanceId?
        get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD)

@@ -209,8 +215,8 @@ constructor(

    private var cancellationInProgress = MutableStateFlow(false)

    override val isBypassEnabled: Flow<Boolean> =
        keyguardBypassController?.let {
    override val isBypassEnabled: StateFlow<Boolean> =
        (keyguardBypassController?.let {
                conflatedCallbackFlow {
                    val callback =
                        object : KeyguardBypassController.OnBypassStateChangedListener {
@@ -222,7 +228,12 @@ constructor(
                    trySendWithFailureLogging(it.bypassEnabled, TAG, "BypassStateChanged")
                    awaitClose { it.unregisterOnBypassStateChangedListener(callback) }
                }
        } ?: flowOf(false)
            } ?: flowOf(false))
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = keyguardBypassController?.isBypassEnabled() ?: false,
            )

    override fun setLockedOut(isLockedOut: Boolean) {
        _isLockedOut.value = isLockedOut
@@ -397,14 +408,15 @@ constructor(
                        .map { it.isTransitioning(to = Scenes.Gone) || it.isIdle(Scenes.Gone) }
                        .isFalse()
                } else {
                    (keyguardTransitionInteractor.isFinishedIn(KeyguardState.GONE)
                    (keyguardTransitionInteractor
                            .isFinishedIn(KeyguardState.GONE)
                            .or(
                                keyguardTransitionInteractor.isInTransition(
                                    Edge.create(to = Scenes.Gone),
                                Edge.create(to = KeyguardState.GONE)
                                    Edge.create(to = KeyguardState.GONE),
                                )
                        )
                    ).isFalse()
                            ))
                        .isFalse()
                },
                "keyguardNotGoneOrTransitioningToGone",
            ),
@@ -552,6 +564,7 @@ constructor(

    private val detectionCallback =
        FaceManager.FaceDetectionCallback { sensorId, userId, isStrong ->
            _isDetectRunning.value = false
            faceAuthLogger.faceDetected()
            _detectionStatus.value = FaceDetectionStatus(sensorId, userId, isStrong)
        }
@@ -669,6 +682,7 @@ constructor(
            return
        }
        withContext(mainDispatcher) {
            _isDetectRunning.value = true
            // We always want to invoke face detect in the main thread.
            faceAuthLogger.faceDetectionStarted()
            detectCancellationSignal?.cancel()
@@ -688,12 +702,13 @@ constructor(
        get() = userRepository.getSelectedUserInfo().id

    private fun cancelDetection() {
        _isDetectRunning.value = false
        detectCancellationSignal?.cancel()
        detectCancellationSignal = null
    }

    override fun cancel() {
        if (authCancellationSignal == null) return
        if (authCancellationSignal == null && detectCancellationSignal == null) return

        authCancellationSignal?.cancel()
        cancelNotReceivedHandlerJob?.cancel()
@@ -711,6 +726,7 @@ constructor(
            }
        cancellationInProgress.value = true
        _isAuthRunning.value = false
        _isDetectRunning.value = false
    }

    companion object {
+4 −2
Original line number Diff line number Diff line
@@ -49,8 +49,10 @@ class NoopDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceA

    override val isAuthRunning: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()

    override val isBypassEnabled: Flow<Boolean>
        get() = emptyFlow()
    override val isDetectRunning: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()

    override val isBypassEnabled: StateFlow<Boolean>
        get() = MutableStateFlow(false)

    override fun setLockedOut(isLockedOut: Boolean) = Unit

Loading