diff options
author | 2025-03-11 00:52:10 +0000 | |
---|---|---|
committer | 2025-03-21 01:37:03 +0000 | |
commit | 02adb885fe493c461c35ae085f741e6f7cbb1d95 (patch) | |
tree | 52c8fc8c1d68283014192af85ac0eccc23e8a2fd | |
parent | 81ab9862188b7bd01347086e2c2be43d1a86562f (diff) |
Add some new modality-specific APIs in BiometricManager.
- getEnrolledFingerprintCount()
- getEnrolledFingerprintCount()
Bug: 399438509
Test: atest AuthServiceTest
Test: atest BiometricSimpleTests
Flag: android.hardware.biometrics.move_fm_api_to_bm
Change-Id: I8bdc8fdf965e65d1ecd3ed34fe469d4f7c7f3201
7 files changed, 299 insertions, 2 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 35720fd17769..41097630918b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4925,6 +4925,20 @@ package android.hardware { package android.hardware.biometrics { + @FlaggedApi("android.hardware.biometrics.move_fm_api_to_bm") public final class BiometricEnrollmentStatus implements android.os.Parcelable { + method public int describeContents(); + method @IntRange(from=0) public int getEnrollCount(); + method public int getModality(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.BiometricEnrollmentStatus> CREATOR; + } + + public class BiometricManager { + method @FlaggedApi("android.hardware.biometrics.move_fm_api_to_bm") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public java.util.Set<android.hardware.biometrics.BiometricEnrollmentStatus> getEnrollmentStatus(); + field @FlaggedApi("android.hardware.biometrics.move_fm_api_to_bm") @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public static final int TYPE_FACE = 8; // 0x8 + field @FlaggedApi("android.hardware.biometrics.move_fm_api_to_bm") @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public static final int TYPE_FINGERPRINT = 2; // 0x2 + } + public static interface BiometricManager.Authenticators { field @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static final int BIOMETRIC_CONVENIENCE = 4095; // 0xfff field @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static final int EMPTY_SET = 0; // 0x0 diff --git a/core/java/android/hardware/biometrics/BiometricEnrollmentStatus.aidl b/core/java/android/hardware/biometrics/BiometricEnrollmentStatus.aidl new file mode 100644 index 000000000000..2d28f3dddb83 --- /dev/null +++ b/core/java/android/hardware/biometrics/BiometricEnrollmentStatus.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2025 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.biometrics; + +// @hide +parcelable BiometricEnrollmentStatus; diff --git a/core/java/android/hardware/biometrics/BiometricEnrollmentStatus.java b/core/java/android/hardware/biometrics/BiometricEnrollmentStatus.java new file mode 100644 index 000000000000..f883b9f72d7c --- /dev/null +++ b/core/java/android/hardware/biometrics/BiometricEnrollmentStatus.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2025 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.biometrics; + +import android.annotation.FlaggedApi; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class contains enrollment information. It keeps track of the modality type (e.g. + * fingerprint, face) and the number of times the biometric has been enrolled. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_MOVE_FM_API_TO_BM) +public final class BiometricEnrollmentStatus implements Parcelable { + @BiometricManager.BiometricModality + private final int mModality; + private final int mEnrollCount; + + /** + * @hide + */ + public BiometricEnrollmentStatus( + @BiometricManager.BiometricModality int modality, int enrollCount) { + mModality = modality; + mEnrollCount = enrollCount; + } + + /** + * Returns the modality associated with this enrollment status. + * + * @return The int value representing the biometric sensor type, e.g. + * {@link BiometricManager#TYPE_FACE} or + * {@link BiometricManager#TYPE_FINGERPRINT}. + */ + @BiometricManager.BiometricModality + public int getModality() { + return mModality; + } + + /** + * Returns the number of enrolled biometric for the associated modality. + * + * @return The number of enrolled biometric. + */ + @IntRange(from = 0) + public int getEnrollCount() { + return mEnrollCount; + } + + private BiometricEnrollmentStatus(Parcel in) { + this(in.readInt(), in.readInt()); + } + + @NonNull + public static final Creator<BiometricEnrollmentStatus> CREATOR = new Creator<>() { + @Override + public BiometricEnrollmentStatus createFromParcel(Parcel in) { + return new BiometricEnrollmentStatus(in); + } + + @Override + public BiometricEnrollmentStatus[] newArray(int size) { + return new BiometricEnrollmentStatus[size]; + } + }; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mModality); + dest.writeInt(mEnrollCount); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + String modality = ""; + if (mModality == BiometricManager.TYPE_FINGERPRINT) { + modality = "Fingerprint"; + } else if (mModality == BiometricManager.TYPE_FACE) { + modality = "Face"; + } + return "Modality: " + modality + ", Enrolled Count: " + mEnrollCount; + } +} diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index cefe20c15ced..8ac24c81a9f0 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -16,6 +16,7 @@ package android.hardware.biometrics; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -45,7 +46,9 @@ import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * A class that contains biometric utilities. For authentication, see {@link BiometricPrompt}. @@ -143,6 +146,37 @@ public class BiometricManager { public @interface BiometricError {} /** + * Constant representing fingerprint. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_MOVE_FM_API_TO_BM) + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + public static final int TYPE_FINGERPRINT = BiometricAuthenticator.TYPE_FINGERPRINT; + + /** + * Constant representing face. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_MOVE_FM_API_TO_BM) + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + public static final int TYPE_FACE = BiometricAuthenticator.TYPE_FACE; + + /** + * An {@link IntDef} representing the biometric modalities. + * @hide + */ + @IntDef(flag = true, value = { + TYPE_FINGERPRINT, + TYPE_FACE + }) + @Retention(RetentionPolicy.SOURCE) + @interface BiometricModality { + } + + + /** * Types of authenticators, defined at a level of granularity supported by * {@link BiometricManager} and {@link BiometricPrompt}. * @@ -406,8 +440,6 @@ public class BiometricManager { /** * @hide - * @param context - * @param service */ public BiometricManager(@NonNull Context context, @NonNull IAuthService service) { mContext = context; @@ -567,6 +599,37 @@ public class BiometricManager { } /** + * Return the current biometrics enrollment status set. + * + * <p>Returning more than one status indicates that the device supports multiple biometric + * modalities (e.g., fingerprint, face, iris). Each {@link BiometricEnrollmentStatus} object + * within the returned collection provides detailed information about the enrollment state for a + * particular modality. + * + * <p>This method is intended for system apps, such as settings or device setup, which require + * detailed enrollment information to show or hide features or to encourage users to enroll + * in a specific modality. Applications should instead use + * {@link BiometricManager#canAuthenticate(int)} to check the enrollment status and use the + * enroll intent, when needed to allow users to enroll. That ensures that users are presented + * with a consistent set of options across all of their apps and can be redirected to a + * single system-managed settings surface.</p> + * + * @hide + */ + @SystemApi + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @FlaggedApi(Flags.FLAG_MOVE_FM_API_TO_BM) + @NonNull + public Set<BiometricEnrollmentStatus> getEnrollmentStatus() { + try { + return new HashSet<BiometricEnrollmentStatus>( + mService.getEnrollmentStatus(mContext.getOpPackageName())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @hide * @param userId * @return diff --git a/core/java/android/hardware/biometrics/IAuthService.aidl b/core/java/android/hardware/biometrics/IAuthService.aidl index 8514f98fbf0d..b6ce79af00fe 100644 --- a/core/java/android/hardware/biometrics/IAuthService.aidl +++ b/core/java/android/hardware/biometrics/IAuthService.aidl @@ -22,6 +22,7 @@ import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.IInvalidationCallback; import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; +import android.hardware.biometrics.BiometricEnrollmentStatus; import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorPropertiesInternal; @@ -64,6 +65,9 @@ interface IAuthService { // Checks if any biometrics are enrolled. boolean hasEnrolledBiometrics(int userId, String opPackageName); + // Return the current biometrics enrollment status. + List<BiometricEnrollmentStatus> getEnrollmentStatus(String opPackageName); + // Register callback for when keyguard biometric eligibility changes. void registerEnabledOnKeyguardCallback(IBiometricEnabledOnKeyguardCallback callback); diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index b6768c9c087a..eede4c9c59d0 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -36,6 +36,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricEnrollmentStatus; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IAuthService; @@ -417,6 +418,55 @@ public class AuthService extends SystemService { } @Override + public List<BiometricEnrollmentStatus> getEnrollmentStatus(String opPackageName) + throws RemoteException { + checkBiometricAdvancedPermission(); + final long identity = Binder.clearCallingIdentity(); + try { + final int userId = UserHandle.myUserId(); + final List<BiometricEnrollmentStatus> enrollmentStatusList = + new ArrayList<>(); + final IFingerprintService fingerprintService = mInjector.getFingerprintService(); + if (fingerprintService != null) { + final List<FingerprintSensorPropertiesInternal> fpProps = + fingerprintService.getSensorPropertiesInternal(opPackageName); + if (!fpProps.isEmpty()) { + int fpCount = fingerprintService.getEnrolledFingerprints(userId, + opPackageName, getContext().getAttributionTag()).size(); + enrollmentStatusList.add( + new BiometricEnrollmentStatus( + BiometricManager.TYPE_FINGERPRINT, fpCount)); + } else { + Slog.e(TAG, "No fingerprint sensors"); + } + } else { + Slog.e(TAG, "No fingerprint sensors"); + } + + final IFaceService faceService = mInjector.getFaceService(); + if (faceService != null) { + final List<FaceSensorPropertiesInternal> faceProps = + faceService.getSensorPropertiesInternal(opPackageName); + if (!faceProps.isEmpty()) { + int faceCount = faceService.getEnrolledFaces(faceProps.getFirst().sensorId, + userId, opPackageName).size(); + enrollmentStatusList.add( + new BiometricEnrollmentStatus( + BiometricManager.TYPE_FACE, faceCount)); + } else { + Slog.e(TAG, "No face sensors"); + } + } else { + Slog.e(TAG, "No face sensors"); + } + + return enrollmentStatusList; + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void registerEnabledOnKeyguardCallback( IBiometricEnabledOnKeyguardCallback callback) throws RemoteException { checkInternalPermission(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index c7efa318af99..cebdce9ed6cc 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -49,11 +49,14 @@ import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.PromptInfo; +import android.hardware.biometrics.SensorProperties; import android.hardware.biometrics.fingerprint.SensorProps; import android.hardware.face.FaceSensorConfigurations; +import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.IFaceService; import android.hardware.fingerprint.FingerprintSensorConfigurations; +import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintService; import android.hardware.iris.IIrisService; @@ -84,6 +87,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Stubber; +import java.util.ArrayList; import java.util.List; @Presubmit @@ -519,6 +523,41 @@ public class AuthServiceTest { verify(mBiometricService).getLastAuthenticationTime(eq(mUserId), eq(authenticators)); } + @Test + public void testGetEnrollmentStatus_callsFingerprintAndFaceService() throws Exception { + setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); + List<FaceSensorPropertiesInternal> faceProps = List.of(new FaceSensorPropertiesInternal( + 0 /* id */, + FaceSensorProperties.STRENGTH_STRONG, + 1 /* maxTemplatesAllowed */, + new ArrayList<>() /* componentInfo */, + FaceSensorProperties.TYPE_UNKNOWN, + true /* supportsFaceDetection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresChallenge */)); + List<FingerprintSensorPropertiesInternal> fpProps = List.of( + new FingerprintSensorPropertiesInternal(1 /* id */, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + new ArrayList<>() /* componentInfo */, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + false /* resetLockoutRequiresHardwareAuthToken */)); + when(mFaceService.getSensorPropertiesInternal(eq(TEST_OP_PACKAGE_NAME))).thenReturn( + faceProps); + when(mFingerprintService.getSensorPropertiesInternal(eq(TEST_OP_PACKAGE_NAME))).thenReturn( + fpProps); + when(mContext.getAttributionTag()).thenReturn("tag"); + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + mAuthService.mImpl.getEnrollmentStatus(TEST_OP_PACKAGE_NAME); + + waitForIdle(); + verify(mFaceService).getEnrolledFaces(eq(0), eq(mUserId), eq(TEST_OP_PACKAGE_NAME)); + verify(mFingerprintService).getEnrolledFingerprints(eq(mUserId), eq(TEST_OP_PACKAGE_NAME), + eq("tag")); + } + private static void setInternalAndTestBiometricPermissions( Context context, boolean hasPermission) { for (String p : List.of(TEST_BIOMETRIC, MANAGE_BIOMETRIC, USE_BIOMETRIC_INTERNAL)) { |