diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java new file mode 100644 index 0000000000000..590579adbe1a3 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -0,0 +1,580 @@ +/* + * 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.aidl; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; +import android.app.TaskStackListener; +import android.content.Context; +import android.content.pm.UserInfo; +import android.hardware.biometrics.face.IFace; +import android.hardware.biometrics.face.SensorProps; +import android.hardware.face.Face; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceServiceReceiver; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.NativeHandle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserManager; +import android.util.Slog; +import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; + +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.sensors.AuthenticationClient; +import com.android.server.biometrics.sensors.ClientMonitor; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.PerformanceTracker; +import com.android.server.biometrics.sensors.face.FaceUtils; +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.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Provider for a single instance of the {@link IFace} HAL. + */ +public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { + private static final String TAG = "FaceProvider"; + private static final int ENROLL_TIMEOUT_SEC = 75; + + @NonNull private final Context mContext; + @NonNull private final String mHalInstanceName; + @NonNull private final SparseArray mSensors; // Map of sensors that this HAL supports + @NonNull private final ClientMonitor.LazyDaemon mLazyDaemon; + @NonNull private final Handler mHandler; + @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher; + @NonNull private final UsageStats mUsageStats; + @NonNull private final IActivityTaskManager mActivityTaskManager; + @NonNull private final BiometricTaskStackListener mTaskStackListener; + + @Nullable private IFace mDaemon; + + private final class BiometricTaskStackListener extends TaskStackListener { + @Override + public void onTaskStackChanged() { + mHandler.post(() -> { + for (int i = 0; i < mSensors.size(); i++) { + final ClientMonitor client = mSensors.get(i).getScheduler() + .getCurrentClient(); + if (!(client instanceof AuthenticationClient)) { + Slog.e(getTag(), "Task stack changed for client: " + client); + continue; + } + if (Utils.isKeyguard(mContext, client.getOwnerString())) { + continue; // Keyguard is always allowed + } + + try { + final List runningTasks = + mActivityTaskManager.getTasks(1); + if (!runningTasks.isEmpty()) { + final String topPackage = + runningTasks.get(0).topActivity.getPackageName(); + if (!topPackage.contentEquals(client.getOwnerString()) + && !client.isAlreadyDone()) { + Slog.e(getTag(), "Stopping background authentication, top: " + + topPackage + " currentClient: " + client); + mSensors.get(i).getScheduler() + .cancelAuthentication(client.getToken()); + } + } + } catch (RemoteException e) { + Slog.e(getTag(), "Unable to get running tasks", e); + } + } + }); + } + } + + public FaceProvider(@NonNull Context context, @NonNull SensorProps[] props, + @NonNull String halInstanceName, + @NonNull LockoutResetDispatcher lockoutResetDispatcher) { + mContext = context; + mHalInstanceName = halInstanceName; + mSensors = new SparseArray<>(); + mLazyDaemon = this::getHalInstance; + mHandler = new Handler(Looper.getMainLooper()); + mUsageStats = new UsageStats(context); + mLockoutResetDispatcher = lockoutResetDispatcher; + mActivityTaskManager = ActivityTaskManager.getService(); + mTaskStackListener = new BiometricTaskStackListener(); + + for (SensorProps prop : props) { + final int sensorId = prop.commonProps.sensorId; + + final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( + prop.commonProps.sensorId, prop.commonProps.sensorStrength, + prop.commonProps.maxEnrollmentsPerUser, false /* supportsFaceDetection */, + prop.halControlsPreview); + final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler, + internalProp); + + mSensors.put(sensorId, sensor); + Slog.d(getTag(), "Added: " + internalProp); + } + } + + private String getTag() { + return "FaceProvider/" + mHalInstanceName; + } + + @Nullable + private synchronized IFace getHalInstance() { + if (mDaemon != null) { + return mDaemon; + } + + Slog.d(getTag(), "Daemon was null, reconnecting"); + + mDaemon = IFace.Stub.asInterface( + ServiceManager.waitForDeclaredService(IFace.DESCRIPTOR + "/" + mHalInstanceName)); + if (mDaemon == null) { + Slog.e(getTag(), "Unable to get daemon"); + return null; + } + + try { + mDaemon.asBinder().linkToDeath(this, 0 /* flags */); + } catch (RemoteException e) { + Slog.e(getTag(), "Unable to linkToDeath", e); + } + + for (int i = 0; i < mSensors.size(); i++) { + final int sensorId = mSensors.keyAt(i); + scheduleLoadAuthenticatorIds(sensorId); + scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser()); + } + + return mDaemon; + } + + private void scheduleForSensor(int sensorId, @NonNull ClientMonitor client) { + if (!mSensors.contains(sensorId)) { + throw new IllegalStateException("Unable to schedule client: " + client + + " for sensor: " + sensorId); + } + mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); + } + + private void scheduleForSensor(int sensorId, @NonNull ClientMonitor client, + ClientMonitor.Callback callback) { + if (!mSensors.contains(sensorId)) { + throw new IllegalStateException("Unable to schedule client: " + client + + " for sensor: " + sensorId); + } + mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client, callback); + } + + private void createNewSessionWithoutHandler(@NonNull IFace daemon, int sensorId, + int userId) throws RemoteException { + // Note that per IFingerprint createSession contract, this method will block until all + // existing operations are canceled/finished. However, also note that this is fine, since + // this method "withoutHandler" means it should only ever be invoked from the worker thread, + // so callers will never be blocked. + mSensors.get(sensorId).createNewSession(daemon, sensorId, userId); + } + + + private void scheduleLoadAuthenticatorIds(int sensorId) { + for (UserInfo user : UserManager.get(mContext).getAliveUsers()) { + scheduleLoadAuthenticatorIdsForUser(sensorId, user.id); + } + } + + private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during loadAuthenticatorIds, sensorId: " + sensorId); + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final FaceGetAuthenticatorIdClient client = new FaceGetAuthenticatorIdClient( + mContext, mSensors.get(sensorId).getLazySession(), userId, + mContext.getOpPackageName(), sensorId, + mSensors.get(sensorId).getAuthenticatorIds()); + + mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling loadAuthenticatorId" + + ", sensorId: " + sensorId + + ", userId: " + userId, e); + } + }); + } + + @Override + public boolean containsSensor(int sensorId) { + return mSensors.contains(sensorId); + } + + @NonNull + @Override + public List getSensorProperties() { + final List props = new ArrayList<>(); + for (int i = 0; i < mSensors.size(); ++i) { + props.add(mSensors.valueAt(i).getSensorProperties()); + } + return props; + } + + @NonNull + @Override + public List getEnrolledFaces(int sensorId, int userId) { + return FaceUtils.getInstance(sensorId).getBiometricsForUser(mContext, userId); + } + + @Override + public int getLockoutModeForUser(int sensorId, int userId) { + return mSensors.get(sensorId).getLockoutCache().getLockoutModeForUser(userId); + } + + @Override + public long getAuthenticatorId(int sensorId, int userId) { + return mSensors.get(sensorId).getAuthenticatorIds().getOrDefault(userId, 0L); + } + + @Override + public boolean isHardwareDetected(int sensorId) { + return getHalInstance() != null; + } + + @Override + public void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token, + @NonNull IFaceServiceReceiver receiver, String opPackageName) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during generateChallenge, sensorId: " + sensorId); + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext, + mSensors.get(sensorId).getLazySession(), token, + new ClientMonitorCallbackConverter(receiver), opPackageName, sensorId); + + scheduleForSensor(sensorId, client); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling generateChallenge", e); + } + }); + } + + @Override + public void scheduleRevokeChallenge(int sensorId, int userId, @NonNull IBinder token, + @NonNull String opPackageName, long challenge) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during revokeChallenge, sensorId: " + sensorId); + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final FaceRevokeChallengeClient client = new FaceRevokeChallengeClient(mContext, + mSensors.get(sensorId).getLazySession(), token, opPackageName, sensorId, + challenge); + + scheduleForSensor(sensorId, client); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling revokeChallenge", e); + } + }); + } + + @Override + public void scheduleEnroll(int sensorId, @NonNull IBinder token, + @NonNull byte[] hardwareAuthToken, int userId, @NonNull IFaceServiceReceiver receiver, + @NonNull String opPackageName, @NonNull int[] disabledFeatures, + @Nullable NativeHandle previewSurface) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during enroll, sensorId: " + sensorId); + // If this happens, we need to send HW_UNAVAILABLE after the scheduler gets to + // this operation. We should not send the callback yet, since the scheduler may + // be processing something else. + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final int maxTemplatesPerUser = mSensors.get( + sensorId).getSensorProperties().maxEnrollmentsPerUser; + final FaceEnrollClient client = new FaceEnrollClient(mContext, + mSensors.get(sensorId).getLazySession(), token, + new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, + opPackageName, FaceUtils.getInstance(sensorId), disabledFeatures, + ENROLL_TIMEOUT_SEC, previewSurface, sensorId, maxTemplatesPerUser); + scheduleForSensor(sensorId, client, new ClientMonitor.Callback() { + @Override + public void onClientFinished(@NonNull ClientMonitor clientMonitor, + boolean success) { + if (success) { + scheduleLoadAuthenticatorIdsForUser(sensorId, userId); + } + } + }); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling enroll", e); + } + }); + } + + @Override + public void cancelEnrollment(int sensorId, @NonNull IBinder token) { + mHandler.post(() -> mSensors.get(sensorId).getScheduler().cancelEnrollment(token)); + } + + @Override + public void scheduleAuthenticate(int sensorId, @NonNull IBinder token, long operationId, + int userId, int cookie, @NonNull ClientMonitorCallbackConverter callback, + @NonNull String opPackageName, boolean restricted, int statsClient, + boolean isKeyguard) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during authenticate, sensorId: " + sensorId); + // If this happens, we need to send HW_UNAVAILABLE after the scheduler gets to + // this operation. We should not send the callback yet, since the scheduler may + // be processing something else. + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId); + final FaceAuthenticationClient client = new FaceAuthenticationClient( + mContext, mSensors.get(sensorId).getLazySession(), token, callback, userId, + operationId, restricted, opPackageName, cookie, + false /* requireConfirmation */, sensorId, isStrongBiometric, statsClient, + mUsageStats, mSensors.get(sensorId).getLockoutCache()); + mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling authenticate", e); + } + }); + } + + @Override + public void cancelAuthentication(int sensorId, @NonNull IBinder token) { + mHandler.post(() -> mSensors.get(sensorId).getScheduler().cancelAuthentication(token)); + } + + @Override + public void scheduleRemove(int sensorId, @NonNull IBinder token, int faceId, int userId, + @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during remove, sensorId: " + sensorId); + // If this happens, we need to send HW_UNAVAILABLE after the scheduler gets to + // this operation. We should not send the callback yet, since the scheduler may + // be processing something else. + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final FaceRemovalClient client = new FaceRemovalClient(mContext, + mSensors.get(sensorId).getLazySession(), token, + new ClientMonitorCallbackConverter(receiver), faceId, userId, + opPackageName, FaceUtils.getInstance(sensorId), sensorId, + mSensors.get(sensorId).getAuthenticatorIds()); + + mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling remove", e); + } + }); + } + + @Override + public void scheduleResetLockout(int sensorId, int userId, @NonNull byte[] hardwareAuthToken) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during resetLockout, sensorId: " + sensorId); + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final FaceResetLockoutClient client = new FaceResetLockoutClient( + mContext, mSensors.get(sensorId).getLazySession(), userId, + mContext.getOpPackageName(), sensorId, hardwareAuthToken, + mSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher); + + scheduleForSensor(sensorId, client); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling resetLockout", e); + } + }); + } + + @Override + public void scheduleSetFeature(int sensorId, @NonNull IBinder token, int userId, int feature, + boolean enabled, @NonNull byte[] hardwareAuthToken, + @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { + // TODO(b/171335732): implement this. + } + + @Override + public void scheduleGetFeature(int sensorId, @NonNull IBinder token, int userId, int feature, + @NonNull ClientMonitorCallbackConverter callback, @NonNull String opPackageName) { + // TODO(b/171335732): implement this. + } + + @Override + public void startPreparedClient(int sensorId, int cookie) { + mHandler.post(() -> { + mSensors.get(sensorId).getScheduler().startPreparedClient(cookie); + }); + } + + @Override + public void scheduleInternalCleanup(int sensorId, int userId) { + mHandler.post(() -> { + final IFace daemon = getHalInstance(); + if (daemon == null) { + Slog.e(getTag(), "Null daemon during internal cleanup, sensorId: " + sensorId); + return; + } + + try { + if (!mSensors.get(sensorId).hasSessionForUser(userId)) { + createNewSessionWithoutHandler(daemon, sensorId, userId); + } + + final List enrolledList = getEnrolledFaces(sensorId, userId); + final FaceInternalCleanupClient client = + new FaceInternalCleanupClient(mContext, + mSensors.get(sensorId).getLazySession(), userId, + mContext.getOpPackageName(), sensorId, enrolledList, + FaceUtils.getInstance(sensorId), + mSensors.get(sensorId).getAuthenticatorIds()); + + mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); + } catch (RemoteException e) { + Slog.e(getTag(), "Remote exception when scheduling internal cleanup", e); + } + }); + } + + @Override + public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto) { + + } + + @Override + public void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd) { + + } + + @Override + public void dumpInternal(int sensorId, @NonNull PrintWriter pw) { + PerformanceTracker performanceTracker = + PerformanceTracker.getInstanceForSensorId(sensorId); + + JSONObject dump = new JSONObject(); + try { + dump.put("service", "Face Manager"); + + JSONArray sets = new JSONArray(); + for (UserInfo user : UserManager.get(mContext).getUsers()) { + final int userId = user.getUserHandle().getIdentifier(); + final int c = FaceUtils.getInstance().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()); + + mSensors.get(sensorId).getScheduler().dump(pw); + mUsageStats.print(pw); + } + + @Override + public void binderDied() { + Slog.e(getTag(), "HAL died"); + mHandler.post(() -> { + mDaemon = null; + for (int i = 0; i < mSensors.size(); i++) { + final int sensorId = mSensors.keyAt(i); + PerformanceTracker.getInstanceForSensorId(sensorId).incrementHALDeathCount(); + } + }); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java new file mode 100644 index 0000000000000..ca1f17be91796 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java @@ -0,0 +1,456 @@ +/* + * 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.aidl; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.biometrics.face.Error; +import android.hardware.biometrics.face.IFace; +import android.hardware.biometrics.face.ISession; +import android.hardware.biometrics.face.ISessionCallback; +import android.hardware.face.Face; +import android.hardware.face.FaceManager; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.keymaster.HardwareAuthToken; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.biometrics.HardwareAuthTokenUtils; +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.BiometricScheduler; +import com.android.server.biometrics.sensors.ClientMonitor; +import com.android.server.biometrics.sensors.EnumerateConsumer; +import com.android.server.biometrics.sensors.Interruptable; +import com.android.server.biometrics.sensors.LockoutCache; +import com.android.server.biometrics.sensors.LockoutConsumer; +import com.android.server.biometrics.sensors.RemovalConsumer; +import com.android.server.biometrics.sensors.face.FaceUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Maintains the state of a single sensor within an instance of the {@link IFace} HAL. + */ +public class Sensor implements IBinder.DeathRecipient { + + @NonNull private final String mTag; + @NonNull private final FaceProvider mProvider; + @NonNull private final Context mContext; + @NonNull private final Handler mHandler; + @NonNull private final FaceSensorPropertiesInternal mSensorProperties; + @NonNull private final BiometricScheduler mScheduler; + @NonNull private final LockoutCache mLockoutCache; + @NonNull private final Map mAuthenticatorIds; + @NonNull private final ClientMonitor.LazyDaemon mLazySession; + @Nullable private Session mCurrentSession; + + @Override + public void binderDied() { + Slog.e(mTag, "Binder died"); + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (client instanceof Interruptable) { + Slog.e(mTag, "Sending ERROR_HW_UNAVAILABLE for client: " + client); + final Interruptable interruptable = (Interruptable) client; + interruptable.onError(FaceManager.FACE_ERROR_HW_UNAVAILABLE, + 0 /* vendorCode */); + + mScheduler.recordCrashState(); + + FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_SYSTEM_HEALTH_ISSUE_DETECTED, + BiometricsProtoEnums.MODALITY_FACE, + BiometricsProtoEnums.ISSUE_HAL_DEATH); + mCurrentSession = null; + } + }); + } + + static class Session { + @NonNull final HalSessionCallback mHalSessionCallback; + @NonNull private final String mTag; + @NonNull private final ISession mSession; + private final int mUserId; + + Session(@NonNull String tag, @NonNull ISession session, int userId, + @NonNull HalSessionCallback halSessionCallback) { + mTag = tag; + mSession = session; + mUserId = userId; + mHalSessionCallback = halSessionCallback; + Slog.d(mTag, "New session created for user: " + userId); + } + } + + Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, + @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties) { + mTag = tag; + mProvider = provider; + mContext = context; + mHandler = handler; + mSensorProperties = sensorProperties; + mScheduler = new BiometricScheduler(tag, null /* gestureAvailabilityDispatcher */); + mLockoutCache = new LockoutCache(); + mAuthenticatorIds = new HashMap<>(); + mLazySession = () -> (mCurrentSession != null) ? mCurrentSession.mSession : null; + } + + @NonNull ClientMonitor.LazyDaemon getLazySession() { + return mLazySession; + } + + @NonNull FaceSensorPropertiesInternal getSensorProperties() { + return mSensorProperties; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + boolean hasSessionForUser(int userId) { + return mCurrentSession != null && mCurrentSession.mUserId == userId; + } + + @Nullable Session getSessionForUser(int userId) { + if (mCurrentSession != null && mCurrentSession.mUserId == userId) { + return mCurrentSession; + } else { + return null; + } + } + + void createNewSession(@NonNull IFace daemon, int sensorId, int userId) + throws RemoteException { + + final HalSessionCallback.Callback callback = () -> { + Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE"); + mCurrentSession = null; + }; + final HalSessionCallback resultController = new HalSessionCallback(mContext, mHandler, + mTag, mScheduler, sensorId, userId, callback); + + final ISession newSession = daemon.createSession(sensorId, userId, resultController); + newSession.asBinder().linkToDeath(this, 0 /* flags */); + mCurrentSession = new Session(mTag, newSession, userId, resultController); + } + + @NonNull BiometricScheduler getScheduler() { + return mScheduler; + } + + @NonNull LockoutCache getLockoutCache() { + return mLockoutCache; + } + + @NonNull Map getAuthenticatorIds() { + return mAuthenticatorIds; + } + + static class HalSessionCallback extends ISessionCallback.Stub { + /** + * Interface to sends results to the HalSessionCallback's owner. + */ + public interface Callback { + /** + * Invoked when the HAL sends ERROR_HW_UNAVAILABLE. + */ + void onHardwareUnavailable(); + } + + @NonNull + private final Context mContext; + @NonNull + private final Handler mHandler; + @NonNull + private final String mTag; + @NonNull + private final BiometricScheduler mScheduler; + private final int mSensorId; + private final int mUserId; + @NonNull + private final Callback mCallback; + + HalSessionCallback(@NonNull Context context, @NonNull Handler handler, @NonNull String tag, + @NonNull BiometricScheduler scheduler, int sensorId, int userId, + @NonNull Callback callback) { + mContext = context; + mHandler = handler; + mTag = tag; + mScheduler = scheduler; + mSensorId = sensorId; + mUserId = userId; + mCallback = callback; + } + + @Override + public void onStateChanged(int cookie, byte state) { + // TODO(b/162973174) + } + + @Override + public void onChallengeGenerated(int sensorId, int userId, long challenge) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof FaceGenerateChallengeClient)) { + Slog.e(mTag, "onChallengeGenerated for wrong client: " + + Utils.getClientName(client)); + return; + } + + final FaceGenerateChallengeClient generateChallengeClient = + (FaceGenerateChallengeClient) client; + generateChallengeClient.onChallengeGenerated(mSensorId, mUserId, challenge); + }); + } + + @Override + public void onChallengeRevoked(int sensorId, int userId, long challenge) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof FaceRevokeChallengeClient)) { + Slog.e(mTag, "onChallengeRevoked for wrong client: " + + Utils.getClientName(client)); + return; + } + + final FaceRevokeChallengeClient revokeChallengeClient = + (FaceRevokeChallengeClient) client; + revokeChallengeClient.onChallengeRevoked(mSensorId, mUserId, challenge); + }); + } + + @Override + public void onAcquired(byte info, int vendorCode) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof AcquisitionClient)) { + Slog.e(mTag, "onAcquired for non-acquisition client: " + + Utils.getClientName(client)); + return; + } + + final AcquisitionClient acquisitionClient = (AcquisitionClient) client; + acquisitionClient.onAcquired(info, vendorCode); + }); + } + + @Override + public void onError(byte error, int vendorCode) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + Slog.d(mTag, "onError" + + ", client: " + Utils.getClientName(client) + + ", error: " + error + + ", vendorCode: " + vendorCode); + if (!(client instanceof Interruptable)) { + Slog.e(mTag, "onError for non-error consumer: " + + Utils.getClientName(client)); + return; + } + + final Interruptable interruptable = (Interruptable) client; + interruptable.onError(error, vendorCode); + + if (error == Error.HW_UNAVAILABLE) { + mCallback.onHardwareUnavailable(); + } + }); + } + + @Override + public void onEnrollmentProgress(int enrollmentId, int remaining) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof FaceEnrollClient)) { + Slog.e(mTag, "onEnrollmentProgress for non-enroll client: " + + Utils.getClientName(client)); + return; + } + + final int currentUserId = client.getTargetUserId(); + final CharSequence name = FaceUtils.getInstance(mSensorId) + .getUniqueName(mContext, currentUserId); + final Face face = new Face(name, enrollmentId, mSensorId); + + final FaceEnrollClient enrollClient = (FaceEnrollClient) client; + enrollClient.onEnrollResult(face, remaining); + }); + } + + @Override + public void onAuthenticationSucceeded(int enrollmentId, HardwareAuthToken hat) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof AuthenticationConsumer)) { + Slog.e(mTag, "onAuthenticationSucceeded for non-authentication consumer: " + + Utils.getClientName(client)); + return; + } + + final AuthenticationConsumer authenticationConsumer = + (AuthenticationConsumer) client; + final Face face = new Face("" /* name */, enrollmentId, mSensorId); + final byte[] byteArray = HardwareAuthTokenUtils.toByteArray(hat); + final ArrayList byteList = new ArrayList<>(); + for (byte b : byteArray) { + byteList.add(b); + } + authenticationConsumer.onAuthenticated(face, true /* authenticated */, byteList); + }); + } + + @Override + public void onAuthenticationFailed() { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof AuthenticationConsumer)) { + Slog.e(mTag, "onAuthenticationFailed for non-authentication consumer: " + + Utils.getClientName(client)); + return; + } + + final AuthenticationConsumer authenticationConsumer = + (AuthenticationConsumer) client; + final Face face = new Face("" /* name */, 0 /* faceId */, mSensorId); + authenticationConsumer.onAuthenticated(face, false /* authenticated */, + null /* hat */); + }); + } + + @Override + public void onLockoutTimed(long durationMillis) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof LockoutConsumer)) { + Slog.e(mTag, "onLockoutTimed for non-lockout consumer: " + + Utils.getClientName(client)); + return; + } + + final LockoutConsumer lockoutConsumer = (LockoutConsumer) client; + lockoutConsumer.onLockoutTimed(durationMillis); + }); + } + + @Override + public void onLockoutPermanent() { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof LockoutConsumer)) { + Slog.e(mTag, "onLockoutPermanent for non-lockout consumer: " + + Utils.getClientName(client)); + return; + } + + final LockoutConsumer lockoutConsumer = (LockoutConsumer) client; + lockoutConsumer.onLockoutPermanent(); + }); + } + + @Override + public void onLockoutCleared() { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof FaceResetLockoutClient)) { + Slog.e(mTag, "onLockoutCleared for non-resetLockout client: " + + Utils.getClientName(client)); + return; + } + + final FaceResetLockoutClient resetLockoutClient = (FaceResetLockoutClient) client; + resetLockoutClient.onLockoutCleared(); + }); + } + + @Override + public void onInteractionDetected() { + // no-op + } + + @Override + public void onEnrollmentsEnumerated(int[] enrollmentIds) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof EnumerateConsumer)) { + Slog.e(mTag, "onEnrollmentsEnumerated for non-enumerate consumer: " + + Utils.getClientName(client)); + return; + } + + final EnumerateConsumer enumerateConsumer = + (EnumerateConsumer) client; + if (enrollmentIds.length > 0) { + for (int i = 0; i < enrollmentIds.length; ++i) { + final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId); + enumerateConsumer.onEnumerationResult(face, enrollmentIds.length - i - 1); + } + } else { + enumerateConsumer.onEnumerationResult(null /* identifier */, 0 /* remaining */); + } + }); + } + + @Override + public void onEnrollmentsRemoved(int[] enrollmentIds) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof RemovalConsumer)) { + Slog.e(mTag, "onRemoved for non-removal consumer: " + + Utils.getClientName(client)); + return; + } + + final RemovalConsumer removalConsumer = (RemovalConsumer) client; + if (enrollmentIds.length > 0) { + for (int i = 0; i < enrollmentIds.length; i++) { + final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId); + removalConsumer.onRemoved(face, enrollmentIds.length - i - 1); + } + } else { + removalConsumer.onRemoved(null /* identifier */, 0 /* remaining */); + } + }); + } + + @Override + public void onAuthenticatorIdRetrieved(long authenticatorId) { + mHandler.post(() -> { + final ClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof FaceGetAuthenticatorIdClient)) { + Slog.e(mTag, "onAuthenticatorIdRetrieved for wrong consumer: " + + Utils.getClientName(client)); + return; + } + + final FaceGetAuthenticatorIdClient getAuthenticatorIdClient = + (FaceGetAuthenticatorIdClient) client; + getAuthenticatorIdClient.onAuthenticatorIdRetrieved(authenticatorId); + }); + } + + @Override + public void onAuthenticatorIdInvalidated() { + // TODO(b/159667191) + } + + } +}