Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e1912712 authored by Kevin Chyn's avatar Kevin Chyn
Browse files

2/n: Add BiometricPrompt implicit UI

In small mode, tapping the gray are is ignored. Combined
StatusBar#showBiometricTryAgain into onBiometricAuthenticated(bool)

We now create a new BiometricDialogView object for each BiometricPrompt
authenticate call. This makes the view's lifecycle much easier to manage.

Bug: 111461540

Test: Small -> Big when error or rejected
Test: Small -> Authenticated looks good
Test: Try again button is shown when rejected
Test: Icon spacing looks good after animation
Test: Big/small state persists across configuration change

Change-Id: Id0157a7506cea9b0e7de079c43f8bd5ba3cbd8c5
parent 158fefb7
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -153,13 +153,11 @@ oneway interface IStatusBar
    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type,
            boolean requireConfirmation, int userId);
    // Used to hide the dialog when a biometric is authenticated
    void onBiometricAuthenticated();
    void onBiometricAuthenticated(boolean authenticated);
    // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
    void onBiometricHelp(String message);
    // Used to set a message - the dialog will dismiss after a certain amount of time
    void onBiometricError(String error);
    // Used to hide the biometric dialog when the AuthenticationClient is stopped
    void hideBiometricDialog();
    // Used to request the "try again" button for authentications which requireConfirmation=true
    void showBiometricTryAgain();
}
+1 −3
Original line number Diff line number Diff line
@@ -97,13 +97,11 @@ interface IStatusBarService
    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type,
            boolean requireConfirmation, int userId);
    // Used to hide the dialog when a biometric is authenticated
    void onBiometricAuthenticated();
    void onBiometricAuthenticated(boolean authenticated);
    // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
    void onBiometricHelp(String message);
    // Used to set a message - the dialog will dismiss after a certain amount of time
    void onBiometricError(String error);
    // Used to hide the biometric dialog when the AuthenticationClient is stopped
    void hideBiometricDialog();
    // Used to request the "try again" button for authentications which requireConfirmation=true
    void showBiometricTryAgain();
}
+1 −1
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="?android:attr/colorBackgroundFloating" />
    <corners android:radius="1dp"
    <corners
        android:topLeftRadius="@dimen/biometric_dialog_corner_size"
        android:topRightRadius="@dimen/biometric_dialog_corner_size"
        android:bottomLeftRadius="@dimen/biometric_dialog_corner_size"
+62 −74
Original line number Diff line number Diff line
@@ -33,9 +33,6 @@ import com.android.internal.os.SomeArgs;
import com.android.systemui.SystemUI;
import com.android.systemui.statusbar.CommandQueue;

import java.util.HashMap;
import java.util.Map;

/**
 * Receives messages sent from AuthenticationClient and shows the appropriate biometric UI (e.g.
 * BiometricDialogView).
@@ -52,10 +49,8 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
    private static final int MSG_BUTTON_NEGATIVE = 6;
    private static final int MSG_USER_CANCELED = 7;
    private static final int MSG_BUTTON_POSITIVE = 8;
    private static final int MSG_BIOMETRIC_SHOW_TRY_AGAIN = 9;
    private static final int MSG_TRY_AGAIN_PRESSED = 10;
    private static final int MSG_TRY_AGAIN_PRESSED = 9;

    private Map<Integer, BiometricDialogView> mDialogs; // BiometricAuthenticator type, view
    private SomeArgs mCurrentDialogArgs;
    private BiometricDialogView mCurrentDialog;
    private WindowManager mWindowManager;
@@ -63,21 +58,22 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
    private boolean mDialogShowing;
    private Callback mCallback = new Callback();

    private boolean mTryAgainShowing; // No good place to save state before config change :/
    private boolean mConfirmShowing; // No good place to save state before config change :/

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case MSG_SHOW_DIALOG:
                    handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */);
                    handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */,
                            null /* savedState */);
                    break;
                case MSG_BIOMETRIC_AUTHENTICATED:
                    handleBiometricAuthenticated();
                    handleBiometricAuthenticated((boolean) msg.obj);
                    break;
                case MSG_BIOMETRIC_HELP:
                    handleBiometricHelp((String) msg.obj);
                    SomeArgs args = (SomeArgs) msg.obj;
                    handleBiometricHelp((String) args.arg1 /* message */,
                            (boolean) args.arg2 /* requireTryAgain */);
                    args.recycle();
                    break;
                case MSG_BIOMETRIC_ERROR:
                    handleBiometricError((String) msg.obj);
