(1/N) Biometric error dialog

Add an error dialog to help user recover from biometric error for
identity check feature

Flags: android.hardware.biometrics.flag.mandatory_biometrics
Fixes: 358641110
Fixes: 358179610
Test: atest DevelopmentSettingsDashboardFragmentTest

Change-Id: I6099bc57672b945fa4fa4de98be35bd097403b22
This commit is contained in:
Diya Bera
2024-09-16 22:48:11 +00:00
parent 3f7e49dbc6
commit 5335e26b29
6 changed files with 325 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="24dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:src="@drawable/ic_settings_safety_center"
android:paddingTop="24dp"/>
<TextView
android:id="@id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="?android:attr/textAppearanceLarge"
android:paddingTop="16dp"/>
<TextView
android:id="@+id/description_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:paddingTop="16dp"
android:lineSpacingMultiplier="1.2"/>
<TextView
android:id="@+id/description_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:lineSpacingMultiplier="1.2"
android:paddingTop="16dp"/>
</LinearLayout>

View File

@@ -938,6 +938,28 @@
<string name="security_settings_fingerprint_multiple_face_watch_preference_summary">Face, fingerprints, and <xliff:g id="watch" example="Dani's Watch">%s</xliff:g> added</string> <string name="security_settings_fingerprint_multiple_face_watch_preference_summary">Face, fingerprints, and <xliff:g id="watch" example="Dani's Watch">%s</xliff:g> added</string>
<!-- Description for mandatory biometrics prompt--> <!-- Description for mandatory biometrics prompt-->
<string name="mandatory_biometrics_prompt_description">Identity Check is on and requires a biometric</string> <string name="mandatory_biometrics_prompt_description">Identity Check is on and requires a biometric</string>
<!-- Text for link to identity check settings [CHAR LIMIT=100] -->
<string name="go_to_settings">Go to Settings</string>
<!-- Dialog title when identity check auth is requested but biometrics is in lockout state [CHAR LIMIT=NONE] -->
<string name="identity_check_lockout_error_title">Identity Check is on and cant verify its you</string>
<!-- Dialog title when identity check auth is requested but biometric hardware has an error. [CHAR LIMIT=NONE] -->
<string name="identity_check_lockout_error_description_1">Biometrics failed too many times. Lock and unlock your device to retry.</string>
<!-- Dialog message when identity check auth is requested but biometrics is in lockout state. "Go to Settings" launches theft protection settings, in case user wishes to disable the feature. [CHAR LIMIT=NONE] -->
<string name="identity_check_lockout_error_description_2">You can manage Identity Check in theft protection settings. Go to Settings</string>
<!-- Dialog title when identity check auth is requested but biometric hardware has an error. [CHAR LIMIT=100] -->
<string name="identity_check_general_error_title">Biometric required to continue</string>
<!-- Dialog message when identity check auth is requested but biometric hardware has an error. [CHAR LIMIT=NONE] -->
<string name="identity_check_general_error_description_1">Identity Check is on and requires a biometric, but your face or fingerprint sensor is unavailable\n<ul><li>Check that your camera is on and try again</li>\n<li>You can turn off Identity Check using your Google Account</li></ul></string>
<!-- Biometric error dialog button text for user to dismiss the dialog. [CHAR LIMIT=20] -->
<string name="identity_check_biometric_error_cancel">Cancel</string>
<!-- Biometric error dialog button text for user to acknowledge the message. [CHAR LIMIT=20] -->
<string name="identity_check_biometric_error_ok">OK</string>
<!-- Biometric error dialog button text to launch identity check settings. [CHAR LIMIT=80] -->
<string name="go_to_identity_check">Go to identity check</string>
<!-- Biometric error dialog button text to lock screen and recover biometric lockout state. [CHAR LIMIT=60] -->
<string name="identity_check_lockout_error_lock_screen">Lock screen</string>
<!-- Action for opening identity check settings page [CHAR LIMIT=NONE] [DO NOT TRANSLATE] -->
<string name="identity_check_settings_action"></string>
<!-- RemoteAuth unlock enrollment and settings --><skip /> <!-- RemoteAuth unlock enrollment and settings --><skip />
<!-- Title shown for menu item that launches watch unlock settings. [CHAR LIMIT=40] --> <!-- Title shown for menu item that launches watch unlock settings. [CHAR LIMIT=40] -->
<string name ="security_settings_remoteauth_preference_title">Remote Authenticator Unlock</string> <string name ="security_settings_remoteauth_preference_title">Remote Authenticator Unlock</string>

