From e19127123062a077dfc69299ab7065ee36fa2d05 Mon Sep 17 00:00:00 2001 From: Kevin Chyn Date: Fri, 4 Jan 2019 14:22:34 -0800 Subject: [PATCH] 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 --- .../internal/statusbar/IStatusBar.aidl | 4 +- .../internal/statusbar/IStatusBarService.aidl | 4 +- .../res/drawable/biometric_dialog_bg.xml | 2 +- .../biometrics/BiometricDialogImpl.java | 134 ++++----- .../biometrics/BiometricDialogView.java | 134 +++++---- .../systemui/biometrics/FaceDialogView.java | 266 +++++++++++++++++- .../biometrics/FingerprintDialogView.java | 10 +- .../systemui/statusbar/CommandQueue.java | 22 +- .../server/biometrics/BiometricService.java | 21 +- .../statusbar/StatusBarManagerService.java | 15 +- 10 files changed, 427 insertions(+), 185 deletions(-) diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 53b56f2e937a0..6a28059d3fd08 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -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(); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 9087dd219d978..197e873a18bc8 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -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(); } diff --git a/packages/SystemUI/res/drawable/biometric_dialog_bg.xml b/packages/SystemUI/res/drawable/biometric_dialog_bg.xml index d041556187814..0c6d57dd6183f 100644 --- a/packages/SystemUI/res/drawable/biometric_dialog_bg.xml +++ b/packages/SystemUI/res/drawable/biometric_dialog_bg.xml @@ -18,7 +18,7 @@ - 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,32 +217,36 @@ 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); - mCurrentDialog.announceForAccessibility( - mContext.getResources() - .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId())); - if (mCurrentDialog.requiresConfirmation()) { - mConfirmShowing = true; - mCurrentDialog.showConfirmationButton(true /* show */); + if (authenticated) { + mCurrentDialog.announceForAccessibility( + mContext.getResources() + .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId())); + if (mCurrentDialog.requiresConfirmation()) { + mCurrentDialog.showConfirmationButton(true /* show */); + } else { + mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED); + mHandler.postDelayed(() -> { + handleHideDialog(false /* userCanceled */); + }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs()); + } } else { - mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED); - mHandler.postDelayed(() -> { - handleHideDialog(false /* userCanceled */); - }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs()); + 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); } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java index 9934bfd11f12f..b8c69c8003c40 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java @@ -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() { - updateState(STATE_AUTHENTICATING); - mErrorText.setText(getHintStringResourceId()); - mErrorText.setTextColor(mTextColor); + // 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() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java index de3f9471a6bae..359cb047c84de 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java @@ -16,8 +16,18 @@ package com.android.systemui.biometrics; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Outline; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewOutlineProvider; import com.android.systemui.R; @@ -28,13 +38,243 @@ import com.android.systemui.R; */ public class FaceDialogView extends BiometricDialogView { + private static final String TAG = "FaceDialogView"; + private static final String KEY_DIALOG_SIZE = "key_dialog_size"; + private static final int HIDE_DIALOG_DELAY = 500; // ms + private static final int IMPLICIT_Y_PADDING = 16; // dp + private static final int GROW_DURATION = 150; // ms + private static final int TEXT_ANIMATE_DISTANCE = 32; // dp + + private static final int SIZE_UNKNOWN = 0; + private static final int SIZE_SMALL = 1; + private static final int SIZE_GROWING = 2; + private static final int SIZE_BIG = 3; + + private int mSize; + private float mIconOriginalY; + private DialogOutlineProvider mOutlineProvider = new DialogOutlineProvider(); + + private final class DialogOutlineProvider extends ViewOutlineProvider { + + float mY; + + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect( + 0 /* left */, + (int) mY, /* top */ + mDialog.getWidth() /* right */, + mDialog.getBottom(), /* bottom */ + getResources().getDimension(R.dimen.biometric_dialog_corner_size)); + } + + int calculateSmall() { + final float padding = dpToPixels(IMPLICIT_Y_PADDING); + return mDialog.getHeight() - mBiometricIcon.getHeight() - 2 * (int) padding; + } + + void setOutlineY(float y) { + mY = y; + } + } public FaceDialogView(Context context, DialogViewCallback callback) { super(context, callback); } + private void updateSize(int newSize) { + final float padding = dpToPixels(IMPLICIT_Y_PADDING); + final float iconSmallPositionY = mDialog.getHeight() - mBiometricIcon.getHeight() - padding; + + if (newSize == SIZE_SMALL) { + // These fields are required and/or always hold a spot on the UI, so should be set to + // INVISIBLE so they keep their position + mTitleText.setVisibility(View.INVISIBLE); + mErrorText.setVisibility(View.INVISIBLE); + mNegativeButton.setVisibility(View.INVISIBLE); + + // These fields are optional, so set them to gone or invisible depending on their + // usage. If they're empty, they're already set to GONE in BiometricDialogView. + if (!TextUtils.isEmpty(mSubtitleText.getText())) { + mSubtitleText.setVisibility(View.INVISIBLE); + } + if (!TextUtils.isEmpty(mDescriptionText.getText())) { + mDescriptionText.setVisibility(View.INVISIBLE); + } + + // Move the biometric icon to the small spot + mBiometricIcon.setY(iconSmallPositionY); + + // Clip the dialog to the small size + mDialog.setOutlineProvider(mOutlineProvider); + mOutlineProvider.setOutlineY(mOutlineProvider.calculateSmall()); + + mDialog.setClipToOutline(true); + mDialog.invalidateOutline(); + + mSize = newSize; + } else if (mSize == SIZE_SMALL && newSize == SIZE_BIG) { + mSize = SIZE_GROWING; + + // Animate the outline + final ValueAnimator outlineAnimator = + ValueAnimator.ofFloat(mOutlineProvider.calculateSmall(), 0); + outlineAnimator.addUpdateListener((animation) -> { + final float y = (float) animation.getAnimatedValue(); + mOutlineProvider.setOutlineY(y); + mDialog.invalidateOutline(); + }); + + // Animate the icon back to original big position + final ValueAnimator iconAnimator = + ValueAnimator.ofFloat(iconSmallPositionY, mIconOriginalY); + iconAnimator.addUpdateListener((animation) -> { + final float y = (float) animation.getAnimatedValue(); + mBiometricIcon.setY(y); + }); + + // Animate the error text so it slides up with the icon + final ValueAnimator textSlideAnimator = + ValueAnimator.ofFloat(dpToPixels(TEXT_ANIMATE_DISTANCE), 0); + textSlideAnimator.addUpdateListener((animation) -> { + final float y = (float) animation.getAnimatedValue(); + mErrorText.setTranslationY(y); + }); + + // Opacity animator for things that should fade in (title, subtitle, details, negative + // button) + final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1); + opacityAnimator.addUpdateListener((animation) -> { + final float opacity = (float) animation.getAnimatedValue(); + + // These fields are required and/or always hold a spot on the UI + mTitleText.setAlpha(opacity); + mErrorText.setAlpha(opacity); + mNegativeButton.setAlpha(opacity); + mTryAgainButton.setAlpha(opacity); + + // These fields are optional, so only animate them if they're supposed to be showing + if (!TextUtils.isEmpty(mSubtitleText.getText())) { + mSubtitleText.setAlpha(opacity); + } + if (!TextUtils.isEmpty(mDescriptionText.getText())) { + mDescriptionText.setAlpha(opacity); + } + }); + + // Choreograph together + final AnimatorSet as = new AnimatorSet(); + as.setDuration(GROW_DURATION); + as.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + // Set the visibility of opacity-animating views back to VISIBLE + mTitleText.setVisibility(View.VISIBLE); + mErrorText.setVisibility(View.VISIBLE); + mNegativeButton.setVisibility(View.VISIBLE); + mTryAgainButton.setVisibility(View.VISIBLE); + + if (!TextUtils.isEmpty(mSubtitleText.getText())) { + mSubtitleText.setVisibility(View.VISIBLE); + } + if (!TextUtils.isEmpty(mDescriptionText.getText())) { + mDescriptionText.setVisibility(View.VISIBLE); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mSize = SIZE_BIG; + } + }); + as.play(outlineAnimator).with(iconAnimator).with(opacityAnimator) + .with(textSlideAnimator); + as.start(); + } else if (mSize == SIZE_BIG) { + mDialog.setClipToOutline(false); + mDialog.invalidateOutline(); + + mBiometricIcon.setY(mIconOriginalY); + + mSize = newSize; + } + } + + @Override + public void onSaveState(Bundle bundle) { + super.onSaveState(bundle); + bundle.putInt(KEY_DIALOG_SIZE, mSize); + } + + @Override + public void restoreState(Bundle bundle) { + super.restoreState(bundle); + // Keep in mind that this happens before onAttachedToWindow() + mSize = bundle.getInt(KEY_DIALOG_SIZE); + } + + /** + * Do small/big layout here instead of onAttachedToWindow, since: + * 1) We need the big layout to be measured, etc for small -> big animation + * 2) We need the dialog measurements to know where to move the biometric icon to + * + * BiometricDialogView already sets the views to their default big state, so here we only + * need to hide the ones that are unnecessary. + */ + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mIconOriginalY == 0) { + mIconOriginalY = mBiometricIcon.getY(); + } + + // UNKNOWN means size hasn't been set yet. First time we create the dialog. + // onLayout can happen when visibility of views change (during animation, etc). + if (mSize != SIZE_UNKNOWN) { + // Probably not the cleanest way to do this, but since dialog is big by default, + // and small dialogs can persist across orientation changes, we need to set it to + // small size here again. + if (mSize == SIZE_SMALL) { + updateSize(SIZE_SMALL); + } + return; + } + + // If we don't require confirmation, show the small dialog first (until errors occur). + if (!requiresConfirmation()) { + updateSize(SIZE_SMALL); + } else { + updateSize(SIZE_BIG); + } + } + + @Override + public void showErrorMessage(String error) { + super.showErrorMessage(error); + + // All error messages will cause the dialog to go from small -> big. Error messages + // are messages such as lockout, auth failed, etc. + if (mSize == SIZE_SMALL) { + updateSize(SIZE_BIG); + } + } + + @Override + public void showTryAgainButton(boolean show) { + if (show && mSize == SIZE_SMALL) { + // Do not call super, we will nicely animate the alpha together with the rest + // of the elements in here. + updateSize(SIZE_BIG); + } else { + super.showTryAgainButton(show); + } + } + @Override protected int getHintStringResourceId() { return R.string.face_dialog_looking_for_face; @@ -56,7 +296,9 @@ public class FaceDialogView extends BiometricDialogView { @Override protected boolean shouldAnimateForTransition(int oldState, int newState) { - if (oldState == STATE_NONE && newState == STATE_AUTHENTICATING) { + if (oldState == STATE_ERROR && newState == STATE_IDLE) { + return true; + } else if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) { return false; } else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) { return true; @@ -77,10 +319,20 @@ public class FaceDialogView extends BiometricDialogView { return HIDE_DIALOG_DELAY; } + @Override + protected boolean shouldGrayAreaDismissDialog() { + if (mSize == SIZE_SMALL) { + return false; + } + return true; + } + @Override protected Drawable getAnimationForTransition(int oldState, int newState) { int iconRes; - if (oldState == STATE_NONE && newState == STATE_AUTHENTICATING) { + if (oldState == STATE_ERROR && newState == STATE_IDLE) { + iconRes = R.drawable.face_dialog_error_to_face; + } else if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) { iconRes = R.drawable.face_dialog_face_to_error; } else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) { iconRes = R.drawable.face_dialog_face_to_error; @@ -97,4 +349,14 @@ public class FaceDialogView extends BiometricDialogView { } return mContext.getDrawable(iconRes); } + + private float dpToPixels(float dp) { + return dp * ((float) mContext.getResources().getDisplayMetrics().densityDpi + / DisplayMetrics.DENSITY_DEFAULT); + } + + private float pixelsToDp(float pixels) { + return pixels / ((float) mContext.getResources().getDisplayMetrics().densityDpi + / DisplayMetrics.DENSITY_DEFAULT); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java index 1a6cee281c843..d63836b2207e9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java @@ -49,7 +49,7 @@ public class FingerprintDialogView extends BiometricDialogView { @Override protected boolean shouldAnimateForTransition(int oldState, int newState) { - if (oldState == STATE_NONE && newState == STATE_AUTHENTICATING) { + if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) { return false; } else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) { return true; @@ -67,10 +67,16 @@ public class FingerprintDialogView extends BiometricDialogView { return 0; } + @Override + protected boolean shouldGrayAreaDismissDialog() { + // Fingerprint dialog always dismisses when region outside the dialog is tapped + return true; + } + @Override protected Drawable getAnimationForTransition(int oldState, int newState) { int iconRes; - if (oldState == STATE_NONE && newState == STATE_AUTHENTICATING) { + if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) { iconRes = R.drawable.fingerprint_dialog_fp_to_error; } else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) { iconRes = R.drawable.fingerprint_dialog_fp_to_error; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 6a015630a076e..904478efb5687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -111,7 +111,6 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< private static final int MSG_SHOW_CHARGING_ANIMATION = 44 << MSG_SHIFT; private static final int MSG_SHOW_PINNING_TOAST_ENTER_EXIT = 45 << MSG_SHIFT; private static final int MSG_SHOW_PINNING_TOAST_ESCAPE = 46 << MSG_SHIFT; - private static final int MSG_BIOMETRIC_TRY_AGAIN = 47 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -271,11 +270,10 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< default void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, boolean requireConfirmation, int userId) { } - default void onBiometricAuthenticated() { } + default void onBiometricAuthenticated(boolean authenticated) { } default void onBiometricHelp(String message) { } default void onBiometricError(String error) { } default void hideBiometricDialog() { } - default void showBiometricTryAgain() { } } @VisibleForTesting @@ -736,9 +734,9 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } @Override - public void onBiometricAuthenticated() { + public void onBiometricAuthenticated(boolean authenticated) { synchronized (mLock) { - mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED).sendToTarget(); + mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, authenticated).sendToTarget(); } } @@ -763,13 +761,6 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } } - @Override - public void showBiometricTryAgain() { - synchronized (mLock) { - mHandler.obtainMessage(MSG_BIOMETRIC_TRY_AGAIN).sendToTarget(); - } - } - private final class H extends Handler { private H(Looper l) { super(l); @@ -991,7 +982,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< break; case MSG_BIOMETRIC_AUTHENTICATED: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).onBiometricAuthenticated(); + mCallbacks.get(i).onBiometricAuthenticated((boolean) msg.obj); } break; case MSG_BIOMETRIC_HELP: @@ -1024,11 +1015,6 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< mCallbacks.get(i).showPinningEscapeToast(); } break; - case MSG_BIOMETRIC_TRY_AGAIN: - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).showBiometricTryAgain(); - } - break; } } } diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 9bd8d0d579e78..33af9f6f75d4c 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -395,7 +395,7 @@ public class BiometricService extends SystemService { // Notify SysUI that the biometric has been authenticated. SysUI already knows // the implicit/explicit state and will react accordingly. - mStatusBarService.onBiometricAuthenticated(); + mStatusBarService.onBiometricAuthenticated(true); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } @@ -412,17 +412,20 @@ public class BiometricService extends SystemService { return; } - mStatusBarService.onBiometricHelp(getContext().getResources().getString( - com.android.internal.R.string.biometric_not_recognized)); - if (requireConfirmation) { + mStatusBarService.onBiometricAuthenticated(false); + + // TODO: This logic will need to be updated if BP is multi-modal + if ((mCurrentAuthSession.mModality & TYPE_FACE) != 0) { + // Pause authentication. onBiometricAuthenticated(false) causes the + // dialog to show a "try again" button for passive modalities. mCurrentAuthSession.mState = STATE_AUTH_PAUSED; - mStatusBarService.showBiometricTryAgain(); // Cancel authentication. Skip the token/package check since we are // cancelling from system server. The interface is permission protected so // this is fine. cancelInternal(null /* token */, null /* package */, false /* fromClient */); } + mCurrentAuthSession.mClientReceiver.onAuthenticationFailed(); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); @@ -579,8 +582,10 @@ public class BiometricService extends SystemService { } if (mPendingAuthSession.mModalitiesWaiting.isEmpty()) { - final boolean mContinuing = mCurrentAuthSession != null - && mCurrentAuthSession.mState == STATE_AUTH_PAUSED; + final boolean continuing = mCurrentAuthSession != null && + (mCurrentAuthSession.mState == STATE_AUTH_PAUSED + || mCurrentAuthSession.mState == STATE_AUTH_PAUSED_CANCELED); + mCurrentAuthSession = mPendingAuthSession; mPendingAuthSession = null; @@ -602,7 +607,7 @@ public class BiometricService extends SystemService { modality |= pair.getKey(); } - if (!mContinuing) { + if (!continuing) { mStatusBarService.showBiometricDialog(mCurrentAuthSession.mBundle, mInternalReceiver, modality, requireConfirmation, userId); mActivityTaskManager.registerTaskStackListener(mTaskStackListener); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index fc21adbdcca3a..7c1e6198080d1 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -598,11 +598,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onBiometricAuthenticated() { + public void onBiometricAuthenticated(boolean authenticated) { enforceBiometricDialog(); if (mBar != null) { try { - mBar.onBiometricAuthenticated(); + mBar.onBiometricAuthenticated(authenticated); } catch (RemoteException ex) { } } @@ -641,17 +641,6 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } - @Override - public void showBiometricTryAgain() { - enforceBiometricDialog(); - if (mBar != null) { - try { - mBar.showBiometricTryAgain(); - } catch (RemoteException ex) { - } - } - } - // TODO(b/117478341): make it aware of multi-display if needed. @Override public void disable(int what, IBinder token, String pkg) {