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

Commit bbd8a6a6 authored by Diya Bera's avatar Diya Bera
Browse files

Distinct callback for each request

1. Created a callback class for face and fingerprint
2. Each biometric request has a new receiver
3. This ensures that the accurate callback is used, instead of the most
   recent one

Test: atest FingerprintManagerTest FaceManagerTest
Bug: 314165741
Change-Id: I37bac28544c422d5cdaa179e464927811f0bfc49
parent 3582d530
Loading
Loading
Loading
Loading
+321 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 android.hardware.face;

import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_VENDOR;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_VENDOR_BASE;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_VENDOR;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_VENDOR_BASE;
import static android.hardware.face.FaceManager.getAuthHelpMessage;
import static android.hardware.face.FaceManager.getEnrollHelpMessage;
import static android.hardware.face.FaceManager.getErrorString;

import android.content.Context;
import android.hardware.biometrics.CryptoObject;
import android.hardware.face.FaceManager.AuthenticationCallback;
import android.hardware.face.FaceManager.EnrollmentCallback;
import android.hardware.face.FaceManager.FaceDetectionCallback;
import android.hardware.face.FaceManager.GenerateChallengeCallback;
import android.hardware.face.FaceManager.GetFeatureCallback;
import android.hardware.face.FaceManager.RemovalCallback;
import android.hardware.face.FaceManager.SetFeatureCallback;
import android.util.Slog;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * Encapsulates callbacks and client specific information for each face related request.
 * @hide
 */
public class FaceCallback {
    private static final String TAG = " FaceCallback";

    @Nullable
    private AuthenticationCallback mAuthenticationCallback;
    @Nullable
    private EnrollmentCallback mEnrollmentCallback;
    @Nullable
    private RemovalCallback mRemovalCallback;
    @Nullable
    private GenerateChallengeCallback mGenerateChallengeCallback;
    @Nullable
    private FaceDetectionCallback mFaceDetectionCallback;
    @Nullable
    private SetFeatureCallback mSetFeatureCallback;
    @Nullable
    private GetFeatureCallback mGetFeatureCallback;
    @Nullable
    private Face mRemovalFace;
    @Nullable
    private CryptoObject mCryptoObject;

    /**
     * Construction for face authentication client callback.
     */
    FaceCallback(AuthenticationCallback authenticationCallback, CryptoObject cryptoObject) {
        mAuthenticationCallback = authenticationCallback;
        mCryptoObject = cryptoObject;
    }

    /**
     * Construction for face detect client callback.
     */
    FaceCallback(FaceDetectionCallback faceDetectionCallback) {
        mFaceDetectionCallback = faceDetectionCallback;
    }

    /**
     * Construction for face enroll client callback.
     */
    FaceCallback(EnrollmentCallback enrollmentCallback) {
        mEnrollmentCallback = enrollmentCallback;
    }

    /**
     * Construction for face generate challenge client callback.
     */
    FaceCallback(GenerateChallengeCallback generateChallengeCallback) {
        mGenerateChallengeCallback = generateChallengeCallback;
    }

    /**
     * Construction for face set feature client callback.
     */
    FaceCallback(SetFeatureCallback setFeatureCallback) {
        mSetFeatureCallback = setFeatureCallback;
    }

    /**
     * Construction for face get feature client callback.
     */
    FaceCallback(GetFeatureCallback getFeatureCallback) {
        mGetFeatureCallback = getFeatureCallback;
    }

    /**
     * Construction for single face removal client callback.
     */
    FaceCallback(RemovalCallback removalCallback, Face removalFace) {
        mRemovalCallback = removalCallback;
        mRemovalFace = removalFace;
    }

    /**
     * Construction for all face removal client callback.
     */
    FaceCallback(RemovalCallback removalCallback) {
        mRemovalCallback = removalCallback;
    }

    /**
     * Propagate set feature completed via the callback.
     * @param success if the operation was completed successfully
     * @param feature the feature that was set
     */
    public void sendSetFeatureCompleted(boolean success, int feature) {
        if (mSetFeatureCallback == null) {
            return;
        }
        mSetFeatureCallback.onCompleted(success, feature);
    }

    /**
     * Propagate get feature completed via the callback.
     * @param success if the operation was completed successfully
     * @param features list of features available
     * @param featureState status of the features corresponding to the previous parameter
     */
    public void sendGetFeatureCompleted(boolean success, int[] features, boolean[] featureState) {
        if (mGetFeatureCallback == null) {
            return;
        }
        mGetFeatureCallback.onCompleted(success, features, featureState);
    }

