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

Commit c7b7bb27 authored by Wenhui Yang's avatar Wenhui Yang Committed by Automerger Merge Worker
Browse files

Merge "[1/n] FRR data capture" into udc-qpr-dev am: 811b7712

parents f97ae1f2 811b7712
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -5339,6 +5339,12 @@
    <!-- Default value for performant auth feature. -->
    <bool name="config_performantAuthDefault">false</bool>

    <!-- Threshold for false rejection rate (FRR) of biometric authentication. Applies for both
         fingerprint and face. If a dual-modality device only enrolled a single biometric and
         experiences high FRR (above threshold), system notification will be sent to encourage user
         to enroll the other eligible biometric. -->
    <fraction name="config_biometricNotificationFrrThreshold">30%</fraction>

    <!-- The component name for the default profile supervisor, which can be set as a profile owner
    even after user setup is complete. The defined component should be used for supervision purposes
    only. The component must be part of a system app. -->
+3 −0
Original line number Diff line number Diff line
@@ -2582,6 +2582,9 @@
  <java-symbol type="string" name="biometric_error_device_not_secured" />
  <java-symbol type="string" name="biometric_error_generic" />

  <!-- Biometric FRR config -->
  <java-symbol type="fraction" name="config_biometricNotificationFrrThreshold" />

  <!-- Device credential strings for BiometricManager -->
  <java-symbol type="string" name="screen_lock_app_setting_name" />
  <java-symbol type="string" name="screen_lock_dialog_default_subtitle" />
+90 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

/**
 * Utility class for on-device biometric authentication data, including total authentication,
 * rejections, and the number of sent enrollment notifications.
 */
public class AuthenticationStats {

    private final int mUserId;
    private int mTotalAttempts;
    private int mRejectedAttempts;
    private int mEnrollmentNotifications;
    private final int mModality;

    public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts,
            int enrollmentNotifications, final int modality) {
        mUserId = userId;
        mTotalAttempts = totalAttempts;
        mRejectedAttempts = rejectedAttempts;
        mEnrollmentNotifications = enrollmentNotifications;
        mModality = modality;
    }

    public AuthenticationStats(final int userId, final int modality) {
        mUserId = userId;
        mTotalAttempts = 0;
        mRejectedAttempts = 0;
        mEnrollmentNotifications = 0;
        mModality = modality;
    }

    public int getUserId() {
        return mUserId;
    }

    public int getTotalAttempts() {
        return mTotalAttempts;
    }

    public int getRejectedAttempts() {
        return mRejectedAttempts;
    }

    public int getEnrollmentNotifications() {
        return mEnrollmentNotifications;
    }

    public int getModality() {
        return mModality;
    }

    /** Calculate FRR. */
    public float getFrr() {
        if (mTotalAttempts > 0) {
            return mRejectedAttempts / (float) mTotalAttempts;
        } else {
            return -1.0f;
        }
    }

    /** Update total authentication attempts and rejections. */
    public void authenticate(boolean authenticated) {
        if (!authenticated) {
            mRejectedAttempts++;
        }
        mTotalAttempts++;
    }

    /** Reset total authentication attempts and rejections. */
    public void resetData() {
        mTotalAttempts = 0;
        mRejectedAttempts = 0;
    }
}
+106 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

import java.util.HashMap;
import java.util.Map;

/**
 * Calculate and collect on-device False Rejection Rates (FRR).
 * FRR = All [given biometric modality] unlock failures / all [given biometric modality] unlock
 * attempts.
 */
public class AuthenticationStatsCollector {

    private static final String TAG = "AuthenticationStatsCollector";

    // The minimum number of attempts that will calculate the FRR and trigger the notification.
    private static final int MINIMUM_ATTEMPTS = 500;
    // The maximum number of eligible biometric enrollment notification can be sent.
    private static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 2;

    private final float mThreshold;
    private final int mModality;

    @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap;