@@ -94,9 +90,6 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
                case MSG_BUTTON_POSITIVE:
                    handleButtonPositive();
                    break;
                case MSG_BIOMETRIC_SHOW_TRY_AGAIN:
                    handleShowTryAgain();
                    break;
                case MSG_TRY_AGAIN_PRESSED:
                    handleTryAgainPressed();
                    break;
@@ -137,26 +130,15 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba

    @Override
    public void start() {
        createDialogs();

        if (!mDialogs.isEmpty()) {
        final PackageManager pm = mContext.getPackageManager();
        if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
                || pm.hasSystemFeature(PackageManager.FEATURE_FACE)
                || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
            getComponent(CommandQueue.class).addCallback(this);
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }
    }

    private void createDialogs() {
        final PackageManager pm = mContext.getPackageManager();
        mDialogs = new HashMap<>();
        if (pm.hasSystemFeature(PackageManager.FEATURE_FACE)) {
            mDialogs.put(BiometricAuthenticator.TYPE_FACE, new FaceDialogView(mContext, mCallback));
        }
        if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
            mDialogs.put(BiometricAuthenticator.TYPE_FINGERPRINT,
                    new FingerprintDialogView(mContext, mCallback));
        }
    }

    @Override
    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
            int type, boolean requireConfirmation, int userId) {
@@ -179,15 +161,18 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
    }

    @Override
    public void onBiometricAuthenticated() {
        if (DEBUG) Log.d(TAG, "onBiometricAuthenticated");
        mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED).sendToTarget();
    public void onBiometricAuthenticated(boolean authenticated) {
        if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: " + authenticated);
        mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, authenticated).sendToTarget();
    }

    @Override
    public void onBiometricHelp(String message) {
        if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message);
        mHandler.obtainMessage(MSG_BIOMETRIC_HELP, message).sendToTarget();
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = message;
        args.arg2 = false; // requireTryAgain
        mHandler.obtainMessage(MSG_BIOMETRIC_HELP, args).sendToTarget();
    }

    @Override
@@ -202,16 +187,21 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
        mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget();
    }

    @Override
    public void showBiometricTryAgain() {
        if (DEBUG) Log.d(TAG, "showBiometricTryAgain");
        mHandler.obtainMessage(MSG_BIOMETRIC_SHOW_TRY_AGAIN).sendToTarget();
    }

    private void handleShowDialog(SomeArgs args, boolean skipAnimation) {
    private void handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
        mCurrentDialogArgs = args;
        final int type = args.argi1;
        mCurrentDialog = mDialogs.get(type);

        if (type == BiometricAuthenticator.TYPE_FINGERPRINT) {
            mCurrentDialog = new FingerprintDialogView(mContext, mCallback);
        } else if (type == BiometricAuthenticator.TYPE_FACE) {
            mCurrentDialog = new FaceDialogView(mContext, mCallback);
        } else {
            Log.e(TAG, "Unsupported type: " + type);
        }

        if (savedState != null) {
            mCurrentDialog.restoreState(savedState);
        }

        if (DEBUG) Log.d(TAG, "handleShowDialog, isAnimatingAway: "
                + mCurrentDialog.isAnimatingAway() + " type: " + type);
@@ -227,20 +217,18 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
        mCurrentDialog.setRequireConfirmation((boolean) args.arg3);
        mCurrentDialog.setUserId(args.argi2);
        mCurrentDialog.setSkipIntro(skipAnimation);
        mCurrentDialog.setPendingTryAgain(mTryAgainShowing);
        mCurrentDialog.setPendingConfirm(mConfirmShowing);
        mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams());
        mDialogShowing = true;
    }

    private void handleBiometricAuthenticated() {
        if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated");
    private void handleBiometricAuthenticated(boolean authenticated) {
        if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated: " + authenticated);

        if (authenticated) {
            mCurrentDialog.announceForAccessibility(
                    mContext.getResources()
                            .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId()));
            if (mCurrentDialog.requiresConfirmation()) {
            mConfirmShowing = true;
                mCurrentDialog.showConfirmationButton(true /* show */);
            } else {
                mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED);
@@ -248,11 +236,17 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
                    handleHideDialog(false /* userCanceled */);
                }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs());
            }
        } else {
            handleBiometricHelp(mContext.getResources()
                    .getString(com.android.internal.R.string.biometric_not_recognized),
                    true /* requireTryAgain */);
            mCurrentDialog.showTryAgainButton(true /* show */);
        }
    }

    private void handleBiometricHelp(String message) {
    private void handleBiometricHelp(String message, boolean requireTryAgain) {
        if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message);
        mCurrentDialog.showHelpMessage(message);
        mCurrentDialog.showHelpMessage(message, requireTryAgain);
    }

    private void handleBiometricError(String error) {
@@ -261,7 +255,6 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
            if (DEBUG) Log.d(TAG, "Dialog already dismissed");
            return;
        }
        mTryAgainShowing = false;
        mCurrentDialog.showErrorMessage(error);
    }