    /**
     * Propagate challenge generated completed via the callback.
     * @param sensorId id of the corresponding sensor
     * @param userId id of the corresponding sensor
     * @param challenge value of the challenge generated
     */
    public void sendChallengeGenerated(int sensorId, int userId, long challenge) {
        if (mGenerateChallengeCallback == null) {
            return;
        }
        mGenerateChallengeCallback.onGenerateChallengeResult(sensorId, userId, challenge);
    }

    /**
     * Propagate face detected completed via the callback.
     * @param sensorId id of the corresponding sensor
     * @param userId id of the corresponding user
     * @param isStrongBiometric if the sensor is strong or not
     */
    public void sendFaceDetected(int sensorId, int userId, boolean isStrongBiometric) {
        if (mFaceDetectionCallback == null) {
            Slog.e(TAG, "sendFaceDetected, callback null");
            return;
        }
        mFaceDetectionCallback.onFaceDetected(sensorId, userId, isStrongBiometric);
    }

    /**
     * Propagate remove face completed via the callback.
     * @param face removed identifier
     * @param remaining number of face enrollments remaining
     */
    public void sendRemovedResult(Face face, int remaining) {
        if (mRemovalCallback == null) {
            return;
        }
        mRemovalCallback.onRemovalSucceeded(face, remaining);
    }

    /**
     * Propagate errors via the callback.
     * @param context corresponding context
     * @param errMsgId represents the framework error id
     * @param vendorCode represents the vendor error code
     */
    public void sendErrorResult(Context context, 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(context, errMsgId, vendorCode));
        } else if (mAuthenticationCallback != null) {
            mAuthenticationCallback.onAuthenticationError(clientErrMsgId,
                    getErrorString(context, errMsgId, vendorCode));
        } else if (mRemovalCallback != null) {
            mRemovalCallback.onRemovalError(mRemovalFace, clientErrMsgId,
                    getErrorString(context, errMsgId, vendorCode));
        } else if (mFaceDetectionCallback != null) {
            mFaceDetectionCallback.onDetectionError(errMsgId);
            mFaceDetectionCallback = null;
        }
    }

    /**
     * Propagate enroll progress via the callback.
     * @param remaining number of enrollment steps remaining
     */
    public void sendEnrollResult(int remaining) {
        if (mEnrollmentCallback != null) {
            mEnrollmentCallback.onEnrollmentProgress(remaining);
        }
    }

    /**
     * Propagate authentication succeeded via the callback.
     * @param face matched identifier
     * @param userId id of the corresponding user
     * @param isStrongBiometric if the sensor is strong or not
     */
    public void sendAuthenticatedSucceeded(Face face, int userId, boolean isStrongBiometric) {
        if (mAuthenticationCallback != null) {
            final FaceManager.AuthenticationResult result = new FaceManager.AuthenticationResult(
                    mCryptoObject, face, userId, isStrongBiometric);
            mAuthenticationCallback.onAuthenticationSucceeded(result);
        }
    }

    /**
     * Propagate authentication failed via the callback.
     */
    public void sendAuthenticatedFailed() {
        if (mAuthenticationCallback != null) {
            mAuthenticationCallback.onAuthenticationFailed();
        }
    }

    /**
     * Propagate acquired result via the callback.
     * @param context corresponding context
     * @param acquireInfo represents the framework acquired id
     * @param vendorCode represents the vendor acquired code
     */
    public void sendAcquiredResult(Context context, int acquireInfo, int vendorCode) {
        if (mAuthenticationCallback != null) {
            final FaceAuthenticationFrame frame = new FaceAuthenticationFrame(
                    new FaceDataFrame(acquireInfo, vendorCode));
            sendAuthenticationFrame(context, frame);
        } else if (mEnrollmentCallback != null) {
            final FaceEnrollFrame frame = new FaceEnrollFrame(
                    null /* cell */,
                    FaceEnrollStages.UNKNOWN,
                    new FaceDataFrame(acquireInfo, vendorCode));
            sendEnrollmentFrame(context, frame);
        }
    }

    /**
     * Propagate authentication frame via the callback.
     * @param context corresponding context
     * @param frame authentication frame to be sent
     */
    public void sendAuthenticationFrame(@NonNull Context context,
            @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(context, acquireInfo, vendorCode);
            mAuthenticationCallback.onAuthenticationAcquired(acquireInfo);

            // Ensure that only non-null help messages are sent.
            if (helpMessage != null) {
                mAuthenticationCallback.onAuthenticationHelp(helpCode, helpMessage);
            }
        }
    }

    /**
     * Propagate enrollment via the callback.
     * @param context corresponding context
     * @param frame enrollment frame to be sent
     */
    public void sendEnrollmentFrame(Context context, @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(context, 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;
    }
}
+57 −269

