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 + + 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 {