For older, non-AIDL implementations, only {@code helpCode} and {@code helpMessage} are * supported. Sensible default values will be provided for all other arguments. * * @param helpCode An integer identifying the capture status for this frame. * @param helpMessage A human-readable help string that can be shown in UI. * @param cell The cell captured during this frame of enrollment, if any. * @param stage An integer representing the current stage of enrollment. * @param pan The horizontal pan of the detected face. Values in the range [-1, 1] * indicate a good capture. * @param tilt The vertical tilt of the detected face. Values in the range [-1, 1] * indicate a good capture. * @param distance The distance of the detected face from the device. Values in * the range [-1, 1] indicate a good capture. */ public void onEnrollmentFrame( int helpCode, @Nullable CharSequence helpMessage, @Nullable FaceEnrollCell cell, @FaceEnrollStages.FaceEnrollStage int stage, float pan, float tilt, float distance) { onEnrollmentHelp(helpCode, helpMessage); } /** * Called as each enrollment step progresses. Enrollment is considered complete when * remaining reaches 0. This function will not be called if enrollment fails. See * {@link EnrollmentCallback#onEnrollmentError(int, CharSequence)} * * @param remaining The number of remaining steps */ public void onEnrollmentProgress(int remaining) { } } /** * Callback structure provided to {@link #remove}. Users of {@link FaceManager} * may * optionally provide an implementation of this to * {@link #remove(Face, int, RemovalCallback)} for listening to face template * removal events. * * @hide */ public abstract static class RemovalCallback { /** * Called when the given face can't be removed. * * @param face The face that the call attempted to remove * @param errMsgId An associated error message id * @param errString An error message indicating why the face id can't be removed */ public void onRemovalError(Face face, int errMsgId, CharSequence errString) { } /** * Called when a given face is successfully removed. * * @param face The face template that was removed. */ public void onRemovalSucceeded(@Nullable Face face, int remaining) { } } /** * @hide */ public abstract static class LockoutResetCallback { /** * Called when lockout period expired and clients are allowed to listen for face * authentication * again. */ public void onLockoutReset(int sensorId) { } } /** * @hide */ public abstract static class SetFeatureCallback { public abstract void onCompleted(boolean success, int feature); } /** * @hide */ public abstract static class GetFeatureCallback { public abstract void onCompleted(boolean success, int[] features, boolean[] featureState); } /** * Callback structure provided to {@link #generateChallenge(int, int, * GenerateChallengeCallback)}. * * @hide */ public interface GenerateChallengeCallback { /** * Invoked when a challenge has been generated. */ void onGenerateChallengeResult(int sensorId, int userId, long challenge); } private class OnEnrollCancelListener implements OnCancelListener { @Override public void onCancel() { cancelEnrollment(); } } private class OnAuthenticationCancelListener implements OnCancelListener { private final long mAuthRequestId; OnAuthenticationCancelListener(long id) { mAuthRequestId = id; } @Override public void onCancel() { Slog.d(TAG, "Cancel face authentication requested for: " + mAuthRequestId); cancelAuthentication(mAuthRequestId); } } private class OnFaceDetectionCancelListener implements OnCancelListener { private final long mAuthRequestId; OnFaceDetectionCancelListener(long id) { mAuthRequestId = id; } @Override public void onCancel() { Slog.d(TAG, "Cancel face detect requested for: " + mAuthRequestId); cancelFaceDetect(mAuthRequestId); } } private class MyHandler extends Handler { private MyHandler(Context context) { super(context.getMainLooper()); } private MyHandler(Looper looper) { super(looper); } @Override public void handleMessage(android.os.Message msg) { Trace.beginSection("FaceManager#handleMessage: " + Integer.toString(msg.what)); switch (msg.what) { case MSG_ENROLL_RESULT: sendEnrollResult((Face) msg.obj, msg.arg1 /* remaining */); break; case MSG_ACQUIRED: sendAcquiredResult(msg.arg1 /* acquire info */, msg.arg2 /* vendorCode */); break; case MSG_AUTHENTICATION_SUCCEEDED: sendAuthenticatedSucceeded((Face) msg.obj, msg.arg1 /* userId */, msg.arg2 == 1 /* isStrongBiometric */); break; case MSG_AUTHENTICATION_FAILED: sendAuthenticatedFailed(); break; case MSG_ERROR: sendErrorResult(msg.arg1 /* errMsgId */, msg.arg2 /* vendorCode */); break; case MSG_REMOVED: sendRemovedResult((Face) msg.obj, msg.arg1 /* remaining */); break; case MSG_SET_FEATURE_COMPLETED: sendSetFeatureCompleted((boolean) msg.obj /* success */, msg.arg1 /* feature */); break; case MSG_GET_FEATURE_COMPLETED: SomeArgs args = (SomeArgs) msg.obj; sendGetFeatureCompleted((boolean) args.arg1 /* success */, (int[]) args.arg2 /* features */, (boolean[]) args.arg3 /* featureState */); args.recycle(); break; case MSG_CHALLENGE_GENERATED: sendChallengeGenerated(msg.arg1 /* sensorId */, msg.arg2 /* userId */, (long) msg.obj /* challenge */); break; case MSG_FACE_DETECTED: sendFaceDetected(msg.arg1 /* sensorId */, msg.arg2 /* userId */, (boolean) msg.obj /* isStrongBiometric */); break; case MSG_AUTHENTICATION_FRAME: sendAuthenticationFrame((FaceAuthenticationFrame) msg.obj /* frame */); break; case MSG_ENROLLMENT_FRAME: sendEnrollmentFrame((FaceEnrollFrame) msg.obj /* frame */); break; default: Slog.w(TAG, "Unknown message: " + msg.what); } Trace.endSection(); } } private void sendSetFeatureCompleted(boolean success, int feature) { if (mSetFeatureCallback == null) { return; } mSetFeatureCallback.onCompleted(success, feature); } private void sendGetFeatureCompleted(boolean success, int[] features, boolean[] featureState) { if (mGetFeatureCallback == null) { return; } mGetFeatureCallback.onCompleted(success, features, featureState); } private void sendChallengeGenerated(int sensorId, int userId, long challenge) { if (mGenerateChallengeCallback == null) { return; } mGenerateChallengeCallback.onGenerateChallengeResult(sensorId, userId, challenge); } private void sendFaceDetected(int sensorId, int userId, boolean isStrongBiometric) { if (mFaceDetectionCallback == null) { Slog.e(TAG, "sendFaceDetected, callback null"); return; } mFaceDetectionCallback.onFaceDetected(sensorId, userId, isStrongBiometric); } private void sendRemovedResult(Face face, int remaining) { if (mRemovalCallback == null) { return; } mRemovalCallback.onRemovalSucceeded(face, remaining); } private void sendErrorResult(int errMsgId, int vendorCode) { // emulate HAL 2.1 behavior and send real errMsgId final int clientErrMsgId = errMsgId == FACE_ERROR_VENDOR ? (vendorCode + FACE_ERROR_VENDOR_BASE) : errMsgId; if (mEnrollmentCallback != null) { mEnrollmentCallback.onEnrollmentError(clientErrMsgId, getErrorString(mContext, errMsgId, vendorCode)); } else if (mAuthenticationCallback != null) { mAuthenticationCallback.onAuthenticationError(clientErrMsgId, getErrorString(mContext, errMsgId, vendorCode)); } else if (mRemovalCallback != null) { mRemovalCallback.onRemovalError(mRemovalFace, clientErrMsgId, getErrorString(mContext, errMsgId, vendorCode)); } } private void sendEnrollResult(Face face, int remaining) { if (mEnrollmentCallback != null) { mEnrollmentCallback.onEnrollmentProgress(remaining); } } private void sendAuthenticatedSucceeded(Face face, int userId, boolean isStrongBiometric) { if (mAuthenticationCallback != null) { final AuthenticationResult result = new AuthenticationResult(mCryptoObject, face, userId, isStrongBiometric); mAuthenticationCallback.onAuthenticationSucceeded(result); } } private void sendAuthenticatedFailed() { if (mAuthenticationCallback != null) { mAuthenticationCallback.onAuthenticationFailed(); } } private void sendAcquiredResult(int acquireInfo, int vendorCode) { if (mAuthenticationCallback != null) { final FaceAuthenticationFrame frame = new FaceAuthenticationFrame( new FaceDataFrame(acquireInfo, vendorCode)); sendAuthenticationFrame(frame); } else if (mEnrollmentCallback != null) { final FaceEnrollFrame frame = new FaceEnrollFrame( null /* cell */, FaceEnrollStages.UNKNOWN, new FaceDataFrame(acquireInfo, vendorCode)); sendEnrollmentFrame(frame); } } private void sendAuthenticationFrame(@Nullable FaceAuthenticationFrame frame) { if (frame == null) { Slog.w(TAG, "Received null authentication frame"); } else if (mAuthenticationCallback != null) { // TODO(b/178414967): Send additional frame data to callback final int acquireInfo = frame.getData().getAcquiredInfo(); final int vendorCode = frame.getData().getVendorCode(); final int helpCode = getHelpCode(acquireInfo, vendorCode); final String helpMessage = getAuthHelpMessage(mContext, acquireInfo, vendorCode); mAuthenticationCallback.onAuthenticationAcquired(acquireInfo); // Ensure that only non-null help messages are sent. if (helpMessage != null) { mAuthenticationCallback.onAuthenticationHelp(helpCode, helpMessage); } } } private void sendEnrollmentFrame(@Nullable FaceEnrollFrame frame) { if (frame == null) { Slog.w(TAG, "Received null enrollment frame"); } else if (mEnrollmentCallback != null) { final FaceDataFrame data = frame.getData(); final int acquireInfo = data.getAcquiredInfo(); final int vendorCode = data.getVendorCode(); final int helpCode = getHelpCode(acquireInfo, vendorCode); final String helpMessage = getEnrollHelpMessage(mContext, acquireInfo, vendorCode); mEnrollmentCallback.onEnrollmentFrame( helpCode, helpMessage, frame.getCell(), frame.getStage(), data.getPan(), data.getTilt(), data.getDistance()); } } private static int getHelpCode(int acquireInfo, int vendorCode) { return acquireInfo == FACE_ACQUIRED_VENDOR ? vendorCode + FACE_ACQUIRED_VENDOR_BASE : acquireInfo; } /** * @hide */ @Nullable public static String getAuthHelpMessage(Context context, int acquireInfo, int vendorCode) { switch (acquireInfo) { // No help message is needed for a good capture. case FACE_ACQUIRED_GOOD: case FACE_ACQUIRED_START: return null; // Consolidate positional feedback to reduce noise during authentication. case FACE_ACQUIRED_NOT_DETECTED: case FACE_ACQUIRED_TOO_CLOSE: case FACE_ACQUIRED_TOO_FAR: case FACE_ACQUIRED_TOO_HIGH: case FACE_ACQUIRED_TOO_LOW: case FACE_ACQUIRED_TOO_RIGHT: case FACE_ACQUIRED_TOO_LEFT: case FACE_ACQUIRED_POOR_GAZE: case FACE_ACQUIRED_PAN_TOO_EXTREME: case FACE_ACQUIRED_TILT_TOO_EXTREME: case FACE_ACQUIRED_ROLL_TOO_EXTREME: return context.getString(R.string.face_acquired_poor_gaze); // Provide more detailed feedback for other soft errors. case FACE_ACQUIRED_INSUFFICIENT: return context.getString(R.string.face_acquired_insufficient); case FACE_ACQUIRED_TOO_BRIGHT: return context.getString(R.string.face_acquired_too_bright); case FACE_ACQUIRED_TOO_DARK: return context.getString(R.string.face_acquired_too_dark); case FACE_ACQUIRED_TOO_MUCH_MOTION: return context.getString(R.string.face_acquired_too_much_motion); case FACE_ACQUIRED_RECALIBRATE: return context.getString(R.string.face_acquired_recalibrate); case FACE_ACQUIRED_TOO_DIFFERENT: return context.getString(R.string.face_acquired_too_different); case FACE_ACQUIRED_TOO_SIMILAR: return context.getString(R.string.face_acquired_too_similar); case FACE_ACQUIRED_FACE_OBSCURED: return context.getString(R.string.face_acquired_obscured); case FACE_ACQUIRED_SENSOR_DIRTY: return context.getString(R.string.face_acquired_sensor_dirty); // Find and return the appropriate vendor-specific message. case FACE_ACQUIRED_VENDOR: { String[] msgArray = context.getResources().getStringArray( R.array.face_acquired_vendor); if (vendorCode < msgArray.length) { return msgArray[vendorCode]; } } } Slog.w(TAG, "Unknown authentication acquired message: " + acquireInfo + ", " + vendorCode); return null; } /** * @hide */ @Nullable public static String getEnrollHelpMessage(Context context, int acquireInfo, int vendorCode) { switch (acquireInfo) { case FACE_ACQUIRED_GOOD: case FACE_ACQUIRED_START: return null; case FACE_ACQUIRED_INSUFFICIENT: return context.getString(R.string.face_acquired_insufficient); case FACE_ACQUIRED_TOO_BRIGHT: return context.getString(R.string.face_acquired_too_bright); case FACE_ACQUIRED_TOO_DARK: return context.getString(R.string.face_acquired_too_dark); case FACE_ACQUIRED_TOO_CLOSE: return context.getString(R.string.face_acquired_too_close); case FACE_ACQUIRED_TOO_FAR: return context.getString(R.string.face_acquired_too_far); case FACE_ACQUIRED_TOO_HIGH: // TODO(b/181269243): Change back once error codes are fixed. return context.getString(R.string.face_acquired_too_low); case FACE_ACQUIRED_TOO_LOW: // TODO(b/181269243) Change back once error codes are fixed. return context.getString(R.string.face_acquired_too_high); case FACE_ACQUIRED_TOO_RIGHT: // TODO(b/181269243) Change back once error codes are fixed. return context.getString(R.string.face_acquired_too_left); case FACE_ACQUIRED_TOO_LEFT: // TODO(b/181269243) Change back once error codes are fixed. return context.getString(R.string.face_acquired_too_right); case FACE_ACQUIRED_POOR_GAZE: return context.getString(R.string.face_acquired_poor_gaze); case FACE_ACQUIRED_NOT_DETECTED: return context.getString(R.string.face_acquired_not_detected); case FACE_ACQUIRED_TOO_MUCH_MOTION: return context.getString(R.string.face_acquired_too_much_motion); case FACE_ACQUIRED_RECALIBRATE: return context.getString(R.string.face_acquired_recalibrate); case FACE_ACQUIRED_TOO_DIFFERENT: return context.getString(R.string.face_acquired_too_different); case FACE_ACQUIRED_TOO_SIMILAR: return context.getString(R.string.face_acquired_too_similar); case FACE_ACQUIRED_PAN_TOO_EXTREME: return context.getString(R.string.face_acquired_pan_too_extreme); case FACE_ACQUIRED_TILT_TOO_EXTREME: return context.getString(R.string.face_acquired_tilt_too_extreme); case FACE_ACQUIRED_ROLL_TOO_EXTREME: return context.getString(R.string.face_acquired_roll_too_extreme); case FACE_ACQUIRED_FACE_OBSCURED: case FACE_ACQUIRED_DARK_GLASSES_DETECTED: case FACE_ACQUIRED_MOUTH_COVERING_DETECTED: return context.getString(R.string.face_acquired_obscured); case FACE_ACQUIRED_SENSOR_DIRTY: return context.getString(R.string.face_acquired_sensor_dirty); case FACE_ACQUIRED_VENDOR: { String[] msgArray = context.getResources().getStringArray( R.array.face_acquired_vendor); if (vendorCode < msgArray.length) { return msgArray[vendorCode]; } } } Slog.w(TAG, "Unknown enrollment acquired message: " + acquireInfo + ", " + vendorCode); return null; } }