blob: c76a2e38aabca3b545f2ef14d0fc9e925605b45e [file] [log] [blame]
/*
* Copyright (C) 2022 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.log;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.SensorManager;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.biometrics.Utils;
/**
* Logger for all reported Biometric framework events.
*/
public class BiometricLogger {
public static final String TAG = "BiometricLogger";
public static final boolean DEBUG = false;
private final int mStatsModality;
private final int mStatsAction;
private final int mStatsClient;
private final BiometricFrameworkStatsLogger mSink;
@NonNull private final ALSProbe mALSProbe;
private long mFirstAcquireTimeMs;
private boolean mShouldLogMetrics = true;
/** 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);
}
/**
* Creates a new logger for an instance of a biometric operation.
*
* Do not reuse across operations. Instead, create a new one or use
* {@link #swapAction(Context, int)}.
*
* @param context system_server context
* @param statsModality One of {@link BiometricsProtoEnums} MODALITY_* constants.
* @param statsAction One of {@link BiometricsProtoEnums} ACTION_* constants.
* @param statsClient One of {@link BiometricsProtoEnums} CLIENT_* constants.
*/
public BiometricLogger(
@NonNull Context context, int statsModality, int statsAction, int statsClient) {
this(statsModality, statsAction, statsClient,
BiometricFrameworkStatsLogger.getInstance(),
context.getSystemService(SensorManager.class));
}
@VisibleForTesting
BiometricLogger(
int statsModality, int statsAction, int statsClient,
BiometricFrameworkStatsLogger logSink, SensorManager sensorManager) {
mStatsModality = statsModality;
mStatsAction = statsAction;
mStatsClient = statsClient;
mSink = logSink;
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);
}
/** Disable logging metrics and only log critical events, such as system health issues. */
public void disableMetrics() {
mShouldLogMetrics = false;
mALSProbe.destroy();
}
/** {@link BiometricsProtoEnums} CLIENT_* constants */
public int getStatsClient() {
return mStatsClient;
}
private boolean shouldSkipLogging() {
boolean shouldSkipLogging = (mStatsModality == BiometricsProtoEnums.MODALITY_UNKNOWN
|| mStatsAction == BiometricsProtoEnums.ACTION_UNKNOWN);
if (mStatsModality == BiometricsProtoEnums.MODALITY_UNKNOWN) {
Slog.w(TAG, "Unknown field detected: MODALITY_UNKNOWN, will not report metric");
}
if (mStatsAction == BiometricsProtoEnums.ACTION_UNKNOWN) {
Slog.w(TAG, "Unknown field detected: ACTION_UNKNOWN, will not report metric");
}
if (mStatsClient == BiometricsProtoEnums.CLIENT_UNKNOWN) {
Slog.w(TAG, "Unknown field detected: CLIENT_UNKNOWN");
}
return shouldSkipLogging;
}
/** Log an acquisition event. */
public void logOnAcquired(Context context, OperationContextExt operationContext,
int acquiredInfo, int vendorCode, int targetUserId) {
if (!mShouldLogMetrics) {
return;
}
final boolean isFace = mStatsModality == BiometricsProtoEnums.MODALITY_FACE;
final boolean isFingerprint = mStatsModality == BiometricsProtoEnums.MODALITY_FINGERPRINT;
if (isFace || isFingerprint) {
if ((isFingerprint && acquiredInfo == FingerprintManager.FINGERPRINT_ACQUIRED_START)
|| (isFace && acquiredInfo == FaceManager.FACE_ACQUIRED_START)) {
mFirstAcquireTimeMs = System.currentTimeMillis();
}
} else if (acquiredInfo == BiometricConstants.BIOMETRIC_ACQUIRED_GOOD) {
if (mFirstAcquireTimeMs == 0) {
mFirstAcquireTimeMs = System.currentTimeMillis();
}
}
if (DEBUG) {
Slog.v(TAG, "Acquired! Modality: " + mStatsModality
+ ", User: " + targetUserId
+ ", IsCrypto: " + operationContext.isCrypto()
+ ", Action: " + mStatsAction
+ ", Client: " + mStatsClient
+ ", AcquiredInfo: " + acquiredInfo
+ ", VendorCode: " + vendorCode);
}
if (shouldSkipLogging()) {
return;
}
mSink.acquired(operationContext,
mStatsModality, mStatsAction, mStatsClient,
Utils.isDebugEnabled(context, targetUserId),
acquiredInfo, vendorCode, targetUserId);
}
/** Log an error during an operation. */
public void logOnError(Context context, OperationContextExt operationContext,
int error, int vendorCode, int targetUserId) {
if (!mShouldLogMetrics) {
return;
}
final long latency = mFirstAcquireTimeMs != 0
? (System.currentTimeMillis() - mFirstAcquireTimeMs) : -1;
if (DEBUG) {
Slog.v(TAG, "Error! Modality: " + mStatsModality
+ ", User: " + targetUserId
+ ", IsCrypto: " + operationContext.isCrypto()
+ ", Action: " + mStatsAction
+ ", Client: " + mStatsClient
+ ", Error: " + error
+ ", VendorCode: " + vendorCode
+ ", Latency: " + latency);
} else {
Slog.v(TAG, "Error latency: " + latency);
}
if (shouldSkipLogging()) {
return;
}
mSink.error(operationContext,
mStatsModality, mStatsAction, mStatsClient,
Utils.isDebugEnabled(context, targetUserId), latency,
error, vendorCode, targetUserId);
}
/** Log authentication attempt. */
public void logOnAuthenticated(Context context, OperationContextExt operationContext,
boolean authenticated, boolean requireConfirmation, int targetUserId,
boolean isBiometricPrompt) {
if (!mShouldLogMetrics) {
return;
}
int authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__UNKNOWN;
if (!authenticated) {
authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__REJECTED;
} else {
// Authenticated
if (isBiometricPrompt && requireConfirmation) {
authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__PENDING_CONFIRMATION;
} else {
authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__CONFIRMED;
}
}
// Only valid if we have a first acquired time, otherwise set to -1
final long latency = mFirstAcquireTimeMs != 0
? (System.currentTimeMillis() - mFirstAcquireTimeMs)
: -1;
if (DEBUG) {
Slog.v(TAG, "Authenticated! Modality: " + mStatsModality
+ ", User: " + targetUserId
+ ", IsCrypto: " + operationContext.isCrypto()
+ ", Client: " + mStatsClient
+ ", RequireConfirmation: " + requireConfirmation
+ ", State: " + authState
+ ", Latency: " + latency
+ ", Lux: " + mALSProbe.getMostRecentLux());
} else {
Slog.v(TAG, "Authentication latency: " + latency);
}
if (shouldSkipLogging()) {
return;
}
mSink.authenticate(operationContext,
mStatsModality, mStatsAction, mStatsClient,
Utils.isDebugEnabled(context, targetUserId),
latency, authState, requireConfirmation, targetUserId, mALSProbe);
}
/** Log enrollment outcome. */
public void logOnEnrolled(int targetUserId, long latency, boolean enrollSuccessful) {
if (!mShouldLogMetrics) {
return;
}
if (DEBUG) {
Slog.v(TAG, "Enrolled! Modality: " + mStatsModality
+ ", User: " + targetUserId
+ ", Client: " + mStatsClient
+ ", Latency: " + latency
+ ", Lux: " + mALSProbe.getMostRecentLux()
+ ", Success: " + enrollSuccessful);
} else {
Slog.v(TAG, "Enroll latency: " + latency);
}
if (shouldSkipLogging()) {
return;
}
mSink.enroll(mStatsModality, mStatsAction, mStatsClient,
targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux());
}
/** Report unexpected enrollment reported by the HAL. */
public void logUnknownEnrollmentInHal() {
if (shouldSkipLogging()) {
return;
}
mSink.reportUnknownTemplateEnrolledHal(mStatsModality);
}
/** Report unknown enrollment in framework settings */
public void logUnknownEnrollmentInFramework() {
if (shouldSkipLogging()) {
return;
}
mSink.reportUnknownTemplateEnrolledFramework(mStatsModality);
}
/**
* Get a callback to start/stop ALS capture when the client runs. Do not create
* multiple callbacks since there is at most one light sensor (they will all share
* a single probe sampling from that sensor).
*
* If the probe should not run for the entire operation, do not set startWithClient and
* start/stop the problem when needed.
*
* @param startWithClient if probe should start automatically when the operation starts.
*/
@NonNull
public CallbackWithProbe<Probe> getAmbientLightProbe(boolean startWithClient) {
return new CallbackWithProbe<>(mALSProbe, startWithClient);
}
}