@@ -282,8 +275,6 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
        }
        mReceiver = null;
        mDialogShowing = false;
        mConfirmShowing = false;
        mTryAgainShowing = false;
        mCurrentDialog.startDismiss();
    }

@@ -297,7 +288,6 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception when handling negative button", e);
        }
        mTryAgainShowing = false;
        handleHideDialog(false /* userCanceled */);
    }

@@ -311,25 +301,16 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception when handling positive button", e);
        }
        mConfirmShowing = false;
        handleHideDialog(false /* userCanceled */);
    }

    private void handleUserCanceled() {
        mTryAgainShowing = false;
        mConfirmShowing = false;
        handleHideDialog(true /* userCanceled */);
    }

    private void handleShowTryAgain() {
        mCurrentDialog.showTryAgainButton(true /* show */);
        mTryAgainShowing = true;
    }

    private void handleTryAgainPressed() {
        try {
            mCurrentDialog.clearTemporaryMessage();
            mTryAgainShowing = false;
            mReceiver.onTryAgainPressed();
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException when handling try again", e);
@@ -340,13 +321,20 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        final boolean wasShowing = mDialogShowing;

        // Save the state of the current dialog (buttons showing, etc)
        final Bundle savedState = new Bundle();
        if (mCurrentDialog != null) {
            mCurrentDialog.onSaveState(savedState);
        }

        if (mDialogShowing) {
            mCurrentDialog.forceRemove();
            mDialogShowing = false;
        }
        createDialogs();

        if (wasShowing) {
            handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */);
            handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
        }
    }
}
+72 −62
Original line number Diff line number Diff line
@@ -56,12 +56,15 @@ public abstract class BiometricDialogView extends LinearLayout {

    private static final String TAG = "BiometricDialogView";

    private static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility";
    private static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility";

    private static final int ANIMATION_DURATION_SHOW = 250; // ms
    private static final int ANIMATION_DURATION_AWAY = 350; // ms

    private static final int MSG_CLEAR_MESSAGE = 1;

    protected static final int STATE_NONE = 0;
    protected static final int STATE_IDLE = 0;
    protected static final int STATE_AUTHENTICATING = 1;
    protected static final int STATE_ERROR = 2;
    protected static final int STATE_PENDING_CONFIRMATION = 3;
@@ -78,12 +81,19 @@ public abstract class BiometricDialogView extends LinearLayout {
    private final float mDialogWidth;
    private final DialogViewCallback mCallback;

    private ViewGroup mLayout;
    private final Button mPositiveButton;
    private final Button mNegativeButton;
    private final TextView mErrorText;
    protected final ViewGroup mLayout;
    protected final LinearLayout mDialog;
    protected final TextView mTitleText;
    protected final TextView mSubtitleText;
    protected final TextView mDescriptionText;
    protected final ImageView mBiometricIcon;
    protected final TextView mErrorText;
    protected final Button mPositiveButton;
    protected final Button mNegativeButton;
    protected final Button mTryAgainButton;

    private Bundle mBundle;
    private final LinearLayout mDialog;

    private int mLastState;
    private boolean mAnimatingAway;
    private boolean mWasForceRemoved;
@@ -91,15 +101,13 @@ public abstract class BiometricDialogView extends LinearLayout {
    protected boolean mRequireConfirmation;
    private int mUserId; // used to determine if we should show work background

    private boolean mPendingShowTryAgain;
    private boolean mPendingShowConfirm;

    protected abstract int getHintStringResourceId();
    protected abstract int getAuthenticatedAccessibilityResourceId();
    protected abstract int getIconDescriptionResourceId();
    protected abstract Drawable getAnimationForTransition(int oldState, int newState);
    protected abstract boolean shouldAnimateForTransition(int oldState, int newState);
    protected abstract int getDelayAfterAuthenticatedDurationMs();
    protected abstract boolean shouldGrayAreaDismissDialog();

    private final Runnable mShowAnimationRunnable = new Runnable() {
        @Override
@@ -124,7 +132,7 @@ public abstract class BiometricDialogView extends LinearLayout {
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case MSG_CLEAR_MESSAGE:
                    handleClearMessage();
                    handleClearMessage((boolean) msg.obj /* requireTryAgain */);
                    break;
                default:
                    Log.e(TAG, "Unhandled message: " + msg.what);
@@ -158,10 +166,6 @@ public abstract class BiometricDialogView extends LinearLayout {
        mLayout = (ViewGroup) factory.inflate(R.layout.biometric_dialog, this, false);
        addView(mLayout);

        mDialog = mLayout.findViewById(R.id.dialog);

        mErrorText = mLayout.findViewById(R.id.error);

        mLayout.setOnKeyListener(new View.OnKeyListener() {
            boolean downPressed = false;
            @Override
@@ -184,12 +188,19 @@ public abstract class BiometricDialogView extends LinearLayout {
        final View space = mLayout.findViewById(R.id.space);
        final View leftSpace = mLayout.findViewById(R.id.left_space);
        final View rightSpace = mLayout.findViewById(R.id.right_space);
        final ImageView icon = mLayout.findViewById(R.id.biometric_icon);
        final Button tryAgain = mLayout.findViewById(R.id.button_try_again);

        mDialog = mLayout.findViewById(R.id.dialog);
        mTitleText = mLayout.findViewById(R.id.title);
        mSubtitleText = mLayout.findViewById(R.id.subtitle);
        mDescriptionText = mLayout.findViewById(R.id.description);
        mBiometricIcon = mLayout.findViewById(R.id.biometric_icon);
        mErrorText = mLayout.findViewById(R.id.error);
        mNegativeButton = mLayout.findViewById(R.id.button2);
        mPositiveButton = mLayout.findViewById(R.id.button1);
        mTryAgainButton = mLayout.findViewById(R.id.button_try_again);

        icon.setContentDescription(getResources().getString(getIconDescriptionResourceId()));
        mBiometricIcon.setContentDescription(
                getResources().getString(getIconDescriptionResourceId()));

        setDismissesDialog(space);
        setDismissesDialog(leftSpace);
@@ -206,8 +217,9 @@ public abstract class BiometricDialogView extends LinearLayout {
            }, getDelayAfterAuthenticatedDurationMs());
        });

        tryAgain.setOnClickListener((View v) -> {
        mTryAgainButton.setOnClickListener((View v) -> {
            showTryAgainButton(false /* show */);
            handleClearMessage(false /* requireTryAgain */);
            mCallback.onTryAgainPressed();
        });

@@ -215,15 +227,17 @@ public abstract class BiometricDialogView extends LinearLayout {
        mLayout.requestFocus();
    }

    public void onSaveState(Bundle bundle) {
        bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility());
        bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility());
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        mErrorText.setText(getHintStringResourceId());

        final TextView title = mLayout.findViewById(R.id.title);
        final TextView subtitle = mLayout.findViewById(R.id.subtitle);
        final TextView description = mLayout.findViewById(R.id.description);
        final ImageView backgroundView = mLayout.findViewById(R.id.background);

        if (mUserManager.isManagedProfile(mUserId)) {
@@ -244,36 +258,34 @@ public abstract class BiometricDialogView extends LinearLayout {
            mDialog.getLayoutParams().width = (int) mDialogWidth;
        }

        mLastState = STATE_NONE;
        mLastState = STATE_IDLE;
        updateState(STATE_AUTHENTICATING);

        CharSequence titleText = mBundle.getCharSequence(BiometricPrompt.KEY_TITLE);

        title.setText(titleText);
        title.setSelected(true);
        mTitleText.setVisibility(View.VISIBLE);
        mTitleText.setText(titleText);
        mTitleText.setSelected(true);

        final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
        if (TextUtils.isEmpty(subtitleText)) {
            subtitle.setVisibility(View.GONE);
            mSubtitleText.setVisibility(View.GONE);
        } else {
            subtitle.setVisibility(View.VISIBLE);
            subtitle.setText(subtitleText);
            mSubtitleText.setVisibility(View.VISIBLE);
            mSubtitleText.setText(subtitleText);
        }

        final CharSequence descriptionText =
                mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
        if (TextUtils.isEmpty(descriptionText)) {
            description.setVisibility(View.GONE);
            mDescriptionText.setVisibility(View.GONE);
        } else {
            description.setVisibility(View.VISIBLE);
            description.setText(descriptionText);
            mDescriptionText.setVisibility(View.VISIBLE);
            mDescriptionText.setText(descriptionText);
        }

        mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));

        showTryAgainButton(mPendingShowTryAgain);
        showConfirmationButton(mPendingShowConfirm);

        if (mWasForceRemoved || mSkipIntro) {
            // Show the dialog immediately
            mLayout.animate().cancel();
@@ -302,8 +314,7 @@ public abstract class BiometricDialogView extends LinearLayout {
                ? (AnimatedVectorDrawable) icon
                : null;

        final ImageView imageView = getLayout().findViewById(R.id.biometric_icon);
        imageView.setImageDrawable(icon);
        mBiometricIcon.setImageDrawable(icon);

        if (animation != null && shouldAnimateForTransition(lastState, newState)) {
            animation.forceAnimationOnUI();
@@ -314,7 +325,7 @@ public abstract class BiometricDialogView extends LinearLayout {
    private void setDismissesDialog(View v) {
        v.setClickable(true);
        v.setOnTouchListener((View view, MotionEvent event) -> {
            if (mLastState != STATE_AUTHENTICATED) {
            if (mLastState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) {
                mCallback.onUserCanceled();
            }
            return true;
@@ -331,11 +342,9 @@ public abstract class BiometricDialogView extends LinearLayout {
                mWindowManager.removeView(BiometricDialogView.this);
                mAnimatingAway = false;
                // Set the icons / text back to normal state
                handleClearMessage();
                handleClearMessage(false /* requireTryAgain */);
                showTryAgainButton(false /* show */);
                mPendingShowTryAgain = false;
                mPendingShowConfirm = false;
                updateState(STATE_NONE);
                updateState(STATE_IDLE);
            }
        };

@@ -412,35 +421,42 @@ public abstract class BiometricDialogView extends LinearLayout {
        return mLayout;
    }

    // Clears the temporary message and shows the help message.
    private void handleClearMessage() {
    // Clears the temporary message and shows the help message. If requireTryAgain is true,
    // we will start the authenticating state again.
    private void handleClearMessage(boolean requireTryAgain) {
        if (!requireTryAgain) {
            updateState(STATE_AUTHENTICATING);
            mErrorText.setText(getHintStringResourceId());
            mErrorText.setTextColor(mTextColor);
            mErrorText.setVisibility(View.VISIBLE);
        } else {
            updateState(STATE_IDLE);
            mErrorText.setVisibility(View.INVISIBLE);
        }
    }

    // Shows an error/help message
    private void showTemporaryMessage(String message) {
    private void showTemporaryMessage(String message, boolean requireTryAgain) {
        mHandler.removeMessages(MSG_CLEAR_MESSAGE);
        updateState(STATE_ERROR);
        mErrorText.setText(message);
        mErrorText.setTextColor(mErrorColor);
        mErrorText.setContentDescription(message);
        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CLEAR_MESSAGE),
        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CLEAR_MESSAGE, requireTryAgain),
                BiometricPrompt.HIDE_DIALOG_DELAY);
    }

    public void clearTemporaryMessage() {
        mHandler.removeMessages(MSG_CLEAR_MESSAGE);
        mHandler.obtainMessage(MSG_CLEAR_MESSAGE).sendToTarget();
        mHandler.obtainMessage(MSG_CLEAR_MESSAGE, false /* requireTryAgain */).sendToTarget();
    }

    public void showHelpMessage(String message) {
        showTemporaryMessage(message);
    public void showHelpMessage(String message, boolean requireTryAgain) {
        showTemporaryMessage(message, requireTryAgain);
    }

    public void showErrorMessage(String error) {
        showTemporaryMessage(error);
        showTemporaryMessage(error, false /* requireTryAgain */);
        showTryAgainButton(false /* show */);
        mCallback.onErrorShown();
    }
@@ -459,22 +475,16 @@ public abstract class BiometricDialogView extends LinearLayout {
    }

    public void showTryAgainButton(boolean show) {
        final Button tryAgain = mLayout.findViewById(R.id.button_try_again);
        if (show) {
            tryAgain.setVisibility(View.VISIBLE);
            mTryAgainButton.setVisibility(View.VISIBLE);
        } else {
            tryAgain.setVisibility(View.GONE);
            mTryAgainButton.setVisibility(View.GONE);
        }
    }

    // Set the state before the window is attached, so we know if the dialog should be started
    // with or without the button. This is because there's no good onPause signal
    public void setPendingTryAgain(boolean show) {
        mPendingShowTryAgain = show;
    }

    public void setPendingConfirm(boolean show) {
        mPendingShowConfirm = show;
    public void restoreState(Bundle bundle) {
        mTryAgainButton.setVisibility(bundle.getInt(KEY_TRY_AGAIN_VISIBILITY));
        mPositiveButton.setVisibility(bundle.getInt(KEY_CONFIRM_VISIBILITY));
    }

    public WindowManager.LayoutParams getLayoutParams() {
Loading