View File

@@ -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;
}
}

View File

@@ -76,6 +76,7 @@ import com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngle
import com.android.settings.development.qstile.DevelopmentTiles; import com.android.settings.development.qstile.DevelopmentTiles;
import com.android.settings.development.storage.SharedDataPreferenceController; import com.android.settings.development.storage.SharedDataPreferenceController;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ConfirmDeviceCredentialActivity;
import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.search.actionbar.SearchMenuController; import com.android.settings.search.actionbar.SearchMenuController;
import com.android.settings.widget.SettingsMainSwitchBar; import com.android.settings.widget.SettingsMainSwitchBar;
@@ -377,6 +378,8 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
userId, false /* hideBackground */); userId, false /* hideBackground */);
} else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) {
mSwitchBar.setChecked(false); mSwitchBar.setChecked(false);
BiometricErrorDialog.showBiometricErrorDialog(
getActivity(), biometricAuthStatus);
} else { } else {
//Reset biometrics once enable dialog is shown //Reset biometrics once enable dialog is shown
mIsBiometricsAuthenticated = false; mIsBiometricsAuthenticated = false;
@@ -559,6 +562,10 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
mIsBiometricsAuthenticated = true; mIsBiometricsAuthenticated = true;
mSwitchBar.setChecked(true); mSwitchBar.setChecked(true);
} else if (resultCode
== ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) {
BiometricErrorDialog.showBiometricErrorDialog(getActivity(),
Utils.BiometricStatus.LOCKOUT);
} }
} }
for (AbstractPreferenceController controller : mPreferenceControllers) { for (AbstractPreferenceController controller : mPreferenceControllers) {

View File

@@ -43,6 +43,7 @@ import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
import android.hardware.biometrics.Flags;
import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.PromptInfo;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@@ -82,6 +83,7 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity {
"biometric_prompt_negative_button_text"; "biometric_prompt_negative_button_text";
public static final String BIOMETRIC_PROMPT_HIDE_BACKGROUND = public static final String BIOMETRIC_PROMPT_HIDE_BACKGROUND =
"biometric_prompt_hide_background"; "biometric_prompt_hide_background";
public static final int BIOMETRIC_LOCKOUT_ERROR_RESULT = 100;
public static class InternalActivity extends ConfirmDeviceCredentialActivity { public static class InternalActivity extends ConfirmDeviceCredentialActivity {
} }
@@ -129,6 +131,10 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity {
showConfirmCredentials(); showConfirmCredentials();
} else { } else {
Log.i(TAG, "Finishing, device credential not requested"); Log.i(TAG, "Finishing, device credential not requested");
if (Flags.mandatoryBiometrics()
&& errorCode == BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT) {
setResult(BIOMETRIC_LOCKOUT_ERROR_RESULT);
}
finish(); finish();
} }
} }

View File

@@ -41,6 +41,8 @@ import androidx.fragment.app.FragmentActivity;
import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R; 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.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settings.widget.SettingsMainSwitchBar; import com.android.settings.widget.SettingsMainSwitchBar;
@@ -233,6 +235,21 @@ public class DevelopmentSettingsDashboardFragmentTest {
assertThat(ShadowEnableDevelopmentSettingWarningDialog.mShown).isTrue(); 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 @Test
@Ignore @Ignore
@Config(shadows = ShadowEnableDevelopmentSettingWarningDialog.class) @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) @Implements(DisableDevSettingsDialogFragment.class)
public static class ShadowDisableDevSettingsDialogFragment { public static class ShadowDisableDevSettingsDialogFragment {