File changed.

Preview size limit exceeded, changes collapsed.

+299 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 android.hardware.fingerprint;

import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR_BASE;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR_BASE;
import static android.hardware.fingerprint.FingerprintManager.getAcquiredString;
import static android.hardware.fingerprint.FingerprintManager.getErrorString;

import android.annotation.IntDef;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.hardware.fingerprint.FingerprintManager.CryptoObject;
import android.hardware.fingerprint.FingerprintManager.EnrollmentCallback;
import android.hardware.fingerprint.FingerprintManager.FingerprintDetectionCallback;
import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback;
import android.hardware.fingerprint.FingerprintManager.RemovalCallback;
import android.util.Slog;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * Encapsulates callbacks and client specific information for each fingerprint related request.
 * @hide
 */
public class FingerprintCallback {
    private static final String TAG = "FingerprintCallback";
    public static final int REMOVE_SINGLE = 1;
    public static final int REMOVE_ALL = 2;
    @IntDef({REMOVE_SINGLE, REMOVE_ALL})
    public @interface RemoveRequest {}
    @Nullable
    private AuthenticationCallback mAuthenticationCallback;
    @Nullable
    private EnrollmentCallback mEnrollmentCallback;
    @Nullable
    private RemovalCallback mRemovalCallback;
    @Nullable
    private GenerateChallengeCallback mGenerateChallengeCallback;
    @Nullable
    private FingerprintDetectionCallback mFingerprintDetectionCallback;
    @Nullable
    private CryptoObject mCryptoObject;
    @Nullable
    private @RemoveRequest int mRemoveRequest;
    @Nullable
    private Fingerprint mRemoveFingerprint;

    /**
     * Construction for fingerprint authentication client callback.
     */
    FingerprintCallback(@NonNull AuthenticationCallback authenticationCallback,
            @Nullable CryptoObject cryptoObject) {
        mAuthenticationCallback = authenticationCallback;
        mCryptoObject = cryptoObject;
    }

    /**
     * Construction for fingerprint detect client callback.
     */
    FingerprintCallback(@NonNull FingerprintDetectionCallback fingerprintDetectionCallback) {
        mFingerprintDetectionCallback = fingerprintDetectionCallback;
    }

    /**
     * Construction for fingerprint enroll client callback.
     */
    FingerprintCallback(@NonNull EnrollmentCallback enrollmentCallback) {
        mEnrollmentCallback = enrollmentCallback;
    }

    /**
     * Construction for fingerprint generate challenge client callback.
     */
    FingerprintCallback(@NonNull GenerateChallengeCallback generateChallengeCallback) {
        mGenerateChallengeCallback = generateChallengeCallback;
    }

    /**
     * Construction for fingerprint removal client callback.
     */
    FingerprintCallback(@NonNull RemovalCallback removalCallback, @RemoveRequest int removeRequest,
            @Nullable Fingerprint removeFingerprint) {
        mRemovalCallback = removalCallback;
        mRemoveRequest = removeRequest;
        mRemoveFingerprint = removeFingerprint;
    }

    /**
     * Propagate enroll progress via the callback.
     * @param remaining number of enrollment steps remaining
     */
    public void sendEnrollResult(int remaining) {
        if (mEnrollmentCallback != null) {
            mEnrollmentCallback.onEnrollmentProgress(remaining);
        }
    }

    /**
     * Propagate remove face completed via the callback.
     * @param fingerprint removed identifier
     * @param remaining number of face enrollments remaining
     */
    public void sendRemovedResult(@Nullable Fingerprint fingerprint, int remaining) {
        if (mRemovalCallback == null) {
            return;
        }

        if (mRemoveRequest == REMOVE_SINGLE) {
            if (fingerprint == null) {
                Slog.e(TAG, "Received MSG_REMOVED, but fingerprint is null");
                return;
            }

            if (mRemoveFingerprint == null) {
                Slog.e(TAG, "Missing fingerprint");
                return;
            }

            final int fingerId = fingerprint.getBiometricId();
            int reqFingerId = mRemoveFingerprint.getBiometricId();
            if (reqFingerId != 0 && fingerId != 0 && fingerId != reqFingerId) {
                Slog.w(TAG, "Finger id didn't match: " + fingerId + " != " + reqFingerId);
                return;
            }
        }

        mRemovalCallback.onRemovalSucceeded(fingerprint, remaining);
    }

