From 1c604f29ef13ed97352cc732598b7e6bad1e71b4 Mon Sep 17 00:00:00 2001 From: Diya Bera Date: Sat, 14 Sep 2024 00:26:14 +0000 Subject: [PATCH] (2/N) Biometric error dialog Add an error dialog to help user recover from biometric error for for identity check while trying to factory reset Flag: android.hardware.biometrics.flag.mandatory_biometrics Bug: 358641110 Bug: 358179610 Test: atest MainClearTest Change-Id: Ia20389a3146aa45ad42bdc4d31f1bd9488f2dc42 --- res/values/strings.xml | 4 +- src/com/android/settings/MainClear.java | 10 +++ src/com/android/settings/Utils.java | 1 + .../IdentityCheckBiometricErrorDialog.java} | 65 ++++++++++++------- .../DevelopmentSettingsDashboardFragment.java | 9 +-- .../BuildNumberPreferenceController.java | 16 ++++- .../com/android/settings/MainClearTest.java | 39 +++++++++++ ...elopmentSettingsDashboardFragmentTest.java | 10 +-- .../BuildNumberPreferenceControllerTest.java | 20 ------ 9 files changed, 118 insertions(+), 56 deletions(-) rename src/com/android/settings/{development/BiometricErrorDialog.java => biometrics/IdentityCheckBiometricErrorDialog.java} (76%) diff --git a/res/values/strings.xml b/res/values/strings.xml index 7d740d48131..7a1936d8a99 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -942,8 +942,10 @@ 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. + + Biometrics failed too many times. Try again. You can manage Identity Check in theft protection settings. Go to Settings diff --git a/src/com/android/settings/MainClear.java b/src/com/android/settings/MainClear.java index 711d7943ffa..1800a38fd5b 100644 --- a/src/com/android/settings/MainClear.java +++ b/src/com/android/settings/MainClear.java @@ -65,11 +65,13 @@ import android.widget.TextView; import androidx.annotation.VisibleForTesting; +import com.android.settings.biometrics.IdentityCheckBiometricErrorDialog; import com.android.settings.core.InstrumentedFragment; import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; import com.android.settings.flags.Flags; import com.android.settings.network.SubscriptionUtil; import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settings.password.ConfirmLockPattern; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -178,6 +180,12 @@ public class MainClear extends InstrumentedFragment implements OnGlobalLayoutLis if (resultCode != Activity.RESULT_OK) { establishInitialState(); + if (requestCode == BIOMETRICS_REQUEST) { + if (resultCode == ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) { + IdentityCheckBiometricErrorDialog.showBiometricErrorDialog(getActivity(), + Utils.BiometricStatus.LOCKOUT, true /* twoFactorAuthentication */); + } + } return; } @@ -192,6 +200,8 @@ public class MainClear extends InstrumentedFragment implements OnGlobalLayoutLis userId, false /* hideBackground */); return; } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { + IdentityCheckBiometricErrorDialog.showBiometricErrorDialog(getActivity(), + biometricAuthStatus, true /* twoFactorAuthentication */); return; } } diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 62a4ce36753..57a5380e87d 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -1522,6 +1522,7 @@ public final class Utils extends com.android.settingslib.Utils { case BiometricManager.BIOMETRIC_ERROR_LOCKOUT: return BiometricStatus.LOCKOUT; case BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE: + case BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS: return BiometricStatus.NOT_ACTIVE; default: return BiometricStatus.ERROR; diff --git a/src/com/android/settings/development/BiometricErrorDialog.java b/src/com/android/settings/biometrics/IdentityCheckBiometricErrorDialog.java similarity index 76% rename from src/com/android/settings/development/BiometricErrorDialog.java rename to src/com/android/settings/biometrics/IdentityCheckBiometricErrorDialog.java index 517d019360f..47decddc4c0 100644 --- a/src/com/android/settings/development/BiometricErrorDialog.java +++ b/src/com/android/settings/biometrics/IdentityCheckBiometricErrorDialog.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.development; +package com.android.settings.biometrics; import android.annotation.NonNull; import android.annotation.Nullable; @@ -45,10 +45,12 @@ 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 { +public class IdentityCheckBiometricErrorDialog extends InstrumentedDialogFragment { private static final String TAG = "BiometricErrorDialog"; private static final String KEY_ERROR_CODE = "key_error_code"; + private static final String KEY_TWO_FACTOR_AUTHENTICATION = "key_two_factor_authentication"; + private String mActionIdentityCheckSettings = Settings.ACTION_SETTINGS; @Nullable private BroadcastReceiver mBroadcastReceiver; @@ -62,17 +64,19 @@ public class BiometricErrorDialog extends InstrumentedDialogFragment { Utils.BiometricStatus.LOCKOUT.name()); final View customView = inflater.inflate(R.layout.biometric_lockout_error_dialog, null); + final boolean twoFactorAuthentication = getArguments().getBoolean( + KEY_TWO_FACTOR_AUTHENTICATION); 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); + setBody(customView, isLockoutError, twoFactorAuthentication); alertDialogBuilder.setView(customView); - setPositiveButton(alertDialogBuilder, isLockoutError); - setNegativeButton(alertDialogBuilder, isLockoutError); - + setPositiveButton(alertDialogBuilder, isLockoutError, twoFactorAuthentication); + if (!isLockoutError || !twoFactorAuthentication) { + setNegativeButton(alertDialogBuilder, isLockoutError); + } if (isLockoutError) { mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -103,15 +107,16 @@ public class BiometricErrorDialog extends InstrumentedDialogFragment { * @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(); + public static void showBiometricErrorDialog(FragmentActivity fragmentActivity, + Utils.BiometricStatus errorCode, boolean twoFactorAuthentication) { + final IdentityCheckBiometricErrorDialog identityCheckBiometricErrorDialog = + new IdentityCheckBiometricErrorDialog(); final Bundle args = new Bundle(); args.putCharSequence(KEY_ERROR_CODE, errorCode.name()); - biometricErrorDialog.setArguments(args); - biometricErrorDialog.show(fragmentActivity.getSupportFragmentManager(), - BiometricErrorDialog.class.getName()); - return biometricErrorDialog; + args.putBoolean(KEY_TWO_FACTOR_AUTHENTICATION, twoFactorAuthentication); + identityCheckBiometricErrorDialog.setArguments(args); + identityCheckBiometricErrorDialog.show(fragmentActivity.getSupportFragmentManager(), + IdentityCheckBiometricErrorDialog.class.getName()); } private void setTitle(View view, boolean lockout) { @@ -123,12 +128,17 @@ public class BiometricErrorDialog extends InstrumentedDialogFragment { } } - private void setBody(View view, boolean lockout) { + private void setBody(View view, boolean lockout, boolean twoFactorAuthentication) { 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); + if (twoFactorAuthentication) { + textView1.setText( + R.string.identity_check_lockout_error_two_factor_auth_description_1); + } else { + textView1.setText(R.string.identity_check_lockout_error_description_1); + } textView2.setText(getClickableDescriptionForLockoutError()); textView2.setMovementMethod(LinkMovementMethod.getInstance()); } else { @@ -167,15 +177,22 @@ public class BiometricErrorDialog extends InstrumentedDialogFragment { return spannableString; } - private void setPositiveButton(AlertDialog.Builder alertDialogBuilder, boolean lockout) { + private void setPositiveButton(AlertDialog.Builder alertDialogBuilder, boolean lockout, + boolean twoFactorAuthentication) { 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(); - }); + if (twoFactorAuthentication) { + alertDialogBuilder.setPositiveButton((R.string.okay), + (dialog, which) -> dialog.dismiss()); + } else { + 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()); diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 6533622d29e..09b7503397e 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -57,6 +57,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; +import com.android.settings.biometrics.IdentityCheckBiometricErrorDialog; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settings.development.autofill.AutofillCategoryController; @@ -378,8 +379,8 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra userId, false /* hideBackground */); } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { mSwitchBar.setChecked(false); - BiometricErrorDialog.showBiometricErrorDialog( - getActivity(), biometricAuthStatus); + IdentityCheckBiometricErrorDialog.showBiometricErrorDialog(getActivity(), + biometricAuthStatus, false /* twoFactorAuthentication */); } else { //Reset biometrics once enable dialog is shown mIsBiometricsAuthenticated = false; @@ -564,8 +565,8 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra mSwitchBar.setChecked(true); } else if (resultCode == ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) { - BiometricErrorDialog.showBiometricErrorDialog(getActivity(), - Utils.BiometricStatus.LOCKOUT); + IdentityCheckBiometricErrorDialog.showBiometricErrorDialog(getActivity(), + Utils.BiometricStatus.LOCKOUT, false /* twoFactorAuthentication */); } } for (AbstractPreferenceController controller : mPreferenceControllers) { diff --git a/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java b/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java index a9f94b49b45..e8a4e8d2923 100644 --- a/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java +++ b/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java @@ -36,10 +36,12 @@ import androidx.preference.Preference; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.Utils; +import com.android.settings.biometrics.IdentityCheckBiometricErrorDialog; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -235,10 +237,18 @@ public class BuildNumberPreferenceController extends BasePreferenceController im userId, false /* hideBackground */); } else if (biometricAuthStatus == Utils.BiometricStatus.NOT_ACTIVE) { enableDevelopmentSettings(); + } else { + IdentityCheckBiometricErrorDialog.showBiometricErrorDialog(mFragment.getActivity(), + biometricAuthStatus, true /* twoFactorAuthentication */); + } + } else if (requestCode == REQUEST_IDENTITY_CHECK_FOR_DEV_PREF) { + if (resultCode == Activity.RESULT_OK) { + enableDevelopmentSettings(); + } else if (resultCode + == ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) { + IdentityCheckBiometricErrorDialog.showBiometricErrorDialog(mFragment.getActivity(), + Utils.BiometricStatus.LOCKOUT, true /* twoFactorAuthentication */); } - } else if (requestCode == REQUEST_IDENTITY_CHECK_FOR_DEV_PREF - && resultCode == Activity.RESULT_OK) { - enableDevelopmentSettings(); } mProcessingLastDevHit = false; return true; diff --git a/tests/robotests/src/com/android/settings/MainClearTest.java b/tests/robotests/src/com/android/settings/MainClearTest.java index b705ae14cac..0f823d6ab33 100644 --- a/tests/robotests/src/com/android/settings/MainClearTest.java +++ b/tests/robotests/src/com/android/settings/MainClearTest.java @@ -55,7 +55,11 @@ import android.widget.LinearLayout; import android.widget.ScrollView; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import com.android.settings.biometrics.IdentityCheckBiometricErrorDialog; +import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settings.testutils.shadow.ShadowUtils; import com.android.settingslib.development.DevelopmentSettingsEnabler; @@ -114,6 +118,10 @@ public class MainClearTest { @Mock private Intent mMockIntent; + @Mock + private FragmentManager mMockFragmentManager; + @Mock + private FragmentTransaction mMockFragmentTransaction; private MainClear mMainClear; private ShadowActivity mShadowActivity; @@ -391,6 +399,9 @@ public class MainClearTest { @Test @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) public void testOnActivityResultInternal_keyguardRequestNotTriggeringBiometricPrompt_lockoutError() { + final ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(IdentityCheckBiometricErrorDialog.class); + when(mContext.getResources()).thenReturn(mResources); when(mMockActivity.getSystemService(BiometricManager.class)).thenReturn(mBiometricManager); when(mResources.getString(anyInt())).thenReturn(TEST_ACCOUNT_NAME); @@ -400,12 +411,17 @@ public class MainClearTest { doReturn(true).when(mMainClear).isValidRequestCode(eq(MainClear.KEYGUARD_REQUEST)); doNothing().when(mMainClear).startActivityForResult(any(), anyInt()); doReturn(mMockActivity).when(mMainClear).getActivity(); + doReturn(mMockFragmentManager).when(mMockActivity).getSupportFragmentManager(); + doReturn(mMockFragmentTransaction).when(mMockFragmentManager).beginTransaction(); doReturn(mContext).when(mMainClear).getContext(); mMainClear .onActivityResultInternal(MainClear.KEYGUARD_REQUEST, Activity.RESULT_OK, null); verify(mMainClear).isValidRequestCode(eq(MainClear.KEYGUARD_REQUEST)); + verify(mMainClear.getActivity().getSupportFragmentManager().beginTransaction()).add( + argumentCaptor.capture(), any()); + assertThat(argumentCaptor.getValue()).isInstanceOf(IdentityCheckBiometricErrorDialog.class); verify(mMainClear, never()).startActivityForResult(any(), eq(MainClear.BIOMETRICS_REQUEST)); verify(mMainClear, never()).establishInitialState(); verify(mMainClear, never()).getAccountConfirmationIntent(); @@ -427,6 +443,29 @@ public class MainClearTest { verify(mMainClear).showFinalConfirmation(); } + @Test + public void testOnActivityResultInternal_biometricRequestTriggeringBiometricErrorDialog() { + final ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(IdentityCheckBiometricErrorDialog.class); + + doReturn(true).when(mMainClear).isValidRequestCode( + eq(MainClear.BIOMETRICS_REQUEST)); + doNothing().when(mMainClear).establishInitialState(); + doReturn(mMockActivity).when(mMainClear).getActivity(); + doReturn(mMockFragmentManager).when(mMockActivity).getSupportFragmentManager(); + doReturn(mMockFragmentTransaction).when(mMockFragmentManager).beginTransaction(); + doReturn(mContext).when(mMainClear).getContext(); + + mMainClear + .onActivityResultInternal(MainClear.BIOMETRICS_REQUEST, + ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT, null); + + verify(mMainClear).isValidRequestCode(eq(MainClear.BIOMETRICS_REQUEST)); + verify(mMainClear.getActivity().getSupportFragmentManager().beginTransaction()).add( + argumentCaptor.capture(), any()); + verify(mMainClear).establishInitialState(); + } + @Test public void testOnActivityResultInternal_biometricRequestTriggeringInitialState() { doReturn(true).when(mMainClear).isValidRequestCode(eq(MainClear.BIOMETRICS_REQUEST)); diff --git a/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java index 334b7f133bb..9b6896c38e8 100644 --- a/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/development/DevelopmentSettingsDashboardFragmentTest.java @@ -42,6 +42,7 @@ 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.biometrics.IdentityCheckBiometricErrorDialog; import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowUserManager; @@ -236,7 +237,8 @@ public class DevelopmentSettingsDashboardFragmentTest { } @Test - @Config(shadows = ShadowBiometricErrorDialog.class) + @Config(shadows = ShadowIdentityCheckBiometricErrorDialog.class) + @Ignore("b/354820314") @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) public void onActivityResult_requestBiometricPrompt_showErrorDialog() { when(mDashboard.getContext()).thenReturn(mContext); @@ -247,7 +249,7 @@ public class DevelopmentSettingsDashboardFragmentTest { ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT, null); assertThat(mSwitchBar.isChecked()).isFalse(); - assertThat(ShadowBiometricErrorDialog.sShown).isTrue(); + assertThat(ShadowIdentityCheckBiometricErrorDialog.sShown).isTrue(); } @Test @@ -379,8 +381,8 @@ public class DevelopmentSettingsDashboardFragmentTest { } } - @Implements(BiometricErrorDialog.class) - public static class ShadowBiometricErrorDialog { + @Implements(IdentityCheckBiometricErrorDialog.class) + public static class ShadowIdentityCheckBiometricErrorDialog { static boolean sShown; @Implementation public static void showBiometricErrorDialog(FragmentActivity fragmentActivity, diff --git a/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java index 34878e1ef2c..7e942d9e94e 100644 --- a/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java @@ -246,26 +246,6 @@ public class BuildNumberPreferenceControllerTest { eq(BuildNumberPreferenceController.REQUEST_IDENTITY_CHECK_FOR_DEV_PREF)); } - @Test - @UiThreadTest - @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) - public void onActivityResult_confirmPasswordRequestCompleted_lockoutError() { - when(mUserManager.isAdminUser()).thenReturn(true); - when(mBiometricManager.canAuthenticate(mContext.getUserId(), - BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) - .thenReturn(BiometricManager.BIOMETRIC_ERROR_LOCKOUT); - - final boolean activityResultHandled = mController.onActivityResult( - BuildNumberPreferenceController.REQUEST_CONFIRM_PASSWORD_FOR_DEV_PREF, - Activity.RESULT_OK, - null); - - assertThat(activityResultHandled).isTrue(); - verify(mFragment, never()).startActivityForResult(any(), - eq(BuildNumberPreferenceController.REQUEST_IDENTITY_CHECK_FOR_DEV_PREF)); - assertThat(DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(mContext)).isFalse(); - } - @Test public void onActivityResult_confirmBiometricAuthentication_enableDevPref() { when(mUserManager.isAdminUser()).thenReturn(true);