diff --git a/res/layout/biometric_lockout_error_dialog.xml b/res/layout/biometric_lockout_error_dialog.xml
new file mode 100644
index 00000000000..8d4275b6aaa
--- /dev/null
+++ b/res/layout/biometric_lockout_error_dialog.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f05fdd12b67..36f4aefac95 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -938,6 +938,28 @@
Face, fingerprints, and %s added
Identity Check is on and requires a biometric
+
+ Go to Settings
+
+ Identity Check is on and can’t verify it’s you
+
+ Biometrics failed too many times. Lock and unlock your device to retry.
+
+ You can manage Identity Check in theft protection settings. Go to Settings
+
+ Biometric required to continue
+
+ Identity Check is on and requires a biometric, but your face or fingerprint sensor is unavailable\n- Check that your camera is on and try again
\n- You can turn off Identity Check using your Google Account
+
+ Cancel
+
+ OK
+
+ Go to identity check
+
+ Lock screen
+
+
Remote Authenticator Unlock
diff --git a/src/com/android/settings/development/BiometricErrorDialog.java b/src/com/android/settings/development/BiometricErrorDialog.java
new file mode 100644
index 00000000000..517d019360f
--- /dev/null
+++ b/src/com/android/settings/development/BiometricErrorDialog.java
@@ -0,0 +1,209 @@
+/*
+ * 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 com.android.settings.development;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/** Initializes and shows biometric error dialogs related to identity check. */
+public class BiometricErrorDialog extends InstrumentedDialogFragment {
+ private static final String TAG = "BiometricErrorDialog";
+
+ private static final String KEY_ERROR_CODE = "key_error_code";
+ private String mActionIdentityCheckSettings = Settings.ACTION_SETTINGS;
+ @Nullable private BroadcastReceiver mBroadcastReceiver;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(
+ @Nullable Bundle savedInstanceState) {
+ final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
+ final LayoutInflater inflater = getActivity().getLayoutInflater();
+ final boolean isLockoutError = getArguments().getString(KEY_ERROR_CODE).equals(
+ Utils.BiometricStatus.LOCKOUT.name());
+ final View customView = inflater.inflate(R.layout.biometric_lockout_error_dialog,
+ null);
+ final String identityCheckSettingsAction = getActivity().getString(
+ R.string.identity_check_settings_action);
+ mActionIdentityCheckSettings = identityCheckSettingsAction.isEmpty()
+ ? mActionIdentityCheckSettings : identityCheckSettingsAction;
+ Log.d(TAG, mActionIdentityCheckSettings);
+ setTitle(customView, isLockoutError);
+ setBody(customView, isLockoutError);
+ alertDialogBuilder.setView(customView);
+ setPositiveButton(alertDialogBuilder, isLockoutError);
+ setNegativeButton(alertDialogBuilder, isLockoutError);
+
+ if (isLockoutError) {
+ mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+ dismiss();
+ }
+ }
+ };
+ getContext().registerReceiver(mBroadcastReceiver,
+ new IntentFilter(Intent.ACTION_SCREEN_OFF));
+ }
+
+ return alertDialogBuilder.create();
+ }
+
+ @Override
+ public void dismiss() {
+ super.dismiss();
+ if (mBroadcastReceiver != null) {
+ getContext().unregisterReceiver(mBroadcastReceiver);
+ mBroadcastReceiver = null;
+ }
+ }
+
+ /**
+ * Shows an error dialog to prompt the user to resolve biometric errors for identity check.
+ * @param fragmentActivity calling activity
+ * @param errorCode refers to the biometric error
+ */
+ public static BiometricErrorDialog showBiometricErrorDialog(FragmentActivity fragmentActivity,
+ Utils.BiometricStatus errorCode) {
+ final BiometricErrorDialog biometricErrorDialog = new BiometricErrorDialog();
+ final Bundle args = new Bundle();
+ args.putCharSequence(KEY_ERROR_CODE, errorCode.name());
+ biometricErrorDialog.setArguments(args);
+ biometricErrorDialog.show(fragmentActivity.getSupportFragmentManager(),
+ BiometricErrorDialog.class.getName());
+ return biometricErrorDialog;
+ }
+
+ private void setTitle(View view, boolean lockout) {
+ final TextView titleTextView = view.findViewById(R.id.title);
+ if (lockout) {
+ titleTextView.setText(R.string.identity_check_lockout_error_title);
+ } else {
+ titleTextView.setText(R.string.identity_check_general_error_title);
+ }
+ }
+
+ private void setBody(View view, boolean lockout) {
+ final TextView textView1 = view.findViewById(R.id.description_1);
+ final TextView textView2 = view.findViewById(R.id.description_2);
+
+ if (lockout) {
+ textView1.setText(R.string.identity_check_lockout_error_description_1);
+ textView2.setText(getClickableDescriptionForLockoutError());
+ textView2.setMovementMethod(LinkMovementMethod.getInstance());
+ } else {
+ textView1.setText(R.string.identity_check_general_error_description_1);
+ textView2.setVisibility(View.GONE);
+ }
+ }
+
+ private SpannableString getClickableDescriptionForLockoutError() {
+ final String description = getResources().getString(
+ R.string.identity_check_lockout_error_description_2);
+ final SpannableString spannableString = new SpannableString(description);
+ final ClickableSpan clickableSpan = new ClickableSpan() {
+ @Override
+ public void onClick(View textView) {
+ dismiss();
+ final Intent autoLockSettingsIntent = new Intent(mActionIdentityCheckSettings);
+ final ResolveInfo autoLockSettingsInfo = getActivity().getPackageManager()
+ .resolveActivity(autoLockSettingsIntent, 0 /* flags */);
+ if (autoLockSettingsInfo != null) {
+ startActivity(autoLockSettingsIntent);
+ } else {
+ Log.e(TAG, "Auto lock settings intent could not be resolved.");
+ }
+ }
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ super.updateDrawState(ds);
+ ds.setUnderlineText(true);
+ }
+ };
+ final String goToSettings = getActivity().getString(R.string.go_to_settings);
+ spannableString.setSpan(clickableSpan, description.indexOf(goToSettings),
+ description.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ return spannableString;
+ }
+
+ private void setPositiveButton(AlertDialog.Builder alertDialogBuilder, boolean lockout) {
+ if (lockout) {
+ DevicePolicyManager devicePolicyManager = (DevicePolicyManager)
+ getContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
+ alertDialogBuilder.setPositiveButton(R.string.identity_check_lockout_error_lock_screen,
+ (dialog, which) -> {
+ dialog.dismiss();
+ devicePolicyManager.lockNow();
+ });
+ } else {
+ alertDialogBuilder.setPositiveButton(R.string.identity_check_biometric_error_ok,
+ (dialog, which) -> dialog.dismiss());
+ }
+ }
+
+ private void setNegativeButton(AlertDialog.Builder alertDialogBuilder, boolean lockout) {
+ if (lockout) {
+ alertDialogBuilder.setNegativeButton(R.string.identity_check_biometric_error_cancel,
+ (dialog, which) -> dialog.dismiss());
+ } else {
+ alertDialogBuilder.setNegativeButton(R.string.go_to_identity_check,
+ (dialog, which) -> {
+ final Intent autoLockSettingsIntent = new Intent(
+ mActionIdentityCheckSettings);
+ final ResolveInfo autoLockSettingsInfo = getActivity().getPackageManager()
+ .resolveActivity(autoLockSettingsIntent, 0 /* flags */);
+ if (autoLockSettingsInfo != null) {
+ startActivity(autoLockSettingsIntent);
+ } else {
+ Log.e(TAG, "Identity check settings intent could not be resolved.");
+ }
+ });
+ }
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return 0;
+ }
+}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 5933015017b..6533622d29e 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -76,6 +76,7 @@ import com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngle
import com.android.settings.development.qstile.DevelopmentTiles;
import com.android.settings.development.storage.SharedDataPreferenceController;
import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.password.ConfirmDeviceCredentialActivity;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.search.actionbar.SearchMenuController;
import com.android.settings.widget.SettingsMainSwitchBar;
@@ -377,6 +378,8 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
userId, false /* hideBackground */);
} else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) {
mSwitchBar.setChecked(false);
+ BiometricErrorDialog.showBiometricErrorDialog(
+ getActivity(), biometricAuthStatus);
} else {
//Reset biometrics once enable dialog is shown
mIsBiometricsAuthenticated = false;
@@ -559,6 +562,10 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
if (resultCode == RESULT_OK) {
mIsBiometricsAuthenticated = true;
mSwitchBar.setChecked(true);
+ } else if (resultCode
+ == ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) {
+ BiometricErrorDialog.showBiometricErrorDialog(getActivity(),
+ Utils.BiometricStatus.LOCKOUT);
}
}
for (AbstractPreferenceController controller : mPreferenceControllers) {
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
index e987ebea1cb..d656934a26b 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
@@ -43,6 +43,7 @@ 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.Flags;
import android.hardware.biometrics.PromptInfo;
import android.os.Bundle;
import android.os.Handler;
@@ -82,6 +83,7 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity {
"biometric_prompt_negative_button_text";
public static final String BIOMETRIC_PROMPT_HIDE_BACKGROUND =
"biometric_prompt_hide_background";
+ public static final int BIOMETRIC_LOCKOUT_ERROR_RESULT = 100;
public static class InternalActivity extends ConfirmDeviceCredentialActivity {
}
@@ -129,6 +131,10 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity {
showConfirmCredentials();
} else {
Log.i(TAG, "Finishing, device credential not requested");
+ if (Flags.mandatoryBiometrics()
+ && errorCode == BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT) {
+ setResult(BIOMETRIC_LOCKOUT_ERROR_RESULT);
+ }
finish();
}
}
diff --git a/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java
index 9f45edb4930..334b7f133bb 100644
--- a/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java
@@ -41,6 +41,8 @@ import androidx.fragment.app.FragmentActivity;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.password.ConfirmDeviceCredentialActivity;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settings.widget.SettingsMainSwitchBar;
@@ -233,6 +235,21 @@ public class DevelopmentSettingsDashboardFragmentTest {
assertThat(ShadowEnableDevelopmentSettingWarningDialog.mShown).isTrue();
}
+ @Test
+ @Config(shadows = ShadowBiometricErrorDialog.class)
+ @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS)
+ public void onActivityResult_requestBiometricPrompt_showErrorDialog() {
+ when(mDashboard.getContext()).thenReturn(mContext);
+
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0);
+ mDashboard.onActivityResult(DevelopmentSettingsDashboardFragment.REQUEST_BIOMETRIC_PROMPT,
+ ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT, null);
+
+ assertThat(mSwitchBar.isChecked()).isFalse();
+ assertThat(ShadowBiometricErrorDialog.sShown).isTrue();
+ }
+
@Test
@Ignore
@Config(shadows = ShadowEnableDevelopmentSettingWarningDialog.class)
@@ -362,6 +379,16 @@ public class DevelopmentSettingsDashboardFragmentTest {
}
}
+ @Implements(BiometricErrorDialog.class)
+ public static class ShadowBiometricErrorDialog {
+ static boolean sShown;
+ @Implementation
+ public static void showBiometricErrorDialog(FragmentActivity fragmentActivity,
+ Utils.BiometricStatus errorCode) {
+ sShown = true;
+ }
+ }
+
@Implements(DisableDevSettingsDialogFragment.class)
public static class ShadowDisableDevSettingsDialogFragment {