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

Commit 5a456fff authored by Joe Bolinger's avatar Joe Bolinger Committed by Android (Google) Code Review
Browse files

Merge "Fallback to fingerprint on all error types." into sc-dev

parents 0c1b382a f45b0ff0
Loading
Loading
Loading
Loading
+16 −6
Original line number Diff line number Diff line
@@ -130,19 +130,29 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
    @Override
    public void onAuthenticationFailed(
            @Modality int modality, @Nullable String failureReason) {
        if (modality == TYPE_FACE && mActiveSensorType == TYPE_FACE) {
            // switching from face -> fingerprint mode, suppress soft error messages
            failureReason = mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead);
        super.onAuthenticationFailed(modality, checkErrorForFallback(failureReason));
    }
        super.onAuthenticationFailed(modality, failureReason);

    @Override
    public void onError(int modality, String error) {
        super.onError(modality, checkErrorForFallback(error));
    }

    private String checkErrorForFallback(String message) {
        if (mActiveSensorType == TYPE_FACE) {
            Log.d(TAG, "Falling back to fingerprint: " + message);

            // switching from face -> fingerprint mode, suppress root error messages
            mCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR);
            return mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead);
        }
        return message;
    }

    @Override
    @BiometricState
    protected int getStateForAfterError() {
        if (mActiveSensorType == TYPE_FACE) {
            mHandler.post(() -> mCallback.onAction(
                    Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
            return STATE_AUTHENTICATING;
        }

+37 −1
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;

import android.content.Context;
@@ -124,16 +126,50 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
    }

    @Test
    public void testModeUpdated_whenSwitchToFingerprint() {
    public void testModeUpdated_onSoftError_whenSwitchToFingerprint() {
        mFaceToFpView.onDialogAnimatedIn();
        mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face");
        waitForIdleSync();

        verify(mIndicatorView).setText(
                eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
        verify(mCallback).onAction(
                eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
        assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING, mFaceToFpView.mState);
    }

    @Test
    public void testModeUpdated_onHardError_whenSwitchToFingerprint() {
        mFaceToFpView.onDialogAnimatedIn();
        mFaceToFpView.onError(TYPE_FACE, "oh no!");
        waitForIdleSync();

        verify(mIndicatorView).setText(
                eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
        verify(mCallback).onAction(
                eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
        assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING, mFaceToFpView.mState);
    }

    @Test
    public void testFingerprintOnlyStartsOnFirstError() {
        mFaceToFpView.onDialogAnimatedIn();
        verify(mFaceToFpView.mIconController)
                .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));

        mFaceToFpView.onDialogAnimatedIn();
        mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR);
        mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING);

        reset(mCallback);

        mFaceToFpView.onError(TYPE_FACE, "oh no!");
        mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face");

        verify(mCallback, never()).onAction(
                eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
    }

    public class TestableView extends AuthBiometricFaceToFingerprintView {
        public TestableView(Context context) {
            super(context, null, new MockInjector());
+14 −36
Original line number Diff line number Diff line
@@ -355,10 +355,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
        }
    }

    private void cancelAllFingerprintSensors() {
        cancelAllSensors(sensor -> sensor.modality == TYPE_FINGERPRINT);
    }

    private void cancelAllSensors() {
        cancelAllSensors(sensor -> true);
    }
@@ -387,6 +383,9 @@ public final class AuthSession implements IBinder.DeathRecipient {
     */
    boolean onErrorReceived(int sensorId, int cookie, @BiometricConstants.Errors int error,
            int vendorCode) throws RemoteException {
        if (DEBUG) {
            Slog.v(TAG, "onErrorReceived sensor: " + sensorId + " error: " + error);
        }

        if (!containsCookie(cookie)) {
            Slog.e(TAG, "Unknown/expired cookie: " + cookie);
@@ -454,12 +453,14 @@ public final class AuthSession implements IBinder.DeathRecipient {
                    // a round trip to SystemUI.
                    mClientReceiver.onError(modality, error, vendorCode);
                    return true;
                } else if (shouldErrorTriggerMultiSensorTransition()) {
                    // wait for the UI to signal when modality should switch
                    mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING;
                    Slog.d(TAG, "onErrorReceived: waiting for modality switch callback");
                } else {
                    mState = STATE_ERROR_PENDING_SYSUI;
                    if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT
                            && mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING) {
                        // wait for the UI to signal when modality should switch
                        Slog.d(TAG, "onErrorReceived: waiting for modality switch callback");
                        mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING;
                    }
                    mStatusBarService.onBiometricError(modality, error, vendorCode);
                }
                break;
@@ -627,7 +628,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
    void onDeviceCredentialPressed() {
        // Cancel authentication. Skip the token/package check since we are cancelling
        // from system server. The interface is permission protected so this is fine.
        cancelBiometricOnly();
        cancelAllSensors();
        mState = STATE_SHOWING_DEVICE_CREDENTIAL;
    }

@@ -733,12 +734,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
                    }
                    mClientReceiver.onAuthenticationSucceeded(
                            Utils.getAuthenticationTypeForResult(reason));
                    cancelBiometricOnly();
                    break;

                case BiometricPrompt.DISMISSED_REASON_NEGATIVE:
                    mClientReceiver.onDialogDismissed(reason);
                    cancelBiometricOnly();
                    break;

                case BiometricPrompt.DISMISSED_REASON_USER_CANCEL:
@@ -747,7 +746,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
                            BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED,
                            0 /* vendorCode */
                    );
                    cancelBiometricOnly();
                    break;

                case BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED:
@@ -765,6 +763,9 @@ public final class AuthSession implements IBinder.DeathRecipient {
            }
        } catch (RemoteException e) {
            Slog.e(TAG, "Remote exception", e);
        } finally {
            // ensure everything is cleaned up when dismissed
            cancelAllSensors();
        }
    }

