From 049de84f2deeb77bf89e3b5182e128422aca4aa8 Mon Sep 17 00:00:00 2001 From: Diya Bera Date: Fri, 28 Jun 2024 16:55:57 +0000 Subject: [PATCH] Add mandatory biometric prompt to platform surfaces (1/N) 1. Face settings 2. Fingerprint settings 3. Change device credential Flag: android.hardware.biometrics.Flags.MANDATORY_BIOMETRICS Bug: 339910718 Test: atest UtilsTest Change-Id: I69778d1733ea9fb312e7c26ae0fa23b6008dde5d --- res/values/strings.xml | 3 +- src/com/android/settings/Utils.java | 54 +++++++++++++ .../biometrics/BiometricEnrollBase.java | 3 + .../combination/BiometricsSettingsBase.java | 41 ++++++++++ .../biometrics/face/FaceSettings.java | 23 ++++++ .../fingerprint/FingerprintSettings.java | 34 ++++++++ .../settings/password/BiometricFragment.java | 14 +++- .../settings/password/ChooseLockGeneric.java | 13 +++ .../ConfirmDeviceCredentialActivity.java | 13 ++- .../src/com/android/settings/UtilsTest.java | 80 +++++++++++++++++++ 10 files changed, 275 insertions(+), 3 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 075056db64e..d152c7c1f82 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -932,7 +932,8 @@ Face, fingerprint, and %s added Face, fingerprints, and %s added - + + This is needed since Identity Check is on Remote Authenticator Unlock diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 7ed6ba0969e..27a628c5308 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -24,6 +24,9 @@ import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH; import static android.text.format.DateUtils.FORMAT_SHOW_DATE; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_AUTHENTICATORS; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT; + import android.app.ActionBar; import android.app.Activity; import android.app.ActivityManager; @@ -54,6 +57,7 @@ import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.SensorProperties; import android.hardware.face.Face; import android.hardware.face.FaceManager; @@ -122,6 +126,7 @@ import com.android.settings.dashboard.profileselector.ProfileFragmentBridge; import com.android.settings.dashboard.profileselector.ProfileSelectFragment; import com.android.settings.dashboard.profileselector.ProfileSelectFragment.ProfileType; import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settingslib.widget.ActionBarShadowController; import com.android.settingslib.widget.AdaptiveIcon; @@ -1478,6 +1483,55 @@ public final class Utils extends com.android.settingslib.Utils { disableComponent(pm, new ComponentName(context, Settings.CreateShortcutActivity.class)); } + /** + * Request biometric authentication if all requirements for mandatory biometrics is satisfied. + * @param context of the corresponding activity/fragment + * @param biometricsSuccessfullyAuthenticated if the user has already authenticated using + * biometrics + * @param biometricsAuthenticationRequested if the activity/fragment has already requested for + * biometric prompt + * @return true if all requirements for mandatory biometrics is satisfied + */ + public static boolean requestBiometricAuthenticationForMandatoryBiometrics( + @NonNull Context context, + boolean biometricsSuccessfullyAuthenticated, + boolean biometricsAuthenticationRequested) { + final BiometricManager biometricManager = context.getSystemService(BiometricManager.class); + if (biometricManager == null) { + Log.e(TAG, "Biometric Manager is null."); + return false; + } + final int status = biometricManager.canAuthenticate( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS); + return android.hardware.biometrics.Flags.mandatoryBiometrics() + && status == BiometricManager.BIOMETRIC_SUCCESS + && !biometricsSuccessfullyAuthenticated + && !biometricsAuthenticationRequested; + } + + /** + * Launch biometric prompt for mandatory biometrics. Call + * {@link #requestBiometricAuthenticationForMandatoryBiometrics(Context, boolean, boolean)} + * to check if all requirements for mandatory biometrics is satisfied + * before launching biometric prompt. + * + * @param fragment corresponding fragment of the surface + * @param requestCode for starting the new activity + */ + public static void launchBiometricPromptForMandatoryBiometrics(@NonNull Fragment fragment, + int requestCode) { + final Intent intent = new Intent(); + intent.putExtra(BIOMETRIC_PROMPT_AUTHENTICATORS, + BiometricManager.Authenticators.MANDATORY_BIOMETRICS); + intent.putExtra(BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT, + fragment.getString(R.string.cancel)); + intent.putExtra(KeyguardManager.EXTRA_DESCRIPTION, + fragment.getString(R.string.mandatory_biometrics_prompt_description)); + intent.setClassName(SETTINGS_PACKAGE_NAME, + ConfirmDeviceCredentialActivity.class.getName()); + fragment.startActivityForResult(intent, requestCode); + } + private static void disableComponent(PackageManager pm, ComponentName componentName) { pm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java index 335d0b9dd99..37ada236343 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollBase.java +++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java @@ -68,6 +68,8 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity { public static final String EXTRA_FINISHED_ENROLL_FACE = "finished_enrolling_face"; public static final String EXTRA_FINISHED_ENROLL_FINGERPRINT = "finished_enrolling_fingerprint"; public static final String EXTRA_LAUNCHED_POSTURE_GUIDANCE = "launched_posture_guidance"; + public static final String EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY = + "biometrics_authenticated_successfully"; /** * Used by the choose fingerprint wizard to indicate the wizard is @@ -115,6 +117,7 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity { public static final int LEARN_MORE_REQUEST = 3; public static final int CONFIRM_REQUEST = 4; public static final int ENROLL_REQUEST = 5; + public static final int BIOMETRIC_AUTH_REQUEST = 6; /** * Request code when starting another biometric enrollment from within a biometric flow. For diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java index b17478881fe..caa7327394a 100644 --- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java +++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java @@ -65,6 +65,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { static final int CONFIRM_REQUEST = 2001; private static final int CHOOSE_LOCK_REQUEST = 2002; protected static final int ACTIVE_UNLOCK_REQUEST = 2003; + private static final int BIOMETRIC_AUTH_REQUEST = 2004; private static final String SAVE_STATE_CONFIRM_CREDETIAL = "confirm_credential"; private static final String DO_NOT_FINISH_ACTIVITY = "do_not_finish_activity"; @@ -72,10 +73,15 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { static final String RETRY_PREFERENCE_KEY = "retry_preference_key"; @VisibleForTesting static final String RETRY_PREFERENCE_BUNDLE = "retry_preference_bundle"; + private static final String BIOMETRICS_AUTH_REQUESTED = "biometrics_auth_requested"; + private static final String BIOMETRICS_AUTHENTICATED_SUCCESSFULLY = + "biometrics_authenticated_successfully"; protected int mUserId; protected long mGkPwHandle; private boolean mConfirmCredential; + private boolean mBiometricsAuthenticationRequested; + private boolean mBiometricsSuccessfullyAuthenticated; @Nullable private FaceManager mFaceManager; @Nullable private FingerprintManager mFingerprintManager; // Do not finish() if choosing/confirming credential, showing fp/face settings, or launching @@ -113,6 +119,9 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(getIntent()); } + mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( + BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); + if (savedInstanceState != null) { mConfirmCredential = savedInstanceState.getBoolean(SAVE_STATE_CONFIRM_CREDETIAL); mDoNotFinishActivity = savedInstanceState.getBoolean(DO_NOT_FINISH_ACTIVITY); @@ -123,11 +132,20 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { mGkPwHandle = savedInstanceState.getLong( ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE); } + mBiometricsAuthenticationRequested = savedInstanceState.getBoolean( + BIOMETRICS_AUTH_REQUESTED); + mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( + BIOMETRICS_AUTHENTICATED_SUCCESSFULLY); } if (mGkPwHandle == 0L && !mConfirmCredential) { mConfirmCredential = true; launchChooseOrConfirmLock(); + } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics( + getActivity(), mBiometricsSuccessfullyAuthenticated, + mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); } updateUnlockPhonePreferenceSummary(); @@ -141,6 +159,12 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { @Override public void onResume() { super.onResume(); + if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested) + && mGkPwHandle != 0L) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); + } if (!mConfirmCredential) { mDoNotFinishActivity = false; } @@ -177,6 +201,9 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putInt(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, sensorId); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); + extras.putBoolean( + BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, + mBiometricsSuccessfullyAuthenticated); onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { @@ -206,6 +233,9 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { final Bundle extras = preference.getExtras(); extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); + extras.putBoolean( + BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, + mBiometricsSuccessfullyAuthenticated); onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { @@ -288,6 +318,10 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { outState.putString(RETRY_PREFERENCE_KEY, mRetryPreferenceKey); outState.putBundle(RETRY_PREFERENCE_BUNDLE, mRetryPreferenceExtra); } + outState.putBoolean(BIOMETRICS_AUTH_REQUESTED, + mBiometricsAuthenticationRequested); + outState.putBoolean(BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, + mBiometricsSuccessfullyAuthenticated); } @Override @@ -315,6 +349,13 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { } mRetryPreferenceKey = null; mRetryPreferenceExtra = null; + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + mBiometricsAuthenticationRequested = false; + if (resultCode == RESULT_OK) { + mBiometricsSuccessfullyAuthenticated = true; + } else { + finish(); + } } } diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java index 8884ce36253..2a0dd83a491 100644 --- a/src/com/android/settings/biometrics/face/FaceSettings.java +++ b/src/com/android/settings/biometrics/face/FaceSettings.java @@ -20,8 +20,10 @@ import static android.app.Activity.RESULT_OK; import static android.app.admin.DevicePolicyResources.Strings.Settings.FACE_SETTINGS_FOR_WORK_TITLE; import static com.android.settings.Utils.isPrivateProfile; +import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.ENROLL_REQUEST; +import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT; @@ -66,6 +68,8 @@ public class FaceSettings extends DashboardFragment { private static final String TAG = "FaceSettings"; private static final String KEY_TOKEN = "hw_auth_token"; private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock"; + private static final String KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED = + "biometrics_successfully_authenticated"; private static final String PREF_KEY_DELETE_FACE_DATA = "security_settings_face_delete_faces_container"; @@ -93,6 +97,8 @@ public class FaceSettings extends DashboardFragment { private FaceFeatureProvider mFaceFeatureProvider; private boolean mConfirmingPassword; + private boolean mBiometricsAuthenticationRequested; + private boolean mBiometricsSuccessfullyAuthenticated; private final FaceSettingsRemoveButtonPreferenceController.Listener mRemovalListener = () -> { @@ -144,6 +150,8 @@ public class FaceSettings extends DashboardFragment { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putByteArray(KEY_TOKEN, mToken); + outState.putBoolean(KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED, + mBiometricsSuccessfullyAuthenticated); } @Override @@ -163,6 +171,8 @@ public class FaceSettings extends DashboardFragment { mToken = getIntent().getByteArrayExtra(KEY_TOKEN); mSensorId = getIntent().getIntExtra(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, -1); mChallenge = getIntent().getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, 0L); + mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( + EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); mUserId = getActivity().getIntent().getIntExtra( Intent.EXTRA_USER_ID, UserHandle.myUserId()); @@ -231,6 +241,8 @@ public class FaceSettings extends DashboardFragment { if (savedInstanceState != null) { mToken = savedInstanceState.getByteArray(KEY_TOKEN); + mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( + KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED); } } @@ -276,6 +288,10 @@ public class FaceSettings extends DashboardFragment { Log.e(TAG, "Password not set"); finish(); } + } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); } else { mAttentionController.setToken(mToken); mEnrollController.setToken(mToken); @@ -318,6 +334,13 @@ public class FaceSettings extends DashboardFragment { setResult(resultCode, data); finish(); } + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + mBiometricsAuthenticationRequested = false; + if (resultCode == RESULT_OK) { + mBiometricsSuccessfullyAuthenticated = true; + } else { + finish(); + } } } diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index db87c0facac..e30a3b637f1 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.UNDEFINED; import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; import static com.android.settings.Utils.isPrivateProfile; +import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY; import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE; @@ -218,6 +219,10 @@ public class FingerprintSettings extends SubSettings { "security_settings_fingerprint_unlock_category"; private static final String KEY_FINGERPRINT_UNLOCK_FOOTER = "security_settings_fingerprint_footer"; + private static final String KEY_BIOMETRICS_AUTHENTICATION_REQUESTED = + "biometrics_authentication_requested"; + private static final String KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED = + "biometrics_successfully_authenticated"; private static final int MSG_REFRESH_FINGERPRINT_TEMPLATES = 1000; private static final int MSG_FINGER_AUTH_SUCCESS = 1001; @@ -251,6 +256,8 @@ public class FingerprintSettings extends SubSettings { private boolean mInFingerprintLockout; private byte[] mToken; private boolean mLaunchedConfirm; + private boolean mBiometricsAuthenticationRequested; + private boolean mBiometricsSuccessfullyAuthenticated; private boolean mHasFirstEnrolled = true; private Drawable mHighlightDrawable; private int mUserId; @@ -418,6 +425,8 @@ public class FingerprintSettings extends SubSettings { ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); mChallenge = activity.getIntent() .getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L); + mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( + BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); mAuthenticateSidecar = (FingerprintAuthenticateSidecar) getFragmentManager().findFragmentByTag(TAG_AUTHENTICATE_SIDECAR); @@ -459,6 +468,10 @@ public class FingerprintSettings extends SubSettings { mIsEnrolling = savedInstanceState.getBoolean(KEY_IS_ENROLLING, mIsEnrolling); mHasFirstEnrolled = savedInstanceState.getBoolean(KEY_HAS_FIRST_ENROLLED, mHasFirstEnrolled); + mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( + KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED); + mBiometricsAuthenticationRequested = savedInstanceState.getBoolean( + KEY_BIOMETRICS_AUTHENTICATION_REQUESTED); } // (mLaunchedConfirm or mIsEnrolling) means that we are waiting an activity result. @@ -467,6 +480,10 @@ public class FingerprintSettings extends SubSettings { if (mToken == null) { mLaunchedConfirm = true; launchChooseOrConfirmLock(); + } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); } else if (!mHasFirstEnrolled) { mIsEnrolling = true; addFirstFingerprint(null); @@ -746,6 +763,12 @@ public class FingerprintSettings extends SubSettings { mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider() .getUdfpsEnrollCalibrator(getActivity().getApplicationContext(), null, null); + + if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); + } } private void updatePreferences() { @@ -793,6 +816,10 @@ public class FingerprintSettings extends SubSettings { outState.putSerializable("mFingerprintsRenaming", mFingerprintsRenaming); outState.putBoolean(KEY_IS_ENROLLING, mIsEnrolling); outState.putBoolean(KEY_HAS_FIRST_ENROLLED, mHasFirstEnrolled); + outState.putBoolean(KEY_BIOMETRICS_AUTHENTICATION_REQUESTED, + mBiometricsAuthenticationRequested); + outState.putBoolean(KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED, + mBiometricsSuccessfullyAuthenticated); } @Override @@ -1013,6 +1040,13 @@ public class FingerprintSettings extends SubSettings { mIsEnrolling = false; mHasFirstEnrolled = true; updateAddPreference(); + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + mBiometricsAuthenticationRequested = false; + if (resultCode == RESULT_OK) { + mBiometricsSuccessfullyAuthenticated = true; + } else { + finish(); + } } } diff --git a/src/com/android/settings/password/BiometricFragment.java b/src/com/android/settings/password/BiometricFragment.java index 02f5b861ea3..a7a039e8485 100644 --- a/src/com/android/settings/password/BiometricFragment.java +++ b/src/com/android/settings/password/BiometricFragment.java @@ -16,8 +16,11 @@ package com.android.settings.password; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED; + import android.app.settings.SettingsEnums; import android.content.ComponentName; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; import android.hardware.biometrics.BiometricPrompt.AuthenticationResult; @@ -137,7 +140,7 @@ public class BiometricFragment extends InstrumentedFragment { BiometricPrompt.Builder promptBuilder = new BiometricPrompt.Builder(getContext()) .setTitle(promptInfo.getTitle()) .setUseDefaultTitle() // use default title if title is null/empty - .setDeviceCredentialAllowed(true) + .setAllowedAuthenticators(promptInfo.getAuthenticators()) .setSubtitle(promptInfo.getSubtitle()) .setDescription(promptInfo.getDescription()) .setTextForDeviceCredential( @@ -170,6 +173,15 @@ public class BiometricFragment extends InstrumentedFragment { if (promptInfo.isUseDefaultSubtitle()) { promptBuilder.setUseDefaultSubtitle(); } + + if ((promptInfo.getAuthenticators() + & BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { + promptBuilder.setNegativeButton(promptInfo.getNegativeButtonText(), + getContext().getMainExecutor(), + (dialog, which) -> mAuthenticationCallback.onAuthenticationError( + BIOMETRIC_ERROR_USER_CANCELED, + null /* errString */)); + } mBiometricPrompt = promptBuilder.build(); } diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java index ce9a5667dfc..4c18309384c 100644 --- a/src/com/android/settings/password/ChooseLockGeneric.java +++ b/src/com/android/settings/password/ChooseLockGeneric.java @@ -160,11 +160,13 @@ public class ChooseLockGeneric extends SettingsActivity { static final int CHOOSE_LOCK_BEFORE_BIOMETRIC_REQUEST = 103; @VisibleForTesting static final int SKIP_FINGERPRINT_REQUEST = 104; + private static final int BIOMETRIC_AUTH_REQUEST = 105; private LockPatternUtils mLockPatternUtils; private DevicePolicyManager mDpm; private boolean mRequestGatekeeperPasswordHandle = false; private boolean mPasswordConfirmed = false; + private boolean mBiometricsAuthSuccessful = false; private boolean mWaitingForConfirmation = false; private boolean mWaitingForActivityResult = false; private LockscreenCredential mUserPassword; @@ -488,6 +490,17 @@ public class ChooseLockGeneric extends SettingsActivity { ? data.getParcelableExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD) : null; updatePreferencesOrFinish(false /* isRecreatingActivity */); + if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getContext(), + mBiometricsAuthSuccessful, mWaitingForConfirmation)) { + mWaitingForConfirmation = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); + } + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + mBiometricsAuthSuccessful = true; + } else { + finish(); + } } else if (requestCode == CHOOSE_LOCK_REQUEST) { if (resultCode != RESULT_CANCELED) { getActivity().setResult(resultCode, data); diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java index 7f362c32904..c0b3093c2f8 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java @@ -17,10 +17,10 @@ package com.android.settings.password; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PASSWORD_HEADER; import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PATTERN_HEADER; import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PIN_HEADER; -import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; import static com.android.systemui.biometrics.Utils.toBitmap; @@ -40,6 +40,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; import android.hardware.biometrics.PromptInfo; @@ -76,6 +77,9 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { /** Use this extra value to provide a custom logo description for the biometric prompt. **/ public static final String CUSTOM_BIOMETRIC_PROMPT_LOGO_DESCRIPTION_KEY = "custom_logo_description"; + public static final String BIOMETRIC_PROMPT_AUTHENTICATORS = "biometric_prompt_authenticators"; + public static final String BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT = + "biometric_prompt_negative_button_text"; public static class InternalActivity extends ConfirmDeviceCredentialActivity { } @@ -177,6 +181,11 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { mDetails = intent.getCharSequenceExtra(KeyguardManager.EXTRA_DESCRIPTION); String alternateButton = intent.getStringExtra( KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); + final int authenticators = intent.getIntExtra(BIOMETRIC_PROMPT_AUTHENTICATORS, + BiometricManager.Authenticators.DEVICE_CREDENTIAL + | BiometricManager.Authenticators.BIOMETRIC_WEAK); + final String negativeButtonText = intent.getStringExtra( + BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT); final boolean frp = KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction()); final boolean repairMode = @@ -213,6 +222,8 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { promptInfo.setTitle(mTitle); promptInfo.setDescription(mDetails); promptInfo.setDisallowBiometricsIfPolicyExists(mCheckDevicePolicyManager); + promptInfo.setAuthenticators(authenticators); + promptInfo.setNegativeButtonText(negativeButtonText); if (android.multiuser.Flags.enablePrivateSpaceFeatures() && android.multiuser.Flags.usePrivateSpaceIconInBiometricPrompt() diff --git a/tests/robotests/src/com/android/settings/UtilsTest.java b/tests/robotests/src/com/android/settings/UtilsTest.java index 0c57b014506..77de7496046 100644 --- a/tests/robotests/src/com/android/settings/UtilsTest.java +++ b/tests/robotests/src/com/android/settings/UtilsTest.java @@ -20,6 +20,10 @@ import static android.hardware.biometrics.SensorProperties.STRENGTH_CONVENIENCE; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_AUTHENTICATORS; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; @@ -35,10 +39,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActionBar; +import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyResourcesManager; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.UserInfo; @@ -47,6 +53,8 @@ import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.VectorDrawable; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.Flags; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; @@ -61,21 +69,28 @@ import android.os.UserManager; import android.os.storage.DiskInfo; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.IconDrawableFactory; import android.widget.EditText; import android.widget.ScrollView; import android.widget.TextView; import androidx.core.graphics.drawable.IconCompat; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settings.testutils.shadow.ShadowLockPatternUtils; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; @@ -92,6 +107,9 @@ import java.util.List; @Config(shadows = ShadowLockPatternUtils.class) public class UtilsTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String PACKAGE_NAME = "com.android.app"; private static final int USER_ID = 1; @@ -113,6 +131,11 @@ public class UtilsTest { private IconDrawableFactory mIconDrawableFactory; @Mock private ApplicationInfo mApplicationInfo; + @Mock + private BiometricManager mBiometricManager; + @Mock + private Fragment mFragment; + private Context mContext; private UserManager mUserManager; private static final int FLAG_SYSTEM = 0x00000000; @@ -128,6 +151,7 @@ public class UtilsTest { when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) .thenReturn(connectivityManager); when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(BiometricManager.class)).thenReturn(mBiometricManager); } @After @@ -503,6 +527,62 @@ public class UtilsTest { assertThat(Utils.isFaceNotConvenienceBiometric(mContext)).isFalse(); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testRequestBiometricAuthentication_biometricManagerNull_shouldReturnFalse() { + when(mContext.getSystemService(Context.BIOMETRIC_SERVICE)).thenReturn(null); + assertThat(Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, + false /* biometricsSuccessfullyAuthenticated */, + false /* biometricsAuthenticationRequested */)).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testRequestBiometricAuthentication_biometricManagerReturnsSuccess_shouldReturnTrue() + throws InterruptedException { + when(mBiometricManager.canAuthenticate( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + boolean requestBiometricAuthenticationForMandatoryBiometrics = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, + true /* biometricsSuccessfullyAuthenticated */, + false /* biometricsAuthenticationRequested */); + assertThat(requestBiometricAuthenticationForMandatoryBiometrics).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testRequestBiometricAuthentication_biometricManagerReturnsError_shouldReturnFalse() { + when(mBiometricManager.canAuthenticate( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); + assertThat(Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, + false /* biometricsSuccessfullyAuthenticated */, + false /* biometricsAuthenticationRequested */)).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testLaunchBiometricPrompt_checkIntentValues() { + when(mFragment.getContext()).thenReturn(mContext); + + final int requestCode = 1; + final ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); + Utils.launchBiometricPromptForMandatoryBiometrics(mFragment, requestCode); + + verify(mFragment).startActivityForResult(intentArgumentCaptor.capture(), eq(requestCode)); + + final Intent intent = intentArgumentCaptor.getValue(); + + assertThat(intent.getExtra(BIOMETRIC_PROMPT_AUTHENTICATORS)).isEqualTo( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS); + assertThat(intent.getExtra(BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT)).isNotNull(); + assertThat(intent.getExtra(KeyguardManager.EXTRA_DESCRIPTION)).isNotNull(); + assertThat(intent.getComponent().getPackageName()).isEqualTo(SETTINGS_PACKAGE_NAME); + assertThat(intent.getComponent().getClassName()).isEqualTo( + ConfirmDeviceCredentialActivity.class.getName()); + } + private void setUpForConfirmCredentialString(boolean isEffectiveUserManagedProfile) { when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager); when(mMockUserManager.getCredentialOwnerProfile(USER_ID)).thenReturn(USER_ID);