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
This commit is contained in:
Kevin Chyn
2019-01-04 14:22:34 -08:00
parent 158fefb72d
commit e191271230
10 changed files with 427 additions and 185 deletions

View File

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

View File

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

View File

@@ -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"

View File

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

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {