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

Commit 8c27e631 authored by Kevin Chyn's avatar Kevin Chyn Committed by Automerger Merge Worker
Browse files

Merge "3/n: Add CoexCoordinator" into sc-dev am: 6cb9ecad am: 56180f60

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15305872

Change-Id: I5daea19bb9e04984ecb6aeab6f70d4becb9a0ac6
parents 187536ab 56180f60
Loading
Loading
Loading
Loading
+122 −103
Original line number Original line Diff line number Diff line
@@ -138,7 +138,6 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>


        final ClientMonitorCallbackConverter listener = getListener();
        final ClientMonitorCallbackConverter listener = getListener();


        try {
        if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")"
        if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")"
                + ", ID:" + identifier.getBiometricId()
                + ", ID:" + identifier.getBiometricId()
                + ", Owner: " + getOwnerString()
                + ", Owner: " + getOwnerString()
@@ -206,10 +205,6 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>


            mAlreadyDone = true;
            mAlreadyDone = true;


                if (listener != null && mShouldVibrate) {
                    vibrateSuccess();
                }

            if (mTaskStackListener != null) {
            if (mTaskStackListener != null) {
                mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
                mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
            }
            }
@@ -225,54 +220,78 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
                        getSensorId(), getTargetUserId(), byteToken);
                        getSensorId(), getTargetUserId(), byteToken);
            }
            }


                if (isBiometricPrompt() && listener != null) {
            final CoexCoordinator coordinator = CoexCoordinator.getInstance();
                    // BiometricService will add the token to keystore
            coordinator.onAuthenticationSucceeded(this, new CoexCoordinator.Callback() {
                    listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
                @Override
                            getTargetUserId(), mIsStrongBiometric);
                public void sendAuthenticationResult(boolean addAuthTokenIfStrong) {
                } else if (!isBiometricPrompt() && listener != null) {
                    if (addAuthTokenIfStrong && mIsStrongBiometric) {
                    if (mIsStrongBiometric) {
                        final int result = KeyStore.getInstance().addAuthToken(byteToken);
                        final int result = KeyStore.getInstance().addAuthToken(byteToken);
                        Slog.d(TAG, "addAuthToken: " + result);
                        Slog.d(TAG, "addAuthToken: " + result);
                    } else {
                    } else {
                        Slog.d(TAG, "Skipping addAuthToken");
                        Slog.d(TAG, "Skipping addAuthToken");
                    }
                    }


                    // Explicitly have if/else here to make it super obvious in case the code is
                    if (listener != null) {
                    // touched in the future.
                        try {
                            // Explicitly have if/else here to make it super obvious in case the
                            // code is touched in the future.
                            if (!mIsRestricted) {
                            if (!mIsRestricted) {
                        listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
                                listener.onAuthenticationSucceeded(getSensorId(),
                                getTargetUserId(), mIsStrongBiometric);
                                        identifier,
                                        byteToken,
                                        getTargetUserId(),
                                        mIsStrongBiometric);
                            } else {
                            } else {
                        listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
                                listener.onAuthenticationSucceeded(getSensorId(),
                                byteToken, getTargetUserId(), mIsStrongBiometric);
                                        null /* identifier */,
                                        byteToken,
                                        getTargetUserId(),
                                        mIsStrongBiometric);
                            }
                        } catch (RemoteException e) {
                            Slog.e(TAG, "Unable to notify listener", e);
                        }
                        }

                    } else {
                    } else {
                    // Client not listening
                        Slog.w(TAG, "Client not listening");
                        Slog.w(TAG, "Client not listening");
                    }
                    }
            } else {
                if (listener != null && mShouldVibrate) {
                    vibrateError();
                }
                }


                @Override
                public void sendHapticFeedback() {
                    if (listener != null && mShouldVibrate) {
                        vibrateSuccess();
                    }
                }
            });
        } else {
            // Allow system-defined limit of number of attempts before giving up
            // Allow system-defined limit of number of attempts before giving up
            final @LockoutTracker.LockoutMode int lockoutMode =
            final @LockoutTracker.LockoutMode int lockoutMode =
                    handleFailedAttempt(getTargetUserId());
                    handleFailedAttempt(getTargetUserId());
                if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
            if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {
                    // Don't send onAuthenticationFailed if we're in lockout, it causes a
                mAlreadyDone = true;
                    // janky UI on Keyguard/BiometricPrompt since "authentication failed"
            }
                    // will show briefly and be replaced by "device locked out" message.

            final CoexCoordinator coordinator = CoexCoordinator.getInstance();
            coordinator.onAuthenticationRejected(this, lockoutMode,
                    new CoexCoordinator.Callback() {
                @Override
                public void sendAuthenticationResult(boolean addAuthTokenIfStrong) {
                    if (listener != null) {
                    if (listener != null) {
                        try {
                            listener.onAuthenticationFailed(getSensorId());
                            listener.onAuthenticationFailed(getSensorId());
                        } catch (RemoteException e) {
                            Slog.e(TAG, "Unable to notify listener", e);
                        }
                        }
                } else {
                    mAlreadyDone = true;
                    }
                    }
                }
                }
        } catch (RemoteException e) {

            Slog.e(TAG, "Unable to notify listener, finishing", e);
                @Override
            mCallback.onClientFinished(this, false /* success */);
                public void sendHapticFeedback() {
                    if (listener != null && mShouldVibrate) {
                        vibrateError();
                    }
                }
            });
        }
        }
    }
    }


+86 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2021 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.server.biometrics.sensors;

import android.annotation.NonNull;

/**
 * Singleton that contains the core logic for determining if haptics and authentication callbacks
 * should be sent to receivers. Note that this class is used even when coex is not required (e.g.
 * single sensor devices, or multi-sensor devices where only a single sensor is authenticating).
 * This allows us to have all business logic in one testable place.
 */
public class CoexCoordinator {

    private static final String TAG = "BiometricCoexCoordinator";

    /**
     * Callback interface notifying the owner of "results" from the CoexCoordinator's business
     * logic.
     */
    interface Callback {
        /**
         * Requests the owner to send the result (success/reject) and any associated info to the
         * receiver (e.g. keyguard, BiometricService, etc).
         */
        void sendAuthenticationResult(boolean addAuthTokenIfStrong);

        /**
         * Requests the owner to initiate a vibration for this event.
         */
        void sendHapticFeedback();
    }

    private static CoexCoordinator sInstance;

    private CoexCoordinator() {
        // Singleton
    }

    @NonNull
    static CoexCoordinator getInstance() {
        if (sInstance == null) {
            sInstance = new CoexCoordinator();
        }
        return sInstance;
    }

    public void onAuthenticationSucceeded(@NonNull AuthenticationClient<?> client,
            @NonNull Callback callback) {
        if (client.isBiometricPrompt()) {
            callback.sendHapticFeedback();
            // For BP, BiometricService will add the authToken to Keystore.
            callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
        } else {
            // Keyguard, FingerprintManager, FaceManager, etc
            callback.sendHapticFeedback();
            callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
        }
    }

    public void onAuthenticationRejected(@NonNull AuthenticationClient<?> client,
            @LockoutTracker.LockoutMode int lockoutMode,
            @NonNull Callback callback) {
        callback.sendHapticFeedback();
        if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
            // Don't send onAuthenticationFailed if we're in lockout, it causes a
            // janky UI on Keyguard/BiometricPrompt since "authentication failed"
            // will show briefly and be replaced by "device locked out" message.
            callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
        }
    }
}
+79 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2021 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.server.biometrics.sensors;

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.platform.test.annotations.Presubmit;

import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@Presubmit
@SmallTest
public class CoexCoordinatorTest {

    private CoexCoordinator mCoexCoordinator;

    @Mock
    private CoexCoordinator.Callback mCallback;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mCoexCoordinator = CoexCoordinator.getInstance();
    }

    @Test
    public void testBiometricPrompt_authSuccess() {
        AuthenticationClient<?> client = mock(AuthenticationClient.class);
        when(client.isBiometricPrompt()).thenReturn(true);

        mCoexCoordinator.onAuthenticationSucceeded(client, mCallback);
        verify(mCallback).sendHapticFeedback();
        verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
    }

    @Test
    public void testBiometricPrompt_authReject_whenNotLockedOut() {
        AuthenticationClient<?> client = mock(AuthenticationClient.class);
        when(client.isBiometricPrompt()).thenReturn(true);

        mCoexCoordinator.onAuthenticationRejected(client, LockoutTracker.LOCKOUT_NONE, mCallback);
        verify(mCallback).sendHapticFeedback();
        verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
    }

    @Test
    public void testBiometricPrompt_authReject_whenLockedOut() {
        AuthenticationClient<?> client = mock(AuthenticationClient.class);
        when(client.isBiometricPrompt()).thenReturn(true);

        mCoexCoordinator.onAuthenticationRejected(client, LockoutTracker.LOCKOUT_TIMED, mCallback);
        verify(mCallback).sendHapticFeedback();
        verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
    }
}