    /**
     * Propagate authentication succeeded via the callback.
     * @param fingerprint matched identifier
     * @param userId id of the corresponding user
     * @param isStrongBiometric if the sensor is strong or not
     */
    public void sendAuthenticatedSucceeded(@NonNull Fingerprint fingerprint, int userId,
            boolean isStrongBiometric) {
        if (mAuthenticationCallback == null) {
            Slog.e(TAG, "Authentication succeeded but callback is null.");
            return;
        }

        final AuthenticationResult result = new AuthenticationResult(mCryptoObject, fingerprint,
                userId, isStrongBiometric);
        mAuthenticationCallback.onAuthenticationSucceeded(result);
    }

    /**
     * Propagate authentication failed via the callback.
     */
    public void sendAuthenticatedFailed() {
        if (mAuthenticationCallback != null) {
            mAuthenticationCallback.onAuthenticationFailed();
        }
    }

    /**
     * Propagate acquired result via the callback.
     * @param context corresponding context
     * @param acquireInfo represents the framework acquired id
     * @param vendorCode represents the vendor acquired code
     */
    public void sendAcquiredResult(@NonNull Context context, int acquireInfo, int vendorCode) {
        if (mAuthenticationCallback != null) {
            mAuthenticationCallback.onAuthenticationAcquired(acquireInfo);
        }
        if (mEnrollmentCallback != null && acquireInfo != FINGERPRINT_ACQUIRED_START) {
            mEnrollmentCallback.onAcquired(acquireInfo == FINGERPRINT_ACQUIRED_GOOD);
        }
        final String msg = getAcquiredString(context, acquireInfo, vendorCode);
        if (msg == null) {
            return;
        }
        // emulate HAL 2.1 behavior and send real acquiredInfo
        final int clientInfo = acquireInfo == FINGERPRINT_ACQUIRED_VENDOR
                ? (vendorCode + FINGERPRINT_ACQUIRED_VENDOR_BASE) : acquireInfo;
        if (mEnrollmentCallback != null) {
            mEnrollmentCallback.onEnrollmentHelp(clientInfo, msg);
        } else if (mAuthenticationCallback != null) {
            if (acquireInfo != FINGERPRINT_ACQUIRED_START) {
                mAuthenticationCallback.onAuthenticationHelp(clientInfo, msg);
            }
        }
    }

    /**
     * Propagate errors via the callback.
     * @param context corresponding context
     * @param errMsgId represents the framework error id
     * @param vendorCode represents the vendor error code
     */
    public void sendErrorResult(@NonNull Context context, int errMsgId, int vendorCode) {
        // emulate HAL 2.1 behavior and send real errMsgId
        final int clientErrMsgId = errMsgId == FINGERPRINT_ERROR_VENDOR
                ? (vendorCode + FINGERPRINT_ERROR_VENDOR_BASE) : errMsgId;
        if (mEnrollmentCallback != null) {
            mEnrollmentCallback.onEnrollmentError(clientErrMsgId,
                    getErrorString(context, errMsgId, vendorCode));
        } else if (mAuthenticationCallback != null) {
            mAuthenticationCallback.onAuthenticationError(clientErrMsgId,
                    getErrorString(context, errMsgId, vendorCode));
        } else if (mRemovalCallback != null) {
            mRemovalCallback.onRemovalError(mRemoveFingerprint, clientErrMsgId,
                    getErrorString(context, errMsgId, vendorCode));
        } else if (mFingerprintDetectionCallback != null) {
            mFingerprintDetectionCallback.onDetectionError(errMsgId);
            mFingerprintDetectionCallback = null;
        }
    }

    /**
     * Propagate challenge generated completed via the callback.
     * @param sensorId id of the corresponding sensor
     * @param userId id of the corresponding sensor
     * @param challenge value of the challenge generated
     */
    public void sendChallengeGenerated(long challenge, int sensorId, int userId) {
        if (mGenerateChallengeCallback == null) {
            Slog.e(TAG, "sendChallengeGenerated, callback null");
            return;
        }
        mGenerateChallengeCallback.onChallengeGenerated(sensorId, userId, challenge);
    }