@@ -780,8 +781,8 @@ public final class AuthSession implements IBinder.DeathRecipient {
                || mState == STATE_AUTH_STARTED
                || mState == STATE_AUTH_STARTED_UI_SHOWING;

        if (authStarted && !force) {
        cancelAllSensors();
        if (authStarted && !force) {
            // Wait for ERROR_CANCELED to be returned from the sensors
            return false;
        } else {
@@ -804,22 +805,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
        return false;
    }

    /**
     * Cancels biometric authentication only. AuthSession may either be going into
     * {@link #STATE_SHOWING_DEVICE_CREDENTIAL} or dismissed.
     */
    private void cancelBiometricOnly() {
        if (mState == STATE_AUTH_STARTED || mState == STATE_AUTH_STARTED_UI_SHOWING) {
            cancelAllSensors();
        } else if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
            cancelAllFingerprintSensors();
        } else {
            if (DEBUG)  {
                Slog.v(TAG, "nothing to cancel - wrong state: " + mState);
            }
        }
    }

    boolean isCrypto() {
        return mOperationId != 0;
    }
@@ -896,13 +881,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
        }
    }

    private boolean shouldErrorTriggerMultiSensorTransition() {
        if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
            return mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING;
        }
        return false;
    }

    @BiometricMultiSensorMode
    private static int getMultiSensorModeForNewSession(Collection<BiometricSensor> sensors) {
        boolean hasFace = false;
+19 −5
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.biometrics;

import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE;

import static com.android.server.biometrics.BiometricServiceStateProto.*;

@@ -38,7 +39,6 @@ import android.annotation.NonNull;
import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustManager;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager.Authenticators;
import android.hardware.biometrics.ComponentInfoInternal;
import android.hardware.biometrics.IBiometricAuthenticator;
@@ -67,6 +67,7 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;

@Presubmit
@SmallTest
@@ -245,9 +246,6 @@ public class AuthSessionTest {
            assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
            assertEquals(BiometricSensor.STATE_COOKIE_RETURNED,
                    session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
            session.onErrorReceived(fingerprintSensorId,
                    session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getCookie(),
                    BiometricConstants.BIOMETRIC_ERROR_VENDOR, 0 /* vendorCode */);
            session.onStartFingerprint();
        }
        assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
@@ -258,6 +256,21 @@ public class AuthSessionTest {
    @Test
    public void testCancelAuthentication_whenStateAuthCalled_invokesCancel()
            throws RemoteException {
        testInvokesCancel(session -> session.onCancelAuthSession(false /* force */));
    }

    @Test
    public void testCancelAuthentication_whenStateAuthForcedCalled_invokesCancel()
            throws RemoteException {
        testInvokesCancel(session -> session.onCancelAuthSession(true /* force */));
    }

    @Test
    public void testCancelAuthentication_whenDialogDismissed() throws RemoteException {
        testInvokesCancel(session -> session.onDialogDismissed(DISMISSED_REASON_NEGATIVE, null));
    }

    private void testInvokesCancel(Consumer<AuthSession> sessionConsumer) throws RemoteException {
        final IBiometricAuthenticator faceAuthenticator = mock(IBiometricAuthenticator.class);

        setupFace(0 /* id */, false /* confirmationAlwaysRequired */, faceAuthenticator);
@@ -269,7 +282,8 @@ public class AuthSessionTest {

        session.goToInitialState();
        assertEquals(STATE_AUTH_CALLED, session.getState());
        session.onCancelAuthSession(false /* force */);

        sessionConsumer.accept(session);

        verify(faceAuthenticator).cancelAuthenticationFromService(eq(mToken), eq(TEST_PACKAGE));
    }
+8 −15
Original line number Diff line number Diff line
@@ -1036,7 +1036,7 @@ public class BiometricServiceTest {
    }

    @Test
    public void testDismissedReasonNegative_whilePaused_doesntInvokeHalCancel() throws Exception {
    public void testDismissedReasonNegative_whilePaused_invokeHalCancel() throws Exception {
        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
        invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                false /* requireConfirmation */, null /* authenticators */);
@@ -1050,14 +1050,12 @@ public class BiometricServiceTest {
                BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */);
        waitForIdle();

        verify(mBiometricService.mSensors.get(0).impl,
                never()).cancelAuthenticationFromService(
                any(),
                any());
        verify(mBiometricService.mSensors.get(0).impl)
                .cancelAuthenticationFromService(any(), any());
    }

    @Test
    public void testDismissedReasonUserCancel_whilePaused_doesntInvokeHalCancel() throws
    public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws
            Exception {
        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
        invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
@@ -1072,10 +1070,8 @@ public class BiometricServiceTest {
                BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
        waitForIdle();

        verify(mBiometricService.mSensors.get(0).impl,
                never()).cancelAuthenticationFromService(
                any(),
                any());
        verify(mBiometricService.mSensors.get(0).impl)
                .cancelAuthenticationFromService(any(), any());
    }

    @Test
@@ -1091,11 +1087,8 @@ public class BiometricServiceTest {
                BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
        waitForIdle();

        // doesn't send cancel to HAL
        verify(mBiometricService.mSensors.get(0).impl,
                never()).cancelAuthenticationFromService(
                any(),
                any());
        verify(mBiometricService.mSensors.get(0).impl)
                .cancelAuthenticationFromService(any(), any());
        verify(mReceiver1).onError(
                eq(BiometricAuthenticator.TYPE_FACE),
                eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),