Expose `FaceManager` APIs as `@SystemApi`.
Enable privileged apps with `USE_BACKGROUND_FACE_AUTHENTICATION` permission access to the face auth APIs for background face auth.
The first use case is Pixel Health (go/rppg-prd). Design doc: go/pixel-health-face-auth (b/312858933)
Android Feature Request Bug: b/305272520
Bug: 318309705
Test: Unit tests.
Change-Id: I037fbc15353cea6fd8f9a97e1e86e90305716f83
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2596f9c..3ae447f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -390,6 +390,7 @@
field @Deprecated public static final String UPDATE_TIME_ZONE_RULES = "android.permission.UPDATE_TIME_ZONE_RULES";
field public static final String UPGRADE_RUNTIME_PERMISSIONS = "android.permission.UPGRADE_RUNTIME_PERMISSIONS";
field public static final String USER_ACTIVITY = "android.permission.USER_ACTIVITY";
+ field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String USE_BACKGROUND_FACE_AUTHENTICATION = "android.permission.USE_BACKGROUND_FACE_AUTHENTICATION";
field public static final String USE_COLORIZED_NOTIFICATIONS = "android.permission.USE_COLORIZED_NOTIFICATIONS";
field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED";
@@ -3546,6 +3547,7 @@
field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation";
field public static final String ETHERNET_SERVICE = "ethernet";
field public static final String EUICC_CARD_SERVICE = "euicc_card";
+ field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String FACE_SERVICE = "face";
field public static final String FONT_SERVICE = "font";
field public static final String HDMI_CONTROL_SERVICE = "hdmi_control";
field public static final String MEDIA_TRANSCODING_SERVICE = "media_transcoding";
@@ -4726,6 +4728,15 @@
}
+package android.hardware.face {
+
+ @FlaggedApi("android.hardware.biometrics.face_background_authentication") public class FaceManager {
+ method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION) public void authenticateInBackground(@Nullable java.util.concurrent.Executor, @Nullable android.hardware.biometrics.BiometricPrompt.CryptoObject, @Nullable android.os.CancellationSignal, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
+ method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(anyOf={"android.permission.USE_BIOMETRIC_INTERNAL", android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION}) public boolean hasEnrolledTemplates();
+ }
+
+}
+
package android.hardware.hdmi {
public abstract class HdmiClient {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 249c0e43..67a3627 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5079,6 +5079,8 @@
* @see #getSystemService
* @see android.hardware.face.FaceManager
*/
+ @FlaggedApi(android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ @SystemApi
public static final String FACE_SERVICE = "face";
/**
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 3ba8be4..8165d44 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -28,3 +28,10 @@
bug: "302735104"
}
+flag {
+ name: "face_background_authentication"
+ namespace: "biometrics_framework"
+ description: "Feature flag for allowing face background authentication with USE_BACKGROUND_FACE_AUTHENTICATION."
+ bug: "318584190"
+}
+
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 02304b5..bae5e7f 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -18,18 +18,23 @@
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.MANAGE_BIOMETRIC;
+import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_NONE;
+import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFaceConstants;
+import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricStateListener;
import android.hardware.biometrics.CryptoObject;
import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
@@ -37,9 +42,9 @@
import android.os.CancellationSignal;
import android.os.CancellationSignal.OnCancelListener;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.IRemoteCallback;
-import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.Trace;
@@ -49,15 +54,21 @@
import android.view.Surface;
import com.android.internal.R;
-import com.android.internal.os.SomeArgs;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* A class that coordinates access to the face authentication hardware.
+ *
+ * <p>Please use {@link BiometricPrompt} for face authentication unless the experience must be
+ * customized for unique system-level utilities, like the lock screen or ambient background usage.
+ *
* @hide
*/
+@FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+@SystemApi
@SystemService(Context.FACE_SERVICE)
public class FaceManager implements BiometricAuthenticator, BiometricFaceConstants {
@@ -88,81 +99,76 @@
@Nullable private GenerateChallengeCallback mGenerateChallengeCallback;
private CryptoObject mCryptoObject;
private Face mRemovalFace;
- private Handler mHandler;
+ private Executor mExecutor;
private List<FaceSensorPropertiesInternal> mProps = new ArrayList<>();
private final IFaceServiceReceiver mServiceReceiver = new IFaceServiceReceiver.Stub() {
@Override // binder call
public void onEnrollResult(Face face, int remaining) {
- mHandler.obtainMessage(MSG_ENROLL_RESULT, remaining, 0, face).sendToTarget();
+ mExecutor.execute(() -> sendEnrollResult(face, remaining));
}
@Override // binder call
public void onAcquired(int acquireInfo, int vendorCode) {
- mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode).sendToTarget();
+ mExecutor.execute(() -> sendAcquiredResult(acquireInfo, vendorCode));
}
@Override // binder call
public void onAuthenticationSucceeded(Face face, int userId, boolean isStrongBiometric) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId,
- isStrongBiometric ? 1 : 0, face).sendToTarget();
+ mExecutor.execute(() -> sendAuthenticatedSucceeded(face, userId, isStrongBiometric));
}
@Override // binder call
public void onFaceDetected(int sensorId, int userId, boolean isStrongBiometric) {
- mHandler.obtainMessage(MSG_FACE_DETECTED, sensorId, userId, isStrongBiometric)
- .sendToTarget();
+ mExecutor.execute(() -> sendFaceDetected(sensorId, userId, isStrongBiometric));
}
@Override // binder call
public void onAuthenticationFailed() {
- mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
+ mExecutor.execute(() -> sendAuthenticatedFailed());
}
@Override // binder call
public void onError(int error, int vendorCode) {
- mHandler.obtainMessage(MSG_ERROR, error, vendorCode).sendToTarget();
+ mExecutor.execute(() -> sendErrorResult(error, vendorCode));
}
@Override // binder call
public void onRemoved(Face face, int remaining) {
- mHandler.obtainMessage(MSG_REMOVED, remaining, 0, face).sendToTarget();
- if (remaining == 0) {
- Settings.Secure.putIntForUser(mContext.getContentResolver(),
- Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0,
- UserHandle.USER_CURRENT);
- }
+ mExecutor.execute(() -> {
+ sendRemovedResult(face, remaining);
+ if (remaining == 0) {
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
+ Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0,
+ UserHandle.USER_CURRENT);
+ }
+ });
}
@Override
public void onFeatureSet(boolean success, int feature) {
- mHandler.obtainMessage(MSG_SET_FEATURE_COMPLETED, feature, 0, success).sendToTarget();
+ mExecutor.execute(() -> sendSetFeatureCompleted(success, feature));
}
@Override
public void onFeatureGet(boolean success, int[] features, boolean[] featureState) {
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = success;
- args.arg2 = features;
- args.arg3 = featureState;
- mHandler.obtainMessage(MSG_GET_FEATURE_COMPLETED, args).sendToTarget();
+ mExecutor.execute(() -> sendGetFeatureCompleted(success, features, featureState));
}
@Override
public void onChallengeGenerated(int sensorId, int userId, long challenge) {
- mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, userId, challenge)
- .sendToTarget();
+ mExecutor.execute(() -> sendChallengeGenerated(sensorId, userId, challenge));
}
@Override
public void onAuthenticationFrame(FaceAuthenticationFrame frame) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_FRAME, frame).sendToTarget();
+ mExecutor.execute(() -> sendAuthenticationFrame(frame));
}
@Override
public void onEnrollmentFrame(FaceEnrollFrame frame) {
- mHandler.obtainMessage(MSG_ENROLLMENT_FRAME, frame).sendToTarget();
+ mExecutor.execute(() -> sendEnrollmentFrame(frame));
}
};
@@ -175,7 +181,7 @@
if (mService == null) {
Slog.v(TAG, "FaceAuthenticationManagerService was null");
}
- mHandler = new MyHandler(context);
+ mExecutor = context.getMainExecutor();
if (context.checkCallingOrSelfPermission(USE_BIOMETRIC_INTERNAL)
== PackageManager.PERMISSION_GRANTED) {
addAuthenticatorsRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
@@ -189,18 +195,16 @@
}
/**
- * Use the provided handler thread for events.
+ * Returns an {@link Executor} for the given {@link Handler} or the main {@link Executor} if
+ * {@code handler} is {@code null}.
*/
- private void useHandler(Handler handler) {
- if (handler != null) {
- mHandler = new MyHandler(handler.getLooper());
- } else if (mHandler.getLooper() != mContext.getMainLooper()) {
- mHandler = new MyHandler(mContext.getMainLooper());
- }
+ private @NonNull Executor createExecutorForHandlerIfNeeded(@Nullable Handler handler) {
+ return handler != null ? new HandlerExecutor(handler) : mContext.getMainExecutor();
}
/**
* @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FaceAuthenticateOptions)}.
+ * @hide
*/
@Deprecated
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -212,17 +216,22 @@
}
/**
- * Request authentication. This call operates the face recognition hardware and starts capturing images.
+ * Request authentication.
+ *
+ * <p>This call operates the face recognition hardware and starts capturing images.
* It terminates when
* {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
* {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
* which point the object is no longer valid. The operation can be canceled by using the
- * provided cancel object.
+ * provided {@code cancel} object.
*
- * @param crypto object associated with the call or null if none required
- * @param cancel an object that can be used to cancel authentication
+ * @param crypto the cryptographic operations to use for authentication or {@code null} if
+ * none required
+ * @param cancel an object that can be used to cancel authentication or {@code null} if not
+ * needed
* @param callback an object to receive authentication events
- * @param handler an optional handler to handle callback events
+ * @param handler an optional handler to handle callback events or {@code null} to obtain main
+ * {@link Executor} from {@link Context}
* @param options additional options to customize this request
* @throws IllegalArgumentException if the crypto operation is not supported or is not backed
* by
@@ -235,6 +244,14 @@
public void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel,
@NonNull AuthenticationCallback callback, @Nullable Handler handler,
@NonNull FaceAuthenticateOptions options) {
+ authenticate(crypto, cancel, callback, createExecutorForHandlerIfNeeded(handler),
+ options, false /* allowBackgroundAuthentication */);
+ }
+
+ @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION})
+ private void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel,
+ @NonNull AuthenticationCallback callback, @NonNull Executor executor,
+ @NonNull FaceAuthenticateOptions options, boolean allowBackgroundAuthentication) {
if (callback == null) {
throw new IllegalArgumentException("Must supply an authentication callback");
}
@@ -249,13 +266,15 @@
if (mService != null) {
try {
- useHandler(handler);
+ mExecutor = executor;
mAuthenticationCallback = callback;
mCryptoObject = crypto;
final long operationId = crypto != null ? crypto.getOpId() : 0;
Trace.beginSection("FaceManager#authenticate");
- final long authId = mService.authenticate(
- mToken, operationId, mServiceReceiver, options);
+ final long authId = allowBackgroundAuthentication
+ ? mService.authenticateInBackground(
+ mToken, operationId, mServiceReceiver, options)
+ : mService.authenticate(mToken, operationId, mServiceReceiver, options);
if (cancel != null) {
cancel.setOnCancelListener(new OnAuthenticationCancelListener(authId));
}
@@ -273,6 +292,67 @@
}
/**
+ * Request background face authentication.
+ *
+ * <p>This call operates the face recognition hardware and starts capturing images.
+ * It terminates when
+ * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+ * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationSucceeded(
+ * BiometricPrompt.AuthenticationResult)} is called, at which point the object is no longer
+ * valid. The operation can be canceled by using the provided cancel object.
+ *
+ * <p>See {@link BiometricPrompt#authenticate} for more details. Please use
+ * {@link BiometricPrompt} for face authentication unless the experience must be customized for
+ * unique system-level utilities, like the lock screen or ambient background usage.
+ *
+ * @param executor the specified {@link Executor} to handle callback events; if {@code null},
+ * the callback will be executed on the main {@link Executor}.
+ * @param crypto the cryptographic operations to use for authentication or {@code null} if
+ * none required.
+ * @param cancel an object that can be used to cancel authentication or {@code null} if not
+ * needed.
+ * @param callback an object to receive authentication events.
+ * @throws IllegalArgumentException if the crypto operation is not supported or is not backed
+ * by
+ * <a href="{@docRoot}training/articles/keystore.html">Android
+ * Keystore facility</a>.
+ * @hide
+ */
+ @RequiresPermission(USE_BACKGROUND_FACE_AUTHENTICATION)
+ @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ @SystemApi
+ public void authenticateInBackground(@Nullable Executor executor,
+ @Nullable BiometricPrompt.CryptoObject crypto, @Nullable CancellationSignal cancel,
+ @NonNull BiometricPrompt.AuthenticationCallback callback) {
+ authenticate(crypto, cancel, new AuthenticationCallback() {
+ @Override
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
+ callback.onAuthenticationError(errorCode, errString);
+ }
+
+ @Override
+ public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
+ callback.onAuthenticationHelp(helpCode, helpString);
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(AuthenticationResult result) {
+ callback.onAuthenticationSucceeded(
+ new BiometricPrompt.AuthenticationResult(
+ crypto,
+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC));
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ callback.onAuthenticationFailed();
+ }
+ }, executor == null ? mContext.getMainExecutor() : executor,
+ new FaceAuthenticateOptions.Builder().build(),
+ true /* allowBackgroundAuthentication */);
+ }
+
+ /**
* Uses the face hardware to detect for the presence of a face, without giving details about
* accept/reject/lockout.
* @hide
@@ -628,12 +708,14 @@
}
/**
- * Determine if there is a face enrolled.
+ * Determine if there are enrolled {@link Face} templates.
*
- * @return true if a face is enrolled, false otherwise
+ * @return {@code true} if there are enrolled {@link Face} templates, {@code false} otherwise
* @hide
*/
- @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+ @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION})
+ @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ @SystemApi
public boolean hasEnrolledTemplates() {
return hasEnrolledTemplates(UserHandle.myUserId());
}
@@ -798,7 +880,7 @@
PowerManager.PARTIAL_WAKE_LOCK,
"faceLockoutResetCallback");
wakeLock.acquire();
- mHandler.post(() -> {
+ mExecutor.execute(() -> {
try {
callback.onLockoutReset(sensorId);
} finally {
@@ -1268,70 +1350,6 @@
}
}
- 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;
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 0096877..e267e6b 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -45,7 +45,7 @@
byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer);
// Retrieve static sensor properties for all face sensors
- @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+ @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"})
List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(String opPackageName);
// Retrieve static sensor properties for the specified sensor
@@ -57,6 +57,11 @@
long authenticate(IBinder token, long operationId, IFaceServiceReceiver receiver,
in FaceAuthenticateOptions options);
+ // Authenticate with a face. A requestId is returned that can be used to cancel this operation.
+ @EnforcePermission("USE_BACKGROUND_FACE_AUTHENTICATION")
+ long authenticateInBackground(IBinder token, long operationId, IFaceServiceReceiver receiver,
+ in FaceAuthenticateOptions options);
+
// Uses the face hardware to detect for the presence of a face, without giving details
// about accept/reject/lockout. A requestId is returned that can be used to cancel this
// operation.
@@ -131,7 +136,7 @@
void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName, long challenge);
// Determine if a user has at least one enrolled face
- @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+ @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"})
boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName);
// Return the LockoutTracker status for the specified user
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4a82675..35c0a32 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6601,6 +6601,13 @@
<permission android:name="android.permission.USE_BIOMETRIC_INTERNAL"
android:protectionLevel="signature" />
+ <!-- Allows privileged apps to access the background face authentication.
+ @SystemApi
+ @FlaggedApi("android.hardware.biometrics.face_background_authentication")
+ @hide -->
+ <permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION"
+ android:protectionLevel="signature|privileged" />
+
<!-- Allows the system to control the BiometricDialog (SystemUI). Reserved for the system. @hide -->
<permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG"
android:protectionLevel="signature" />
diff --git a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
index b843ad7..d816d085 100644
--- a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
+++ b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
@@ -18,6 +18,7 @@
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS;
+import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
import static com.google.common.truth.Truth.assertThat;
@@ -35,12 +36,15 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
+import android.hardware.biometrics.BiometricPrompt;
import android.os.CancellationSignal;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import com.android.internal.R;
@@ -58,6 +62,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.Executor;
@Presubmit
@RunWith(MockitoJUnitRunner.class)
@@ -78,6 +83,8 @@
@Mock
private FaceManager.AuthenticationCallback mAuthCallback;
@Mock
+ private BiometricPrompt.AuthenticationCallback mBioAuthCallback;
+ @Mock
private FaceManager.EnrollmentCallback mEnrollmentCallback;
@Mock
private FaceManager.FaceDetectionCallback mFaceDetectionCallback;
@@ -91,13 +98,16 @@
private TestLooper mLooper;
private Handler mHandler;
private FaceManager mFaceManager;
+ private Executor mExecutor;
@Before
public void setUp() throws Exception {
mLooper = new TestLooper();
mHandler = new Handler(mLooper.getLooper());
+ mExecutor = new HandlerExecutor(mHandler);
when(mContext.getMainLooper()).thenReturn(mLooper.getLooper());
+ when(mContext.getMainExecutor()).thenReturn(mExecutor);
when(mContext.getOpPackageName()).thenReturn(PACKAGE_NAME);
when(mContext.getAttributionTag()).thenReturn(ATTRIBUTION_TAG);
when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
@@ -159,6 +169,19 @@
}
@Test
+ @RequiresFlagsEnabled(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ public void authenticateInBackground_errorWhenUnavailable() throws Exception {
+ when(mService.authenticateInBackground(any(), anyLong(), any(), any()))
+ .thenThrow(new RemoteException());
+
+ mFaceManager.authenticateInBackground(mExecutor, null, new CancellationSignal(),
+ mBioAuthCallback);
+ mLooper.dispatchAll();
+
+ verify(mBioAuthCallback).onAuthenticationError(eq(FACE_ERROR_HW_UNAVAILABLE), any());
+ }
+
+ @Test
public void enrollment_errorWhenFaceEnrollmentExists() throws RemoteException {
when(mResources.getInteger(R.integer.config_faceMaxTemplatesPerUser)).thenReturn(1);
when(mService.getEnrolledFaces(anyInt(), anyInt(), anyString()))
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 4be75f8..eac2147 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -427,6 +427,7 @@
<permission name="android.permission.USE_BIOMETRIC" />
<permission name="android.permission.TEST_BIOMETRIC" />
<permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" />
+ <permission name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
<!-- Permissions required for CTS test - CtsContactsProviderTestCases -->
<permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" />
<!-- Permissions required for CTS test - CtsHdmiCecHostTestCases -->
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 3dfc454..61e0ca8 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -562,6 +562,9 @@
<!-- Permission required for CTS test - android.server.biometrics -->
<uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" />
+ <!-- Permission required for CTS test - android.server.biometrics -->
+ <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
+
<!-- Permissions required for CTS test - NotificationManagerTest -->
<uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" />
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 0f964bb..73f3999 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -143,7 +143,11 @@
return proto.getBytes();
}
- @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+ @android.annotation.EnforcePermission(
+ anyOf = {
+ android.Manifest.permission.USE_BIOMETRIC_INTERNAL,
+ android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION
+ })
@Override // Binder call
public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(
String opPackageName) {
@@ -285,6 +289,29 @@
restricted, statsClient, isKeyguard);
}
+ @android.annotation.EnforcePermission(
+ android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION)
+ @Override // Binder call
+ public long authenticateInBackground(final IBinder token, final long operationId,
+ final IFaceServiceReceiver receiver, final FaceAuthenticateOptions options) {
+ // TODO(b/152413782): If the sensor supports face detect and the device is encrypted or
+ // lockdown, something wrong happened. See similar path in FingerprintService.
+
+ super.authenticateInBackground_enforcePermission();
+
+ final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+ if (provider == null) {
+ Slog.w(TAG, "Null provider for authenticate");
+ return -1;
+ }
+ options.setSensorId(provider.first);
+
+ return provider.second.scheduleAuthenticate(token, operationId,
+ 0 /* cookie */, new ClientMonitorCallbackConverter(receiver), options,
+ false /* restricted */, BiometricsProtoEnums.CLIENT_UNKNOWN /* statsClient */,
+ true /* allowBackgroundAuthentication */);
+ }
+
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
@Override // Binder call
public long detectFace(final IBinder token,
@@ -548,7 +575,11 @@
return provider.getEnrolledFaces(sensorId, userId);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+ @android.annotation.EnforcePermission(
+ anyOf = {
+ android.Manifest.permission.USE_BIOMETRIC_INTERNAL,
+ android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION
+ })
@Override // Binder call
public boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName) {
super.hasEnrolledFaces_enforcePermission();
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 0089d4c..5e5181b 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -109,6 +109,7 @@
<uses-permission android:name="android.permission.UPDATE_LOCK_TASK_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_CONTEXT_HUB" />
<uses-permission android:name="android.permission.USE_BIOMETRIC_INTERNAL" />
+ <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
<uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
index 3aaac2e..c8a5583d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics.sensors.face;
+import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import static android.hardware.face.FaceSensorProperties.TYPE_UNKNOWN;
@@ -234,6 +235,26 @@
}
@Test
+ public void testAuthenticateInBackground() throws Exception {
+ FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
+ .build();
+ initService();
+ mFaceService.mServiceWrapper.registerAuthenticators(List.of());
+ waitForRegistration();
+
+ mContext.getTestablePermissions().setPermission(
+ USE_BIOMETRIC_INTERNAL, PackageManager.PERMISSION_DENIED);
+ mContext.getTestablePermissions().setPermission(
+ USE_BACKGROUND_FACE_AUTHENTICATION, PackageManager.PERMISSION_GRANTED);
+
+ final long operationId = 5;
+ mFaceService.mServiceWrapper.authenticateInBackground(mToken, operationId,
+ mFaceServiceReceiver, faceAuthenticateOptions);
+
+ assertThat(faceAuthenticateOptions.getSensorId()).isEqualTo(ID_DEFAULT);
+ }
+
+ @Test
public void testOptionsForDetect() throws Exception {
FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
.setOpPackageName(ComponentName.unflattenFromString(OP_PACKAGE_NAME)