    /**
     * Propagate fingerprint detected completed via the callback.
     * @param sensorId id of the corresponding sensor
     * @param userId id of the corresponding user
     * @param isStrongBiometric if the sensor is strong or not
     */
    public void sendFingerprintDetected(int sensorId, int userId, boolean isStrongBiometric) {
        if (mFingerprintDetectionCallback == null) {
            Slog.e(TAG, "sendFingerprintDetected, callback null");
            return;
        }
        mFingerprintDetectionCallback.onFingerprintDetected(sensorId, userId, isStrongBiometric);
    }

    /**
     * Propagate udfps pointer down via the callback.
     * @param sensorId id of the corresponding sensor
     */
    public void sendUdfpsPointerDown(int sensorId) {
        if (mAuthenticationCallback == null) {
            Slog.e(TAG, "sendUdfpsPointerDown, callback null");
        } else {
            mAuthenticationCallback.onUdfpsPointerDown(sensorId);
        }

        if (mEnrollmentCallback != null) {
            mEnrollmentCallback.onUdfpsPointerDown(sensorId);
        }
    }

    /**
     * Propagate udfps pointer up via the callback.
     * @param sensorId id of the corresponding sensor
     */
    public void sendUdfpsPointerUp(int sensorId) {
        if (mAuthenticationCallback == null) {
            Slog.e(TAG, "sendUdfpsPointerUp, callback null");
        } else {
            mAuthenticationCallback.onUdfpsPointerUp(sensorId);
        }
        if (mEnrollmentCallback != null) {
            mEnrollmentCallback.onUdfpsPointerUp(sensorId);
        }
    }

    /**
     * Propagate udfps overlay shown via the callback.
     */
    public void sendUdfpsOverlayShown() {
        if (mEnrollmentCallback != null) {
            mEnrollmentCallback.onUdfpsOverlayShown();
        }
    }
}
+72 −286

File changed.

Preview size limit exceeded, changes collapsed.

+36 −1
Original line number Diff line number Diff line
@@ -28,7 +28,9 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@@ -97,7 +99,7 @@ public class FaceManagerTest {
        mLooper = new TestLooper();
        mHandler = new Handler(mLooper.getLooper());

        when(mContext.getMainLooper()).thenReturn(mLooper.getLooper());
        when(mContext.getMainThreadHandler()).thenReturn(mHandler);
        when(mContext.getOpPackageName()).thenReturn(PACKAGE_NAME);
        when(mContext.getAttributionTag()).thenReturn(ATTRIBUTION_TAG);
        when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
@@ -210,6 +212,39 @@ public class FaceManagerTest {
        verify(mFaceDetectionCallback).onDetectionError(anyInt());
    }

    @Test
    public void authenticate_onErrorCanceled() throws RemoteException {
        final FaceManager.AuthenticationCallback authenticationCallback1 = mock(
                FaceManager.AuthenticationCallback.class);
        final FaceManager.AuthenticationCallback authenticationCallback2 = mock(
                FaceManager.AuthenticationCallback.class);

        final ArgumentCaptor<IFaceServiceReceiver> faceServiceReceiverArgumentCaptor =
                ArgumentCaptor.forClass(IFaceServiceReceiver.class);

        mFaceManager.authenticate(null, new CancellationSignal(),
                authenticationCallback1, mHandler,
                new FaceAuthenticateOptions.Builder().build());
        mFaceManager.authenticate(null, new CancellationSignal(),
                authenticationCallback2, mHandler,
                new FaceAuthenticateOptions.Builder().build());

        verify(mService, times(2)).authenticate(any(IBinder.class), eq(0L),
                faceServiceReceiverArgumentCaptor.capture(), any());

        final List<IFaceServiceReceiver> faceServiceReceivers =
                faceServiceReceiverArgumentCaptor.getAllValues();
        faceServiceReceivers.get(0).onError(5 /* error */, 0 /* vendorCode */);
        mLooper.dispatchAll();

        verify(authenticationCallback1).onAuthenticationError(eq(5), anyString());
        verify(authenticationCallback2, never()).onAuthenticationError(anyInt(), anyString());

        faceServiceReceivers.get(1).onError(5 /* error */, 0 /* vendorCode */);
        mLooper.dispatchAll();
        verify(authenticationCallback2).onAuthenticationError(eq(5), anyString());
    }

    private void initializeProperties() throws RemoteException {
        verify(mService).addAuthenticatorsRegisteredCallback(mCaptor.capture());

Loading