    public AuthenticationStatsCollector(@NonNull Context context, int modality) {
        mThreshold = context.getResources()
                .getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1);
        mUserAuthenticationStatsMap = new HashMap<>();
        mModality = modality;
    }

    /** Update total authentication and rejected attempts. */
    public void authenticate(int userId, boolean authenticated) {
        // Check if this is a new user.
        if (!mUserAuthenticationStatsMap.containsKey(userId)) {
            mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality));
        }

        AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);

        authenticationStats.authenticate(authenticated);

        persistDataIfNeeded(userId);
        sendNotificationIfNeeded(userId);
    }

    private void sendNotificationIfNeeded(int userId) {
        AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
        if (authenticationStats.getTotalAttempts() >= MINIMUM_ATTEMPTS) {
            // Send notification if FRR exceeds the threshold
            if (authenticationStats.getEnrollmentNotifications() < MAXIMUM_ENROLLMENT_NOTIFICATIONS
                    && authenticationStats.getFrr() >= mThreshold) {
                // TODO(wenhuiy): Send notifications.
            }

            authenticationStats.resetData();
        }
    }

    private void persistDataIfNeeded(int userId) {
        AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
        if (authenticationStats.getTotalAttempts() % 50 == 0) {
            // TODO(wenhuiy): Persist data.
        }
    }

    /**
     * Only being used in tests. Callers should not make any changes to the returned
     * authentication stats.
     *
     * @return AuthenticationStats of the user, or null if the stats doesn't exist.
     */
    @Nullable
    @VisibleForTesting
    AuthenticationStats getAuthenticationStatsForUser(int userId) {
        return mUserAuthenticationStatsMap.getOrDefault(userId, null);
    }

    @VisibleForTesting
    void setAuthenticationStatsForUser(int userId, AuthenticationStats authenticationStats) {
        mUserAuthenticationStatsMap.put(userId, authenticationStats);
    }
}
+16 −4
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.biometrics.AuthenticationStatsCollector;
import com.android.server.biometrics.Utils;

/**
@@ -41,6 +42,7 @@ public class BiometricLogger {
    private final int mStatsAction;
    private final int mStatsClient;
    private final BiometricFrameworkStatsLogger mSink;
    @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector;
    @NonNull private final ALSProbe mALSProbe;

    private long mFirstAcquireTimeMs;
@@ -49,7 +51,8 @@ public class BiometricLogger {
    /** Get a new logger with all unknown fields (for operations that do not require logs). */
    public static BiometricLogger ofUnknown(@NonNull Context context) {
        return new BiometricLogger(context, BiometricsProtoEnums.MODALITY_UNKNOWN,
                BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN);
                BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN,
                null /* AuthenticationStatsCollector */);
    }

    /**
@@ -64,26 +67,32 @@ public class BiometricLogger {
     * @param statsClient One of {@link BiometricsProtoEnums} CLIENT_* constants.
     */
    public BiometricLogger(
            @NonNull Context context, int statsModality, int statsAction, int statsClient) {
            @NonNull Context context, int statsModality, int statsAction, int statsClient,
            AuthenticationStatsCollector authenticationStatsCollector) {
        this(statsModality, statsAction, statsClient,
                BiometricFrameworkStatsLogger.getInstance(),
                authenticationStatsCollector,
                context.getSystemService(SensorManager.class));
    }

    @VisibleForTesting
    BiometricLogger(
            int statsModality, int statsAction, int statsClient,
            BiometricFrameworkStatsLogger logSink, SensorManager sensorManager) {
            BiometricFrameworkStatsLogger logSink,
            @NonNull AuthenticationStatsCollector statsCollector,
            SensorManager sensorManager) {
        mStatsModality = statsModality;
        mStatsAction = statsAction;
        mStatsClient = statsClient;
        mSink = logSink;
        mAuthenticationStatsCollector = statsCollector;
        mALSProbe = new ALSProbe(sensorManager);
    }

    /** Creates a new logger with the action replaced with the new action. */
    public BiometricLogger swapAction(@NonNull Context context, int statsAction) {
        return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient);
        return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient,
                null /* AuthenticationStatsCollector */);
    }

    /** Disable logging metrics and only log critical events, such as system health issues. */
@@ -192,10 +201,13 @@ public class BiometricLogger {
    public void logOnAuthenticated(Context context, OperationContextExt operationContext,
            boolean authenticated, boolean requireConfirmation, int targetUserId,
            boolean isBiometricPrompt) {
        // Do not log metrics when fingerprint enrollment reason is ENROLL_FIND_SENSOR
        if (!mShouldLogMetrics) {
            return;
        }

        mAuthenticationStatsCollector.authenticate(targetUserId, authenticated);

        int authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__UNKNOWN;
        if (!authenticated) {
            authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__REJECTED;
Loading