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

Commit 962fc34d authored by Austin Delgado's avatar Austin Delgado
Browse files

Allow SFPS auth to continue running after face auth

Continue running sfps authentication in Biometric prompt after face auth
succeeds and confirmation is required.

Flag: EXEMPT bugfix
Test: atest AuthSessionTest
Bug: 334942806
Change-Id: I493c0dd96878f3d7d4428aefe9ad56ddab65a422
parent c4252906
Loading
Loading
Loading
Loading
+10 −9
Original line number Diff line number Diff line
@@ -457,17 +457,10 @@ object BiometricViewBinder {

                // Retry and confirmation when finger on sensor
                launch {
                    combine(
                            viewModel.canTryAgainNow,
                            viewModel.hasFingerOnSensor,
                            viewModel.isPendingConfirmation,
                            ::Triple
                        )
                        .collect { (canRetry, fingerAcquired, pendingConfirmation) ->
                    combine(viewModel.canTryAgainNow, viewModel.hasFingerOnSensor, ::Pair)
                        .collect { (canRetry, fingerAcquired) ->
                            if (canRetry && fingerAcquired) {
                                legacyCallback.onButtonTryAgain()
                            } else if (pendingConfirmation && fingerAcquired) {
                                viewModel.confirmAuthenticated()
                            }
                        }
                }
@@ -497,13 +490,21 @@ class Spaghetti(
    @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView")
    interface Callback {
        fun onAuthenticated()

        fun onUserCanceled()

        fun onButtonNegative()

        fun onButtonTryAgain()

        fun onContentViewMoreOptionsButtonPressed()

        fun onError()

        fun onUseDeviceCredential()

        fun onStartDelayedFingerprintSensor()

        fun onAuthenticatedAndConfirmed()
    }

+13 −2
Original line number Diff line number Diff line
@@ -230,7 +230,6 @@ constructor(
    val fingerprintStartMode: Flow<FingerprintStartMode> = _fingerprintStartMode.asStateFlow()

    /** Whether a finger has been acquired by the sensor */
    // TODO(b/331948073): Add support for detecting SFPS finger without authentication running
    val hasFingerBeenAcquired: Flow<Boolean> =
        combine(biometricStatusInteractor.fingerprintAcquiredStatus, modalities) {
                status,
@@ -569,7 +568,8 @@ constructor(
        }

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

    /** If the negative button should be shown. */
    val isNegativeButtonVisible: Flow<Boolean> =
@@ -652,6 +652,9 @@ constructor(
        failedModality: BiometricModality = BiometricModality.None,
    ) = coroutineScope {
        if (_isAuthenticated.value.isAuthenticated) {
            if (_isAuthenticated.value.needsUserConfirmation && hapticFeedback) {
                vibrateOnError()
            }
            return@coroutineScope
        }

@@ -775,6 +778,14 @@ constructor(
        helpMessage: String = "",
    ) {
        if (_isAuthenticated.value.isAuthenticated) {
            // Treat second authentication with a different modality as confirmation for the first
            if (
                _isAuthenticated.value.needsUserConfirmation &&
                    modality != _isAuthenticated.value.authenticatedModality
            ) {
                confirmAuthenticated()
                return
            }
            // TODO(jbolinger): convert to go/tex-apc?
            Log.w(TAG, "Cannot show authenticated after authenticated")
            return
+55 −1
Original line number Diff line number Diff line
@@ -823,6 +823,28 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
        assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.NO_HAPTICS)
    }

    @Test
    fun plays_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest {
        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
        viewModel.showAuthenticated(testCase.authenticatedModality, 0)

        viewModel.showTemporaryError(
            "still sad",
            messageAfterError = "",
            authenticateAfterError = false,
            hapticFeedback = true,
        )

        val haptics by collectLastValue(viewModel.hapticsToPlay)
        if (expectConfirmation) {
            assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.REJECT)
            assertThat(haptics?.flag).isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
        } else {
            assertThat(haptics?.hapticFeedbackConstant)
                .isEqualTo(HapticFeedbackConstants.CONFIRM)
        }
    }

    private suspend fun TestScope.showTemporaryErrors(
        restart: Boolean,
        helpAfterError: String = "",
@@ -970,7 +992,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
    }

    @Test
    fun authenticated_at_most_once() = runGenericTest {
    fun authenticated_at_most_once_same_modality() = runGenericTest {
        val authenticating by collectLastValue(viewModel.isAuthenticating)
        val authenticated by collectLastValue(viewModel.isAuthenticated)

@@ -1031,6 +1053,38 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
        assertThat(canTryAgain).isFalse()
    }

    @Test
    fun second_authentication_acts_as_confirmation() = runGenericTest {
        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)

        viewModel.showAuthenticated(testCase.authenticatedModality, 0)

        val authenticating by collectLastValue(viewModel.isAuthenticating)
        val authenticated by collectLastValue(viewModel.isAuthenticated)
        val message by collectLastValue(viewModel.message)
        val size by collectLastValue(viewModel.size)
        val canTryAgain by collectLastValue(viewModel.canTryAgainNow)

        assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
        if (expectConfirmation) {
            assertThat(size).isEqualTo(PromptSize.MEDIUM)
            assertButtonsVisible(
                cancel = true,
                confirm = true,
            )

            if (testCase.modalities.hasSfps) {
                viewModel.showAuthenticated(BiometricModality.Fingerprint, 0)
                assertThat(message).isEqualTo(PromptMessage.Empty)
                assertButtonsVisible()
            }
        }

        assertThat(authenticating).isFalse()
        assertThat(authenticated?.isAuthenticated).isTrue()
        assertThat(canTryAgain).isFalse()
    }

    @Test
    fun auto_confirm_authentication_when_finger_down() = runGenericTest {
        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+45 −12
Original line number Diff line number Diff line
@@ -108,6 +108,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    }

    private final Context mContext;
    private final BiometricManager mBiometricManager;
    @NonNull private final BiometricContext mBiometricContext;
    private final IStatusBarService mStatusBarService;
    @VisibleForTesting final IBiometricSysuiReceiver mSysuiReceiver;
@@ -131,6 +132,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    private final String mOpPackageName;
    private final boolean mDebugEnabled;
    private final List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
    private final List<Integer> mSfpsSensorIds;

    // The current state, which can be either idle, called, or started
    private @SessionState int mState = STATE_AUTH_IDLE;
@@ -220,6 +222,11 @@ public final class AuthSession implements IBinder.DeathRecipient {
        mCancelled = false;
        mBiometricFrameworkStatsLogger = logger;
        mOperationContext = new OperationContextExt(true /* isBP */);
        mBiometricManager = mContext.getSystemService(BiometricManager.class);

        mSfpsSensorIds = mFingerprintSensorProperties.stream().filter(
                FingerprintSensorPropertiesInternal::isAnySidefpsType).map(
                    prop -> prop.sensorId).toList();

        try {
            mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);
@@ -316,7 +323,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
            Slog.w(TAG, "Received cookie but already cancelled (ignoring): " + cookie);
            return;
        }
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onCookieReceived after successful auth");
            return;
        }
@@ -494,6 +501,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
            }

            case STATE_AUTH_STARTED:
            case STATE_AUTH_PENDING_CONFIRM:
            case STATE_AUTH_STARTED_UI_SHOWING: {
                if (isAllowDeviceCredential() && errorLockout) {
                    // SystemUI handles transition from biometric to device credential.
@@ -539,7 +547,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    }

    void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onAcquired after successful auth");
            return;
        }
@@ -562,7 +570,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    }

    void onSystemEvent(int event) {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onSystemEvent after successful auth");
            return;
        }
@@ -579,12 +587,15 @@ public final class AuthSession implements IBinder.DeathRecipient {

    void onDialogAnimatedIn(boolean startFingerprintNow) {
        if (mState != STATE_AUTH_STARTED && mState != STATE_ERROR_PENDING_SYSUI
                && mState != STATE_AUTH_PAUSED) {
                && mState != STATE_AUTH_PAUSED && mState != STATE_AUTH_PENDING_CONFIRM) {
            Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
            return;
        }

        if (mState != STATE_AUTH_PENDING_CONFIRM) {
            mState = STATE_AUTH_STARTED_UI_SHOWING;
        }

        if (startFingerprintNow) {
            startAllPreparedFingerprintSensors();
        } else {
@@ -600,6 +611,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
        if (mState != STATE_AUTH_STARTED
                && mState != STATE_AUTH_STARTED_UI_SHOWING
                && mState != STATE_AUTH_PAUSED
                && mState != STATE_AUTH_PENDING_CONFIRM
                && mState != STATE_ERROR_PENDING_SYSUI) {
            Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
        }
@@ -608,7 +620,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    }

    void onTryAgainPressed() {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onTryAgainPressed after successful auth");
            return;
        }
@@ -625,7 +637,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    }

    void onAuthenticationSucceeded(int sensorId, boolean strong, byte[] token) {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onAuthenticationSucceeded after successful auth");
            return;
        }
@@ -656,11 +668,17 @@ public final class AuthSession implements IBinder.DeathRecipient {
            Slog.e(TAG, "RemoteException", e);
        }

        if (mState == STATE_AUTH_PENDING_CONFIRM) {
            // Do not cancel Sfps sensors so auth can continue running
            cancelAllSensors(
                    sensor -> sensor.id != sensorId && !mSfpsSensorIds.contains(sensor.id));
        } else {
            cancelAllSensors(sensor -> sensor.id != sensorId);
        }
    }

    void onAuthenticationRejected(int sensorId) {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onAuthenticationRejected after successful auth");
            return;
        }
@@ -678,7 +696,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    }

    void onAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onAuthenticationTimedOut after successful auth");
            return;
        }
