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

Commit 6ef648c9 authored by Austin Delgado's avatar Austin Delgado
Browse files

Fix BP face auth requiring full tap after auth

Fixes face auth in BP requiring user to lift and tap again if face auth
completes before finger.

NOTE: This version requires finger to be held down during face auth as confirmation.

Bug: 291960666
Test: atest PromptViewModelTest
Change-Id: I0a82720561a10cf79716784a34e165b86055d5bf
parent 42f90671
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -644,8 +644,9 @@ public class UdfpsController implements DozeReceiver, Dumpable {
            shouldPilfer = true;
        }

        // Pilfer only once per gesture
        if (shouldPilfer && !mPointerPilfered) {
        // Pilfer only once per gesture, don't pilfer for BP
        if (shouldPilfer && !mPointerPilfered
                && getBiometricSessionType() != SESSION_BIOMETRIC_PROMPT) {
            mInputManager.pilferPointers(
                    mOverlay.getOverlayView().getViewRootImpl().getInputToken());
            mPointerPilfered = true;
+18 −9
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics.ui.binder

import android.animation.Animator
import android.annotation.SuppressLint
import android.content.Context
import android.hardware.biometrics.BiometricAuthenticator
import android.hardware.biometrics.BiometricConstants
@@ -25,6 +26,7 @@ import android.hardware.face.FaceManager
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
import android.view.accessibility.AccessibilityManager
@@ -69,6 +71,7 @@ private const val TAG = "BiometricViewBinder"
object BiometricViewBinder {

    /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */
    @SuppressLint("ClickableViewAccessibility")
    @JvmStatic
    fun bind(
        view: BiometricPromptLayout,
@@ -297,21 +300,19 @@ object BiometricViewBinder {

                // reuse the icon as a confirm button
                launch {
                    viewModel.isConfirmButtonVisible
                    viewModel.isIconConfirmButton
                        .map { isPending ->
                            when {
                                isPending && iconController.actsAsConfirmButton ->
                                    View.OnClickListener { viewModel.confirmAuthenticated() }
                                else -> null
                                    View.OnTouchListener { _: View, event: MotionEvent ->
                                        viewModel.onOverlayTouch(event)
                                    }
                                else -> null
                            }
                        .collect { onClick ->
                            iconViewOverlay.setOnClickListener(onClick)
                            iconView.setOnClickListener(onClick)
                            if (onClick == null) {
                                iconViewOverlay.isClickable = false
                                iconView.isClickable = false
                        }
                        .collect { onTouch ->
                            iconViewOverlay.setOnTouchListener(onTouch)
                            iconView.setOnTouchListener(onTouch)
                        }
                }

@@ -344,6 +345,14 @@ object BiometricViewBinder {
                            backgroundView.setOnClickListener(null)
                            backgroundView.importantForAccessibility =
                                IMPORTANT_FOR_ACCESSIBILITY_NO

                            // Allow icon to be used as confirmation button with a11y enabled
                            if (accessibilityManager.isTouchExplorationEnabled) {
                                iconViewOverlay.setOnClickListener {
                                    viewModel.confirmAuthenticated()
                                }
                                iconView.setOnClickListener { viewModel.confirmAuthenticated() }
                            }
                        }
                        if (authState.isAuthenticatedAndConfirmed) {
                            view.announceForAccessibility(
+40 −12
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package com.android.systemui.biometrics.ui.viewmodel

import android.hardware.biometrics.BiometricPrompt
import android.util.Log
import android.view.MotionEvent
import com.android.systemui.biometrics.AuthBiometricView
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
import com.android.systemui.biometrics.domain.model.BiometricModalities
@@ -63,11 +64,18 @@ constructor(
    /** If the user has successfully authenticated and confirmed (when explicitly required). */
    val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow()

    private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false)

    /**
     * If the API caller or the user's personal preferences require explicit confirmation after
     * successful authentication.
     */
    val isConfirmationRequired: Flow<Boolean> = interactor.isConfirmationRequired
    val isConfirmationRequired: Flow<Boolean> =
        combine(_isOverlayTouched, interactor.isConfirmationRequired) {
            isOverlayTouched,
            isConfirmationRequired ->
            !isOverlayTouched && isConfirmationRequired
        }

    /** The kind of credential the user has. */
    val credentialKind: Flow<PromptKind> = interactor.credentialKind
@@ -141,6 +149,12 @@ constructor(
            }
            .distinctUntilChanged()

    /** If the icon can be used as a confirmation button. */
    val isIconConfirmButton: Flow<Boolean> =
        combine(size, interactor.isConfirmationRequired) { size, isConfirmationRequired ->
            size.isNotSmall && isConfirmationRequired
        }

    /** If the negative button should be shown. */
    val isNegativeButtonVisible: Flow<Boolean> =
        combine(
@@ -286,8 +300,10 @@ constructor(
            if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
        _forceMediumSize.value = true
        _legacyState.value =
            if (alreadyAuthenticated) {
            if (alreadyAuthenticated && isConfirmationRequired.first()) {
                AuthBiometricView.STATE_PENDING_CONFIRMATION
            } else if (alreadyAuthenticated && !isConfirmationRequired.first()) {
                AuthBiometricView.STATE_AUTHENTICATED
            } else {
                AuthBiometricView.STATE_HELP
            }
@@ -385,18 +401,10 @@ constructor(
    }

    private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
        val availableModalities = modalities.first()
        val confirmationRequired = isConfirmationRequired.first()

        if (availableModalities.hasFaceAndFingerprint) {
            // coex only needs confirmation when face is successful, unless it happens on the
            // first attempt (i.e. without failure) before fingerprint scanning starts
            val fingerprintStarted = fingerprintStartMode.first() != FingerprintStartMode.Pending
        // Only worry about confirmationRequired if face was used to unlock
        if (modality == BiometricModality.Face) {
                return fingerprintStarted || confirmationRequired
            }
        }
        if (availableModalities.hasFaceOnly) {
            return confirmationRequired
        }
        // fingerprint only never requires confirmation
@@ -426,6 +434,26 @@ constructor(
        messageJob = null
    }

    /**
     * Touch event occurred on the overlay
     *
     * Tracks whether a finger is currently down to set [_isOverlayTouched] to be used as user
     * confirmation
     */
    fun onOverlayTouch(event: MotionEvent): Boolean {
        if (event.actionMasked == MotionEvent.ACTION_DOWN) {
            _isOverlayTouched.value = true

            if (_isAuthenticated.value.needsUserConfirmation) {
                confirmAuthenticated()
            }
            return true
        } else if (event.actionMasked == MotionEvent.ACTION_UP) {
            _isOverlayTouched.value = false
        }
        return false
    }

    /**
     * Switch to the credential view.
     *
+6 −1
Original line number Diff line number Diff line
@@ -499,6 +499,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
        val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
        val size by collectLastValue(viewModel.size)
        val legacyState by collectLastValue(viewModel.legacyState)
        val confirmationRequired by collectLastValue(viewModel.isConfirmationRequired)

        if (testCase.isCoex && testCase.authenticatedByFingerprint) {
            viewModel.ensureFingerprintHasStarted(isDelayed = true)
@@ -507,7 +508,11 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
        viewModel.showHelp(helpMessage)

        assertThat(size).isEqualTo(PromptSize.MEDIUM)
        if (confirmationRequired == true) {
            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION)
        } else {
            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED)
        }
        assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
        assertThat(messageVisible).isTrue()
        assertThat(authenticating).isFalse()