blob: e957794372aacfce8d5f1cffd45cd16f72457c00 [file] [log] [blame]
/*
* 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.hidl;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.SynchronousUserSwitchObserver;
import android.app.UserSwitchObserver;
import android.content.Context;
import android.content.pm.UserInfo;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFaceConstants;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.ITestSession;
import android.hardware.biometrics.ITestSessionCallback;
import android.hardware.biometrics.face.V1_0.IBiometricsFace;
import android.hardware.biometrics.face.V1_0.IBiometricsFaceClientCallback;
import android.hardware.face.Face;
import android.hardware.face.FaceSensorPropertiesInternal;
import android.hardware.face.IFaceServiceReceiver;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.IHwBinder;
import android.os.Looper;
import android.os.NativeHandle;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.view.Surface;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.biometrics.SensorServiceStateProto;
import com.android.server.biometrics.SensorStateProto;
import com.android.server.biometrics.UserStateProto;
import com.android.server.biometrics.Utils;
import com.android.server.biometrics.sensors.AcquisitionClient;
import com.android.server.biometrics.sensors.AuthenticationConsumer;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricNotificationUtils;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.EnumerateConsumer;
import com.android.server.biometrics.sensors.ErrorConsumer;
import com.android.server.biometrics.sensors.HalClientMonitor;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.LockoutTracker;
import com.android.server.biometrics.sensors.PerformanceTracker;
import com.android.server.biometrics.sensors.RemovalConsumer;
import com.android.server.biometrics.sensors.face.FaceUtils;
import com.android.server.biometrics.sensors.face.LockoutHalImpl;
import com.android.server.biometrics.sensors.face.ServiceProvider;
import com.android.server.biometrics.sensors.face.UsageStats;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* Supports a single instance of the {@link android.hardware.biometrics.face.V1_0} or its extended
* minor versions.
*/
public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
private static final String TAG = "Face10";
private static final int ENROLL_TIMEOUT_SEC = 75;
private static final int GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS = 60 * 1000;
private static final int GENERATE_CHALLENGE_COUNTER_TTL_MILLIS =
FaceGenerateChallengeClient.CHALLENGE_TIMEOUT_SEC * 1000;
@VisibleForTesting
public static Clock sSystemClock = Clock.systemUTC();
private boolean mTestHalEnabled;
@NonNull private final FaceSensorPropertiesInternal mSensorProperties;
@NonNull private final Context mContext;
@NonNull private final BiometricScheduler mScheduler;
@NonNull private final Handler mHandler;
@NonNull private final HalClientMonitor.LazyDaemon<IBiometricsFace> mLazyDaemon;
@NonNull private final LockoutHalImpl mLockoutTracker;
@NonNull private final UsageStats mUsageStats;
@NonNull private final Map<Integer, Long> mAuthenticatorIds;
@Nullable private IBiometricsFace mDaemon;
@NonNull private final HalResultController mHalResultController;
// for requests that do not use biometric prompt
@NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
private int mCurrentUserId = UserHandle.USER_NULL;
private final int mSensorId;
private final List<Long> mGeneratedChallengeCount = new ArrayList<>();
private FaceGenerateChallengeClient mGeneratedChallengeCache = null;
private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() {
@Override
public void onUserSwitching(int newUserId) {
scheduleInternalCleanup(newUserId, null /* callback */);
scheduleGetFeature(mSensorId, new Binder(), newUserId,
BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION,
null, mContext.getOpPackageName());
}
};
public static class HalResultController extends IBiometricsFaceClientCallback.Stub {
/**
* Interface to sends results to the HalResultController's owner.
*/
public interface Callback {
/**
* Invoked when the HAL sends ERROR_HW_UNAVAILABLE.
*/
void onHardwareUnavailable();
}
private final int mSensorId;
@NonNull private final Context mContext;
@NonNull private final Handler mHandler;
@NonNull private final BiometricScheduler mScheduler;
@Nullable private Callback mCallback;
@NonNull private final LockoutHalImpl mLockoutTracker;
@NonNull private final LockoutResetDispatcher mLockoutResetDispatcher;
HalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler,
@NonNull BiometricScheduler scheduler, @NonNull LockoutHalImpl lockoutTracker,
@NonNull LockoutResetDispatcher lockoutResetDispatcher) {
mSensorId = sensorId;
mContext = context;
mHandler = handler;
mScheduler = scheduler;
mLockoutTracker = lockoutTracker;
mLockoutResetDispatcher = lockoutResetDispatcher;
}
public void setCallback(@Nullable Callback callback) {
mCallback = callback;
}
@Override
public void onEnrollResult(long deviceId, int faceId, int userId, int remaining) {
mHandler.post(() -> {
final CharSequence name = FaceUtils.getLegacyInstance(mSensorId)
.getUniqueName(mContext, userId);
final Face face = new Face(name, faceId, deviceId);
final BaseClientMonitor client = mScheduler.getCurrentClient();
if (!(client instanceof FaceEnrollClient)) {
Slog.e(TAG, "onEnrollResult for non-enroll client: "
+ Utils.getClientName(client));
return;
}
final FaceEnrollClient enrollClient = (FaceEnrollClient) client;
enrollClient.onEnrollResult(face, remaining);
});
}
@Override
public void onAuthenticated(long deviceId, int faceId, int userId,
ArrayList<Byte> token) {
mHandler.post(() -> {
final BaseClientMonitor client = mScheduler.getCurrentClient();
if (!(client instanceof AuthenticationConsumer)) {
Slog.e(TAG, "onAuthenticated for non-authentication consumer: "
+ Utils.getClientName(client));
return;
}
final AuthenticationConsumer authenticationConsumer =
(AuthenticationConsumer) client;
final boolean authenticated = faceId != 0;
final Face face = new Face("", faceId, deviceId);
authenticationConsumer.onAuthenticated(face, authenticated, token);
});
}
@Override
public void onAcquired(long deviceId, int userId, int acquiredInfo,
int vendorCode) {
mHandler.post(() -> {
final BaseClientMonitor client = mScheduler.getCurrentClient();
if (!(client instanceof AcquisitionClient)) {
Slog.e(TAG, "onAcquired for non-acquire client: "
+ Utils.getClientName(client));
return;
}
final AcquisitionClient<?> acquisitionClient =
(AcquisitionClient<?>) client;
acquisitionClient.onAcquired(acquiredInfo, vendorCode);
});
}
@Override
public void onError(long deviceId, int userId, int error, int vendorCode) {
mHandler.post(() -> {
final BaseClientMonitor client = mScheduler.getCurrentClient();
Slog.d(TAG, "handleError"
+ ", client: " + (client != null ? client.getOwnerString() : null)
+ ", error: " + error
+ ", vendorCode: " + vendorCode);
if (!(client instanceof ErrorConsumer)) {
Slog.e(TAG, "onError for non-error consumer: " + Utils.getClientName(
client));
return;
}
final ErrorConsumer errorConsumer = (ErrorConsumer) client;
errorConsumer.onError(error, vendorCode);
if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE) {
Slog.e(TAG, "Got ERROR_HW_UNAVAILABLE");
if (mCallback != null) {
mCallback.onHardwareUnavailable();
}
}
});
}
@Override
public void onRemoved(long deviceId, ArrayList<Integer> removed, int userId) {
mHandler.post(() -> {
final BaseClientMonitor client = mScheduler.getCurrentClient();
if (!(client instanceof RemovalConsumer)) {
Slog.e(TAG, "onRemoved for non-removal consumer: "
+ Utils.getClientName(client));
return;
}
final RemovalConsumer removalConsumer = (RemovalConsumer) client;
if (!removed.isEmpty()) {
// Convert to old fingerprint-like behavior, where remove() receives
// one removal at a time. This way, remove can share some more common code.
for (int i = 0; i < removed.size(); i++) {
final int id = removed.get(i);
final Face face = new Face("", id, deviceId);
final int remaining = removed.size() - i - 1;
Slog.d(TAG, "Removed, faceId: " + id + ", remaining: " + remaining);
removalConsumer.onRemoved(face, remaining);
}
} else {
removalConsumer.onRemoved(null, 0 /* remaining */);
}
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0, UserHandle.USER_CURRENT);
});
}
@Override
public void onEnumerate(long deviceId, ArrayList<Integer> faceIds, int userId) {
mHandler.post(() -> {
final BaseClientMonitor client = mScheduler.getCurrentClient();
if (!(client instanceof EnumerateConsumer)) {
Slog.e(TAG, "onEnumerate for non-enumerate consumer: "
+ Utils.getClientName(client));
return;
}
final EnumerateConsumer enumerateConsumer = (EnumerateConsumer) client;
if (!faceIds.isEmpty()) {
// Convert to old fingerprint-like behavior, where enumerate() receives one
// template at a time. This way, enumerate can share some more common code.
for (int i = 0; i < faceIds.size(); i++) {
final Face face = new Face("", faceIds.get(i), deviceId);
enumerateConsumer.onEnumerationResult(face, faceIds.size() - i - 1);
}
} else {
// For face, the HIDL contract is to receive an empty list when there are no
// templates enrolled. Send a null identifier since we don't consume them
// anywhere, and send remaining == 0 so this code can be shared with Face@1.1
enumerateConsumer.onEnumerationResult(null /* identifier */, 0);
}
});
}
@Override
public void onLockoutChanged(long duration) {
mHandler.post(() -> {
Slog.d(TAG, "onLockoutChanged: " + duration);
final @LockoutTracker.LockoutMode int lockoutMode;
if (duration == 0) {
lockoutMode = LockoutTracker.LOCKOUT_NONE;
} else if (duration == -1 || duration == Long.MAX_VALUE) {
lockoutMode = LockoutTracker.LOCKOUT_PERMANENT;
} else {
lockoutMode = LockoutTracker.LOCKOUT_TIMED;
}
mLockoutTracker.setCurrentUserLockoutMode(lockoutMode);
if (duration == 0) {
mLockoutResetDispatcher.notifyLockoutResetCallbacks(mSensorId);
}
});
}
}
@VisibleForTesting
Face10(@NonNull Context context,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull Handler handler,
@NonNull BiometricScheduler scheduler) {
mSensorProperties = sensorProps;
mContext = context;
mSensorId = sensorProps.sensorId;
mScheduler = scheduler;
mHandler = handler;
mUsageStats = new UsageStats(context);
mAuthenticatorIds = new HashMap<>();
mLazyDaemon = Face10.this::getDaemon;
mLockoutTracker = new LockoutHalImpl();
mHalResultController = new HalResultController(sensorProps.sensorId, context, mHandler,
mScheduler, mLockoutTracker, lockoutResetDispatcher);
mHalResultController.setCallback(() -> {
mDaemon = null;
mCurrentUserId = UserHandle.USER_NULL;
});
try {
ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to register user switch observer");
}
}
public static Face10 newInstance(@NonNull Context context,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher) {
final Handler handler = new Handler(Looper.getMainLooper());
return new Face10(context, sensorProps, lockoutResetDispatcher, handler,
new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
null /* gestureAvailabilityTracker */));
}
@Override
public void serviceDied(long cookie) {
Slog.e(TAG, "HAL died");
mHandler.post(() -> {
PerformanceTracker.getInstanceForSensorId(mSensorId)
.incrementHALDeathCount();
mDaemon = null;
mCurrentUserId = UserHandle.USER_NULL;
final BaseClientMonitor client = mScheduler.getCurrentClient();
if (client instanceof ErrorConsumer) {
Slog.e(TAG, "Sending ERROR_HW_UNAVAILABLE for client: " + client);
final ErrorConsumer errorConsumer = (ErrorConsumer) client;
errorConsumer.onError(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE,
0 /* vendorCode */);
FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_SYSTEM_HEALTH_ISSUE_DETECTED,
BiometricsProtoEnums.MODALITY_FACE,
BiometricsProtoEnums.ISSUE_HAL_DEATH,
-1 /* sensorId */);
}
mScheduler.recordCrashState();
mScheduler.reset();
});
}
private synchronized IBiometricsFace getDaemon() {
if (mTestHalEnabled) {
final TestHal testHal = new TestHal(mContext, mSensorId);
testHal.setCallback(mHalResultController);
return testHal;
}
if (mDaemon != null) {
return mDaemon;
}
Slog.d(TAG, "Daemon was null, reconnecting, current operation: "
+ mScheduler.getCurrentClient());
try {
mDaemon = IBiometricsFace.getService();
} catch (java.util.NoSuchElementException e) {
// Service doesn't exist or cannot be opened.
Slog.w(TAG, "NoSuchElementException", e);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to get face HAL", e);
}
if (mDaemon == null) {
Slog.w(TAG, "Face HAL not available");
return null;
}
mDaemon.asBinder().linkToDeath(this, 0 /* flags */);
// HAL ID for these HIDL versions are only used to determine if callbacks have been
// successfully set.
long halId = 0;
try {
halId = mDaemon.setCallback(mHalResultController).value;
} catch (RemoteException e) {
Slog.e(TAG, "Failed to set callback for face HAL", e);
mDaemon = null;
}
Slog.d(TAG, "Face HAL ready, HAL ID: " + halId);
if (halId != 0) {
scheduleLoadAuthenticatorIds();
scheduleInternalCleanup(ActivityManager.getCurrentUser(), null /* callback */);
scheduleGetFeature(mSensorId, new Binder(),
ActivityManager.getCurrentUser(),
BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION, null,
mContext.getOpPackageName());
} else {
Slog.e(TAG, "Unable to set callback");
mDaemon = null;
}
return mDaemon;
}
@Override
public boolean containsSensor(int sensorId) {
return mSensorId == sensorId;
}
@Override
@NonNull
public List<FaceSensorPropertiesInternal> getSensorProperties() {
final List<FaceSensorPropertiesInternal> properties = new ArrayList<>();
properties.add(mSensorProperties);
return properties;
}
@NonNull
@Override
public FaceSensorPropertiesInternal getSensorProperties(int sensorId) {
return mSensorProperties;
}
@Override
@NonNull
public List<Face> getEnrolledFaces(int sensorId, int userId) {
return FaceUtils.getLegacyInstance(mSensorId).getBiometricsForUser(mContext, userId);
}
@Override
@LockoutTracker.LockoutMode
public int getLockoutModeForUser(int sensorId, int userId) {
return mLockoutTracker.getLockoutModeForUser(userId);
}
@Override
public long getAuthenticatorId(int sensorId, int userId) {
return mAuthenticatorIds.getOrDefault(userId, 0L);
}
@Override
public boolean isHardwareDetected(int sensorId) {
return getDaemon() != null;
}
private boolean isGeneratedChallengeCacheValid() {
return mGeneratedChallengeCache != null
&& sSystemClock.millis() - mGeneratedChallengeCache.getCreatedAt()
< GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS;
}
private void incrementChallengeCount() {
mGeneratedChallengeCount.add(0, sSystemClock.millis());
}
private int decrementChallengeCount() {
final long now = sSystemClock.millis();
// ignore values that are old in case generate/revoke calls are not matched
// this doesn't ensure revoke if calls are mismatched but it keeps the list from growing
mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS);
if (!mGeneratedChallengeCount.isEmpty()) {
mGeneratedChallengeCount.remove(0);
}
return mGeneratedChallengeCount.size();
}
/**
* {@link IBiometricsFace} only supports a single in-flight challenge but there are cases where
* two callers both need challenges (e.g. resetLockout right before enrollment).
*/
@Override
public void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token,
@NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
mHandler.post(() -> {
incrementChallengeCount();
if (isGeneratedChallengeCacheValid()) {
Slog.d(TAG, "Current challenge is cached and will be reused");
mGeneratedChallengeCache.reuseResult(receiver);
return;
}
scheduleUpdateActiveUserWithoutHandler(userId);
final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext,
mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId,
opPackageName, mSensorId, sSystemClock.millis());
mGeneratedChallengeCache = client;
mScheduler.scheduleClientMonitor(client, new BaseClientMonitor.Callback() {
@Override
public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
if (client != clientMonitor) {
Slog.e(TAG, "scheduleGenerateChallenge onClientStarted, mismatched client."
+ " Expecting: " + client + ", received: " + clientMonitor);
}
}
});
});
}
@Override
public void scheduleRevokeChallenge(int sensorId, int userId, @NonNull IBinder token,
@NonNull String opPackageName, long challenge) {
mHandler.post(() -> {
final boolean shouldRevoke = decrementChallengeCount() == 0;
if (!shouldRevoke) {
Slog.w(TAG, "scheduleRevokeChallenge skipped - challenge still in use: "
+ mGeneratedChallengeCount);
return;
}
Slog.d(TAG, "scheduleRevokeChallenge executing - no active clients");
mGeneratedChallengeCache = null;
final FaceRevokeChallengeClient client = new FaceRevokeChallengeClient(mContext,
mLazyDaemon, token, userId, opPackageName, mSensorId);
mScheduler.scheduleClientMonitor(client, new BaseClientMonitor.Callback() {
@Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
if (client != clientMonitor) {
Slog.e(TAG, "scheduleRevokeChallenge, mismatched client."
+ "Expecting: " + client + ", received: " + clientMonitor);
}
}
});
});
}
@Override
public long scheduleEnroll(int sensorId, @NonNull IBinder token,
@NonNull byte[] hardwareAuthToken, int userId, @NonNull IFaceServiceReceiver receiver,
@NonNull String opPackageName, @NonNull int[] disabledFeatures,
@Nullable Surface previewSurface, boolean debugConsent) {
final long id = mRequestCounter.incrementAndGet();
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
BiometricNotificationUtils.cancelReEnrollNotification(mContext);
final FaceEnrollClient client = new FaceEnrollClient(mContext, mLazyDaemon, token,
new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken,
opPackageName, id, FaceUtils.getLegacyInstance(mSensorId), disabledFeatures,
ENROLL_TIMEOUT_SEC, previewSurface, mSensorId);
mScheduler.scheduleClientMonitor(client, new BaseClientMonitor.Callback() {
@Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
if (success) {
// Update authenticatorIds
scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId());
}
}
});
});
return id;
}
@Override
public void cancelEnrollment(int sensorId, @NonNull IBinder token, long requestId) {
mHandler.post(() -> mScheduler.cancelEnrollment(token, requestId));
}
@Override
public long scheduleFaceDetect(int sensorId, @NonNull IBinder token,
int userId, @NonNull ClientMonitorCallbackConverter callback,
@NonNull String opPackageName, int statsClient) {
throw new IllegalStateException("Face detect not supported by IBiometricsFace@1.0. Did you"
+ "forget to check the supportsFaceDetection flag?");
}
@Override
public void cancelFaceDetect(int sensorId, @NonNull IBinder token, long requestId) {
throw new IllegalStateException("Face detect not supported by IBiometricsFace@1.0. Did you"
+ "forget to check the supportsFaceDetection flag?");
}
@Override
public void scheduleAuthenticate(int sensorId, @NonNull IBinder token, long operationId,
int userId, int cookie, @NonNull ClientMonitorCallbackConverter receiver,
@NonNull String opPackageName, long requestId, boolean restricted, int statsClient,
boolean allowBackgroundAuthentication, boolean isKeyguardBypassEnabled) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
final boolean isStrongBiometric = Utils.isStrongBiometric(mSensorId);
final FaceAuthenticationClient client = new FaceAuthenticationClient(mContext,
mLazyDaemon, token, requestId, receiver, userId, operationId, restricted,
opPackageName, cookie, false /* requireConfirmation */, mSensorId,
isStrongBiometric, statsClient, mLockoutTracker, mUsageStats,
allowBackgroundAuthentication, isKeyguardBypassEnabled);
mScheduler.scheduleClientMonitor(client);
});
}
@Override
public long scheduleAuthenticate(int sensorId, @NonNull IBinder token, long operationId,
int userId, int cookie, @NonNull ClientMonitorCallbackConverter receiver,
@NonNull String opPackageName, boolean restricted, int statsClient,
boolean allowBackgroundAuthentication, boolean isKeyguardBypassEnabled) {
final long id = mRequestCounter.incrementAndGet();
scheduleAuthenticate(sensorId, token, operationId, userId, cookie, receiver,
opPackageName, id, restricted, statsClient,
allowBackgroundAuthentication, isKeyguardBypassEnabled);
return id;
}
@Override
public void cancelAuthentication(int sensorId, @NonNull IBinder token, long requestId) {
mHandler.post(() -> mScheduler.cancelAuthenticationOrDetection(token, requestId));
}
@Override
public void scheduleRemove(int sensorId, @NonNull IBinder token, int faceId, int userId,
@NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
final FaceRemovalClient client = new FaceRemovalClient(mContext, mLazyDaemon, token,
new ClientMonitorCallbackConverter(receiver), faceId, userId, opPackageName,
FaceUtils.getLegacyInstance(mSensorId), mSensorId, mAuthenticatorIds);
mScheduler.scheduleClientMonitor(client);
});
}
@Override
public void scheduleRemoveAll(int sensorId, @NonNull IBinder token, int userId,
@NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
// For IBiometricsFace@1.0, remove(0) means remove all enrollments
final FaceRemovalClient client = new FaceRemovalClient(mContext, mLazyDaemon, token,
new ClientMonitorCallbackConverter(receiver), 0 /* faceId */, userId,
opPackageName,
FaceUtils.getLegacyInstance(mSensorId), mSensorId, mAuthenticatorIds);
mScheduler.scheduleClientMonitor(client);
});
}
@Override
public void scheduleResetLockout(int sensorId, int userId, @NonNull byte[] hardwareAuthToken) {
mHandler.post(() -> {
if (getEnrolledFaces(sensorId, userId).isEmpty()) {
Slog.w(TAG, "Ignoring lockout reset, no templates enrolled for user: " + userId);
return;
}
scheduleUpdateActiveUserWithoutHandler(userId);
final FaceResetLockoutClient client = new FaceResetLockoutClient(mContext,
mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId,
hardwareAuthToken);
mScheduler.scheduleClientMonitor(client);
});
}
@Override
public void scheduleSetFeature(int sensorId, @NonNull IBinder token, int userId, int feature,
boolean enabled, @NonNull byte[] hardwareAuthToken,
@NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
mHandler.post(() -> {
final List<Face> faces = getEnrolledFaces(sensorId, userId);
if (faces.isEmpty()) {
Slog.w(TAG, "Ignoring setFeature, no templates enrolled for user: " + userId);
return;
}
scheduleUpdateActiveUserWithoutHandler(userId);
final int faceId = faces.get(0).getBiometricId();
final FaceSetFeatureClient client = new FaceSetFeatureClient(mContext,
mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId,
opPackageName, mSensorId, feature, enabled, hardwareAuthToken, faceId);
mScheduler.scheduleClientMonitor(client);
});
}
@Override
public void scheduleGetFeature(int sensorId, @NonNull IBinder token, int userId, int feature,
@Nullable ClientMonitorCallbackConverter listener, @NonNull String opPackageName) {
mHandler.post(() -> {
final List<Face> faces = getEnrolledFaces(sensorId, userId);
if (faces.isEmpty()) {
Slog.w(TAG, "Ignoring getFeature, no templates enrolled for user: " + userId);
return;
}
scheduleUpdateActiveUserWithoutHandler(userId);
final int faceId = faces.get(0).getBiometricId();
final FaceGetFeatureClient client = new FaceGetFeatureClient(mContext, mLazyDaemon,
token, listener, userId, opPackageName, mSensorId, feature, faceId);
mScheduler.scheduleClientMonitor(client, new BaseClientMonitor.Callback() {
@Override
public void onClientFinished(
@NonNull BaseClientMonitor clientMonitor, boolean success) {
if (success && feature == BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION) {
final int settingsValue = client.getValue() ? 1 : 0;
Slog.d(TAG, "Updating attention value for user: " + userId
+ " to value: " + settingsValue);
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.FACE_UNLOCK_ATTENTION_REQUIRED,
settingsValue, userId);
}
}
});
});
}
private void scheduleInternalCleanup(int userId,
@Nullable BaseClientMonitor.Callback callback) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
final List<Face> enrolledList = getEnrolledFaces(mSensorId, userId);
final FaceInternalCleanupClient client = new FaceInternalCleanupClient(mContext,
mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId, enrolledList,
FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds);
mScheduler.scheduleClientMonitor(client, callback);
});
}
@Override
public void scheduleInternalCleanup(int sensorId, int userId,
@Nullable BaseClientMonitor.Callback callback) {
scheduleInternalCleanup(userId, callback);
}
@Override
public void startPreparedClient(int sensorId, int cookie) {
mHandler.post(() -> {
mScheduler.startPreparedClient(cookie);
});
}
@Override
public void dumpProtoState(int sensorId, ProtoOutputStream proto,
boolean clearSchedulerBuffer) {
final long sensorToken = proto.start(SensorServiceStateProto.SENSOR_STATES);
proto.write(SensorStateProto.SENSOR_ID, mSensorProperties.sensorId);
proto.write(SensorStateProto.MODALITY, SensorStateProto.FACE);
proto.write(SensorStateProto.CURRENT_STRENGTH,
Utils.getCurrentStrength(mSensorProperties.sensorId));
proto.write(SensorStateProto.SCHEDULER, mScheduler.dumpProtoState(clearSchedulerBuffer));
for (UserInfo user : UserManager.get(mContext).getUsers()) {
final int userId = user.getUserHandle().getIdentifier();
final long userToken = proto.start(SensorStateProto.USER_STATES);
proto.write(UserStateProto.USER_ID, userId);
proto.write(UserStateProto.NUM_ENROLLED, FaceUtils.getLegacyInstance(mSensorId)
.getBiometricsForUser(mContext, userId).size());
proto.end(userToken);
}
proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_HARDWARE_AUTH_TOKEN,
mSensorProperties.resetLockoutRequiresHardwareAuthToken);
proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_CHALLENGE,
mSensorProperties.resetLockoutRequiresChallenge);
proto.end(sensorToken);
}
@Override
public void dumpProtoMetrics(int sensorId, FileDescriptor fd) {
}
@Override
public void dumpInternal(int sensorId, PrintWriter pw) {
PerformanceTracker performanceTracker =
PerformanceTracker.getInstanceForSensorId(mSensorId);
JSONObject dump = new JSONObject();
try {
dump.put("service", TAG);
JSONArray sets = new JSONArray();
for (UserInfo user : UserManager.get(mContext).getUsers()) {
final int userId = user.getUserHandle().getIdentifier();
final int c = FaceUtils.getLegacyInstance(mSensorId)
.getBiometricsForUser(mContext, userId).size();
JSONObject set = new JSONObject();
set.put("id", userId);
set.put("count", c);
set.put("accept", performanceTracker.getAcceptForUser(userId));
set.put("reject", performanceTracker.getRejectForUser(userId));
set.put("acquire", performanceTracker.getAcquireForUser(userId));
set.put("lockout", performanceTracker.getTimedLockoutForUser(userId));
set.put("permanentLockout", performanceTracker.getPermanentLockoutForUser(userId));
// cryptoStats measures statistics about secure face transactions
// (e.g. to unlock password storage, make secure purchases, etc.)
set.put("acceptCrypto", performanceTracker.getAcceptCryptoForUser(userId));
set.put("rejectCrypto", performanceTracker.getRejectCryptoForUser(userId));
set.put("acquireCrypto", performanceTracker.getAcquireCryptoForUser(userId));
sets.put(set);
}
dump.put("prints", sets);
} catch (JSONException e) {
Slog.e(TAG, "dump formatting failure", e);
}
pw.println(dump);
pw.println("HAL deaths since last reboot: " + performanceTracker.getHALDeathCount());
mScheduler.dump(pw);
mUsageStats.print(pw);
}
private void scheduleLoadAuthenticatorIds() {
// Note that this can be performed on the scheduler (as opposed to being done immediately
// when the HAL is (re)loaded, since
// 1) If this is truly the first time it's being performed (e.g. system has just started),
// this will be run very early and way before any applications need to generate keys.
// 2) If this is being performed to refresh the authenticatorIds (e.g. HAL crashed and has
// just been reloaded), the framework already has a cache of the authenticatorIds. This
// is safe because authenticatorIds only change when A) new template has been enrolled,
// or B) all templates are removed.
mHandler.post(() -> {
for (UserInfo user : UserManager.get(mContext).getAliveUsers()) {
final int targetUserId = user.id;
if (!mAuthenticatorIds.containsKey(targetUserId)) {
scheduleUpdateActiveUserWithoutHandler(targetUserId);
}
}
});
}
/**
* Schedules the {@link FaceUpdateActiveUserClient} without posting the work onto the handler.
* Many/most APIs are user-specific. However, the HAL requires explicit "setActiveUser"
* invocation prior to authenticate/enroll/etc. Thus, internally we usually want to schedule
* this operation on the same lambda/runnable as those operations so that the ordering is
* correct.
*/
private void scheduleUpdateActiveUserWithoutHandler(int targetUserId) {
final boolean hasEnrolled = !getEnrolledFaces(mSensorId, targetUserId).isEmpty();
final FaceUpdateActiveUserClient client = new FaceUpdateActiveUserClient(mContext,
mLazyDaemon, targetUserId, mContext.getOpPackageName(), mSensorId,
hasEnrolled, mAuthenticatorIds);
mScheduler.scheduleClientMonitor(client, new BaseClientMonitor.Callback() {
@Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
if (success) {
mCurrentUserId = targetUserId;
} else {
Slog.w(TAG, "Failed to change user, still: " + mCurrentUserId);
}
}
});
}
/**
* Sends a debug message to the HAL with the provided FileDescriptor and arguments.
*/
public void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args) {
// WARNING: CDD restricts image data from leaving TEE unencrypted on
// production devices:
// [C-1-10] MUST not allow unencrypted access to identifiable biometric
// data or any data derived from it (such as embeddings) to the
// Application Processor outside the context of the TEE.
// As such, this API should only be enabled for testing purposes on
// engineering and userdebug builds. All modules in the software stack
// MUST enforce final build products do NOT have this functionality.
// Additionally, the following check MUST NOT be removed.
if (!(Build.IS_ENG || Build.IS_USERDEBUG)) {
return;
}
// Additionally, this flag allows turning off face for a device
// (either permanently through the build or on an individual device).
if (SystemProperties.getBoolean("ro.face.disable_debug_data", false)
|| SystemProperties.getBoolean("persist.face.disable_debug_data", false)) {
return;
}
// The debug method takes two file descriptors. The first is for text
// output, which we will drop. The second is for binary data, which
// will be the protobuf data.
final IBiometricsFace daemon = getDaemon();
if (daemon != null) {
FileOutputStream devnull = null;
try {
devnull = new FileOutputStream("/dev/null");
final NativeHandle handle = new NativeHandle(
new FileDescriptor[]{devnull.getFD(), fd},
new int[0], false);
daemon.debug(handle, new ArrayList<String>(Arrays.asList(args)));
} catch (IOException | RemoteException ex) {
Slog.d(TAG, "error while reading face debugging data", ex);
} finally {
if (devnull != null) {
try {
devnull.close();
} catch (IOException ex) {
}
}
}
}
}
void setTestHalEnabled(boolean enabled) {
mTestHalEnabled = enabled;
}
@NonNull
@Override
public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
@NonNull String opPackageName) {
return new BiometricTestSessionImpl(mContext, mSensorId, callback, this,
mHalResultController);
}
}