@@ -703,7 +721,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    }

    void onDeviceCredentialPressed() {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onDeviceCredentialPressed after successful auth");
            return;
        }
@@ -739,6 +757,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
        return mAuthenticatedSensorId != -1;
    }

    private boolean hasAuthenticatedAndConfirmed() {
        return mAuthenticatedSensorId != -1 && mState == STATE_AUTHENTICATED_PENDING_SYSUI;
    }

    private void logOnDialogDismissed(@BiometricPrompt.DismissedReason int reason) {
        if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) {
            // Explicit auth, authentication confirmed.
@@ -828,6 +850,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
                    } else {
                        Slog.e(TAG, "mTokenEscrow is null");
                    }

                    mClientReceiver.onAuthenticationSucceeded(
                            Utils.getAuthenticationTypeForResult(reason));
                    break;
@@ -861,6 +884,16 @@ public final class AuthSession implements IBinder.DeathRecipient {
        } catch (RemoteException e) {
            Slog.e(TAG, "Remote exception", e);
        } finally {
            if (mTokenEscrow != null && mBiometricManager != null) {
                final byte[] byteToken = new byte[mTokenEscrow.length];
                for (int i = 0; i < mTokenEscrow.length; i++) {
                    byteToken[i] = mTokenEscrow[i];
                }
                mBiometricManager.resetLockoutTimeBound(mToken,
                        mContext.getOpPackageName(),
                        mAuthenticatedSensorId, mUserId, byteToken);
            }

            // ensure everything is cleaned up when dismissed
            cancelAllSensors();
        }
@@ -874,7 +907,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
     * @return true if this AuthSession is finished, e.g. should be set to null
     */
    boolean onCancelAuthSession(boolean force) {
        if (hasAuthenticated()) {
        if (hasAuthenticatedAndConfirmed()) {
            Slog.d(TAG, "onCancelAuthSession after successful auth");
            return true;
        }
+7 −7
Original line number Diff line number Diff line
@@ -242,14 +242,14 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions>
                byteToken[i] = hardwareAuthToken.get(i);
            }

            // For BP, BiometricService will add the authToken to Keystore.
            if (!isBiometricPrompt()) {
                if (mIsStrongBiometric) {
                    mBiometricManager.resetLockoutTimeBound(getToken(),
                            getContext().getOpPackageName(),
                            getSensorId(), getTargetUserId(), byteToken);
                }

            // For BP, BiometricService will add the authToken to Keystore.
            if (!isBiometricPrompt() && mIsStrongBiometric) {
                final int result = KeyStoreAuthorization.getInstance().addAuthToken(byteToken);
                if (result != 0) {
                    Slog.d(TAG, "Error adding auth token : " + result);
Loading