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

Commit 3cd9d71c authored by Kevin Chyn's avatar Kevin Chyn
Browse files

7/n: Move Fingerprint/Face AuthenticationClients to their own file

Makes the AuthenticationClient subclasses completely modular, helping
us move to a more flexible/extendable biometric system server. This
change starts to separate modality-specific details of each
AuthenticationClient subclass (e.g. framework lockout tracking for
fingerprint, authentication stats for face). Further separation of
modality-specific details depend upon further cleanup and will be done
in a separate CL.

Bug: 157790417
Test: lockout on fingerprint/face devices and various combinations
      of auth/reject
Test: Keyguard, BiometricPromptDemo auth

Change-Id: I39ba0653f9c4a1449592fcb47c05d5c670aaf8c2
parent d1da1ecf
Loading
Loading
Loading
Loading
+9 −56
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.biometrics.sensors;

import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.TaskStackListener;
import android.content.Context;
@@ -37,11 +38,8 @@ import java.util.ArrayList;
 */
public abstract class AuthenticationClient extends ClientMonitor {

    public void resetFailedAttempts(int userId) {}

    private final boolean mIsStrongBiometric;
    private final long mOpId;
    private final boolean mShouldFrameworkHandleLockout;
    private final boolean mRequireConfirmation;
    private final IActivityTaskManager mActivityTaskManager;
    private final TaskStackListener mTaskStackListener;
@@ -49,16 +47,12 @@ public abstract class AuthenticationClient extends ClientMonitor {
    private final LockoutTracker mLockoutTracker;
    private final Surface mSurface;

    // We need to track this state since it's possible for applications to request for
    // authentication while the device is already locked out. In that case, the client is created
    // but not started yet. The user shouldn't receive the error haptics in this case.
    private boolean mStarted;
    private long mStartTimeMs;

    /**
     * This method is called when authentication starts.
     */
    private void onStart() {
    protected void onStart() {
        try {
            mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
        } catch (RemoteException e) {
@@ -70,7 +64,7 @@ public abstract class AuthenticationClient extends ClientMonitor {
     * This method is called when a biometric is authenticated or authentication is stopped
     * (cancelled by the user, or an error such as lockout has occurred).
     */
    private void onStop() {
    protected void onStop() {
        try {
            mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
        } catch (RemoteException e) {
@@ -78,26 +72,21 @@ public abstract class AuthenticationClient extends ClientMonitor {
        }
    }

    public abstract boolean wasUserDetected();

    public AuthenticationClient(Context context, Constants constants,
            BiometricServiceBase.DaemonWrapper daemon, IBinder token,
            ClientMonitorCallbackConverter listener, int targetUserId, int groupId, long opId,
            boolean shouldFrameworkHandleLockout, boolean restricted, String owner, int cookie,
            boolean requireConfirmation, int sensorId, boolean isStrongBiometric, int statsModality,
            int statsClient, IActivityTaskManager activityTaskManager,
            TaskStackListener taskStackListener, PowerManager powerManager,
            LockoutTracker lockoutTracker, Surface surface) {
            boolean restricted, String owner, int cookie, boolean requireConfirmation, int sensorId,
            boolean isStrongBiometric, int statsModality, int statsClient,
            TaskStackListener taskStackListener, LockoutTracker lockoutTracker, Surface surface) {
        super(context, constants, daemon, token, listener, targetUserId, groupId,
                restricted, owner, cookie, sensorId, statsModality,
                BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient);
        mIsStrongBiometric = isStrongBiometric;
        mOpId = opId;
        mShouldFrameworkHandleLockout = shouldFrameworkHandleLockout;
        mRequireConfirmation = requireConfirmation;
        mActivityTaskManager = activityTaskManager;
        mActivityTaskManager = ActivityTaskManager.getService();
        mTaskStackListener = taskStackListener;
        mPowerManager = powerManager;
        mPowerManager = context.getSystemService(PowerManager.class);
        mLockoutTracker = lockoutTracker;
        mSurface = surface;
    }
@@ -142,28 +131,6 @@ public abstract class AuthenticationClient extends ClientMonitor {
        return mOpId != 0;
    }

    @Override
    public boolean onError(int error, int vendorCode) {
        if (!mShouldFrameworkHandleLockout) {
            switch (error) {
                case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT:
                    if (!wasUserDetected() && !isBiometricPrompt()) {
                        // No vibration if user was not detected on keyguard
                        break;
                    }
                case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT:
                case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
                    if (mStarted) {
                        vibrateError();
                    }
                    break;
                default:
                    break;
            }
        }
        return super.onError(error, vendorCode);
    }

    @Override
    public boolean onAuthenticated(BiometricAuthenticator.Identifier identifier,
            boolean authenticated, ArrayList<Byte> token) {
@@ -191,9 +158,6 @@ public abstract class AuthenticationClient extends ClientMonitor {
                    vibrateSuccess();
                }
                result = true;
                if (mShouldFrameworkHandleLockout) {
                    resetFailedAttempts(getTargetUserId());
                }
                onStop();

                final byte[] byteToken = new byte[token.size()];
@@ -237,15 +201,7 @@ public abstract class AuthenticationClient extends ClientMonitor {
                // Allow system-defined limit of number of attempts before giving up
                final @LockoutTracker.LockoutMode int lockoutMode =
                        handleFailedAttempt(getTargetUserId());
                if (lockoutMode != LockoutTracker.LOCKOUT_NONE && mShouldFrameworkHandleLockout) {
                    Slog.w(getLogTag(), "Forcing lockout (driver code should do this!), mode("
                            + lockoutMode + ")");
                    stop(false);
                    final int errorCode = lockoutMode == LockoutTracker.LOCKOUT_TIMED
                            ? BiometricConstants.BIOMETRIC_ERROR_LOCKOUT
                            : BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
                    onError(errorCode, 0 /* vendorCode */);
                } else {
                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.
@@ -267,7 +223,6 @@ public abstract class AuthenticationClient extends ClientMonitor {
     */
    @Override
    public int start() {
        mStarted = true;
        onStart();
        try {
            mStartTimeMs = System.currentTimeMillis();
@@ -293,8 +248,6 @@ public abstract class AuthenticationClient extends ClientMonitor {
            return 0;
        }

        mStarted = false;

        onStop();

        try {
+1 −1
Original line number Diff line number Diff line
@@ -198,7 +198,7 @@ public abstract class BiometricServiceBase extends SystemService
     * Wraps a portion of the interface from Service -> Daemon that is used by the ClientMonitor
     * subclasses.
     */
    protected interface DaemonWrapper {
    public interface DaemonWrapper {
        int ERROR_ESRCH = 3; // Likely HAL is dead. see errno.h.
        int authenticate(long operationId, int groupId, Surface surface)
                throws RemoteException;
+210 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.face;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackListener;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceManager;
import android.os.IBinder;
import android.os.UserHandle;

import com.android.internal.R;
import com.android.server.biometrics.sensors.AuthenticationClient;
import com.android.server.biometrics.sensors.BiometricServiceBase;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.Constants;
import com.android.server.biometrics.sensors.LockoutTracker;

import java.util.ArrayList;

/**
 * Face-specific authentication client supporting the {@link android.hardware.biometrics.face.V1_0}
 * and {@link android.hardware.biometrics.face.V1_1} HIDL interfaces.
 */
class FaceAuthenticationClient extends AuthenticationClient {

    private final NotificationManager mNotificationManager;
    private final UsageStats mUsageStats;

    private final int[] mBiometricPromptIgnoreList;
    private final int[] mBiometricPromptIgnoreListVendor;
    private final int[] mKeyguardIgnoreList;
    private final int[] mKeyguardIgnoreListVendor;

    private int mLastAcquire;
    // We need to track this state since it's possible for applications to request for
    // authentication while the device is already locked out. In that case, the client is created
    // but not started yet. The user shouldn't receive the error haptics in this case.
    private boolean mStarted;

    FaceAuthenticationClient(Context context, Constants constants,
            BiometricServiceBase.DaemonWrapper daemon, IBinder token,
            ClientMonitorCallbackConverter listener, int targetUserId, long opId,
            boolean restricted, String owner, int cookie, boolean requireConfirmation, int sensorId,
            boolean isStrongBiometric, int statsClient, TaskStackListener taskStackListener,
            LockoutTracker lockoutTracker, UsageStats usageStats) {
        super(context, constants, daemon, token, listener, targetUserId, 0 /* groupId */, opId,
                restricted, owner, cookie, requireConfirmation, sensorId, isStrongBiometric,
                BiometricsProtoEnums.MODALITY_FACE, statsClient, taskStackListener,
                lockoutTracker, null /* surface */);
        mNotificationManager = context.getSystemService(NotificationManager.class);
        mUsageStats = usageStats;

        final Resources resources = getContext().getResources();
        mBiometricPromptIgnoreList = resources.getIntArray(
                R.array.config_face_acquire_biometricprompt_ignorelist);
        mBiometricPromptIgnoreListVendor = resources.getIntArray(
                R.array.config_face_acquire_vendor_biometricprompt_ignorelist);
        mKeyguardIgnoreList = resources.getIntArray(
                R.array.config_face_acquire_keyguard_ignorelist);
        mKeyguardIgnoreListVendor = resources.getIntArray(
                R.array.config_face_acquire_vendor_keyguard_ignorelist);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mStarted = true;
    }

    @Override
    protected void onStop() {
        super.onStop();
        mStarted = false;
    }

    private boolean wasUserDetected() {
        // Do not provide haptic feedback if the user was not detected, and an error (usually
        // ERROR_TIMEOUT) is received.
        return mLastAcquire != FaceManager.FACE_ACQUIRED_NOT_DETECTED
                && mLastAcquire != FaceManager.FACE_ACQUIRED_SENSOR_DIRTY;
    }

    @Override
    public boolean onAuthenticated(BiometricAuthenticator.Identifier identifier,
            boolean authenticated, ArrayList<Byte> token) {
        final boolean result = super.onAuthenticated(identifier, authenticated, token);

        mUsageStats.addEvent(new UsageStats.AuthenticationEvent(
                getStartTimeMs(),
                System.currentTimeMillis() - getStartTimeMs() /* latency */,
                authenticated,
                0 /* error */,
                0 /* vendorError */,
                getTargetUserId()));

        // For face, the authentication lifecycle ends either when
        // 1) Authenticated == true
        // 2) Error occurred
        // 3) Authenticated == false
        return result || !authenticated;
    }

    @Override
    public boolean onError(int error, int vendorCode) {
        mUsageStats.addEvent(new UsageStats.AuthenticationEvent(
                getStartTimeMs(),
                System.currentTimeMillis() - getStartTimeMs() /* latency */,
                false /* authenticated */,
                error,
                vendorCode,
                getTargetUserId()));

        switch (error) {
            case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT:
                if (!wasUserDetected() && !isBiometricPrompt()) {
                    // No vibration if user was not detected on keyguard
                    break;
                }
            case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT:
            case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
                if (mStarted) {
                    vibrateError();
                }
                break;
            default:
                break;
        }

        return super.onError(error, vendorCode);
    }

    @Override
    public int[] getAcquireIgnorelist() {
        return isBiometricPrompt() ? mBiometricPromptIgnoreList : mKeyguardIgnoreList;
    }

    @Override
    public int[] getAcquireVendorIgnorelist() {
        return isBiometricPrompt() ? mBiometricPromptIgnoreListVendor : mKeyguardIgnoreListVendor;
    }

    @Override
    public boolean onAcquired(int acquireInfo, int vendorCode) {

        mLastAcquire = acquireInfo;

        if (acquireInfo == FaceManager.FACE_ACQUIRED_RECALIBRATE) {
            final String name =
                    getContext().getString(R.string.face_recalibrate_notification_name);
            final String title =
                    getContext().getString(R.string.face_recalibrate_notification_title);
            final String content =
                    getContext().getString(R.string.face_recalibrate_notification_content);

            final Intent intent = new Intent("android.settings.FACE_SETTINGS");
            intent.setPackage("com.android.settings");

            final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(getContext(),
                    0 /* requestCode */, intent, 0 /* flags */, null /* options */,
                    UserHandle.CURRENT);

            final String channelName = "FaceEnrollNotificationChannel";

            NotificationChannel channel = new NotificationChannel(channelName, name,
                    NotificationManager.IMPORTANCE_HIGH);
            Notification notification = new Notification.Builder(getContext(), channelName)
                    .setSmallIcon(R.drawable.ic_lock)
                    .setContentTitle(title)
                    .setContentText(content)
                    .setSubText(name)
                    .setOnlyAlertOnce(true)
                    .setLocalOnly(true)
                    .setAutoCancel(true)
                    .setCategory(Notification.CATEGORY_SYSTEM)
                    .setContentIntent(pendingIntent)
                    .setVisibility(Notification.VISIBILITY_SECRET)
                    .build();

            mNotificationManager.createNotificationChannel(channel);
            mNotificationManager.notifyAsUser(FaceService.NOTIFICATION_TAG,
                    FaceService.NOTIFICATION_ID, notification,
                    UserHandle.CURRENT);
        }

        return super.onAcquired(acquireInfo, vendorCode);
    }
}
+15 −265

File changed.

Preview size limit exceeded, changes collapsed.

+125 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.face;

import android.content.Context;
import android.hardware.face.FaceManager;
import android.util.SparseIntArray;
import android.util.SparseLongArray;

import java.io.PrintWriter;
import java.util.ArrayDeque;

/**
 * Keep a short historical buffer of stats, with an aggregated usage time.
 */

class UsageStats {
    private static final int EVENT_LOG_SIZE = 100;

    /**
     * Events for bugreports.
     */
    public static final class AuthenticationEvent {
        private long mStartTime;
        private long mLatency;
        // Only valid if mError is 0
        private boolean mAuthenticated;
        private int mError;
        // Only valid if mError is ERROR_VENDOR
        private int mVendorError;
        private int mUser;

        AuthenticationEvent(long startTime, long latency, boolean authenticated, int error,
                int vendorError, int user) {
            mStartTime = startTime;
            mLatency = latency;
            mAuthenticated = authenticated;
            mError = error;
            mVendorError = vendorError;
            mUser = user;
        }

        public String toString(Context context) {
            return "Start: " + mStartTime
                    + "\tLatency: " + mLatency
                    + "\tAuthenticated: " + mAuthenticated
                    + "\tError: " + mError
                    + "\tVendorCode: " + mVendorError
                    + "\tUser: " + mUser
                    + "\t" + FaceManager.getErrorString(context, mError, mVendorError);
        }
    }

    private Context mContext;
    private ArrayDeque<AuthenticationEvent> mAuthenticationEvents;

    private int mAcceptCount;
    private int mRejectCount;
    private SparseIntArray mErrorCount;

    private long mAcceptLatency;
    private long mRejectLatency;
    private SparseLongArray mErrorLatency;

    UsageStats(Context context) {
        mAuthenticationEvents = new ArrayDeque<>();
        mErrorCount = new SparseIntArray();
        mErrorLatency = new SparseLongArray();
        mContext = context;
    }

    void addEvent(AuthenticationEvent event) {
        if (mAuthenticationEvents.size() >= EVENT_LOG_SIZE) {
            mAuthenticationEvents.removeFirst();
        }
        mAuthenticationEvents.add(event);

        if (event.mAuthenticated) {
            mAcceptCount++;
            mAcceptLatency += event.mLatency;
        } else if (event.mError == 0) {
            mRejectCount++;
            mRejectLatency += event.mLatency;
        } else {
            mErrorCount.put(event.mError, mErrorCount.get(event.mError, 0) + 1);
            mErrorLatency.put(event.mError, mErrorLatency.get(event.mError, 0L) + event.mLatency);
        }
    }

    void print(PrintWriter pw) {
        pw.println("Events since last reboot: " + mAuthenticationEvents.size());
        for (AuthenticationEvent event : mAuthenticationEvents) {
            pw.println(event.toString(mContext));
        }

        // Dump aggregated usage stats
        pw.println("Accept\tCount: " + mAcceptCount + "\tLatency: " + mAcceptLatency
                + "\tAverage: " + (mAcceptCount > 0 ? mAcceptLatency / mAcceptCount : 0));
        pw.println("Reject\tCount: " + mRejectCount + "\tLatency: " + mRejectLatency
                + "\tAverage: " + (mRejectCount > 0 ? mRejectLatency / mRejectCount : 0));

        for (int i = 0; i < mErrorCount.size(); i++) {
            final int key = mErrorCount.keyAt(i);
            final int count = mErrorCount.get(i);
            pw.println("Error" + key + "\tCount: " + count
                    + "\tLatency: " + mErrorLatency.get(key, 0L)
                    + "\tAverage: " + (count > 0 ? mErrorLatency.get(key, 0L) / count : 0)
                    + "\t" + FaceManager.getErrorString(mContext, key, 0 /* vendorCode */));
        }
    }
}
Loading