10/n: Remove old biometric UI
Test: atest com.android.systemui.biometrics Bug: 123378871 Change-Id: Ic000148f4e4540d143581ca6cc296dba34a3db08
This commit is contained in:
@@ -29,7 +29,6 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.WindowManager;
|
||||
|
||||
@@ -46,8 +45,6 @@ import java.util.List;
|
||||
*/
|
||||
public class AuthController extends SystemUI implements CommandQueue.Callbacks,
|
||||
AuthDialogCallback {
|
||||
private static final String DISABLE_NEW_DIALOG =
|
||||
"com.android.systemui.biometrics.AuthController.DISABLE_NEW_DIALOG";
|
||||
|
||||
private static final String TAG = "BiometricPrompt/AuthController";
|
||||
private static final boolean DEBUG = true;
|
||||
@@ -316,25 +313,13 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
|
||||
|
||||
protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation,
|
||||
int userId, int type, String opPackageName, boolean skipIntro) {
|
||||
if (Settings.Secure.getIntForUser(
|
||||
mContext.getContentResolver(), DISABLE_NEW_DIALOG, userId, 0) == 0) {
|
||||
return new AuthContainerView.Builder(mContext)
|
||||
.setCallback(this)
|
||||
.setBiometricPromptBundle(biometricPromptBundle)
|
||||
.setRequireConfirmation(requireConfirmation)
|
||||
.setUserId(userId)
|
||||
.setOpPackageName(opPackageName)
|
||||
.setSkipIntro(skipIntro)
|
||||
.build(type);
|
||||
} else {
|
||||
return new BiometricDialogView.Builder(mContext)
|
||||
.setCallback(this)
|
||||
.setBiometricPromptBundle(biometricPromptBundle)
|
||||
.setRequireConfirmation(requireConfirmation)
|
||||
.setUserId(userId)
|
||||
.setOpPackageName(opPackageName)
|
||||
.setSkipIntro(skipIntro)
|
||||
.build(type);
|
||||
}
|
||||
return new AuthContainerView.Builder(mContext)
|
||||
.setCallback(this)
|
||||
.setBiometricPromptBundle(biometricPromptBundle)
|
||||
.setRequireConfirmation(requireConfirmation)
|
||||
.setUserId(userId)
|
||||
.setOpPackageName(opPackageName)
|
||||
.setSkipIntro(skipIntro)
|
||||
.build(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,963 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.hardware.biometrics.BiometricAuthenticator;
|
||||
import android.hardware.biometrics.BiometricPrompt;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.UserManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.systemui.Dependency;
|
||||
import com.android.systemui.Interpolators;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.keyguard.WakefulnessLifecycle;
|
||||
import com.android.systemui.util.leak.RotationUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class. Shows a dialog for BiometricPrompt.
|
||||
*/
|
||||
public abstract class BiometricDialogView extends LinearLayout implements AuthDialog {
|
||||
|
||||
private static final String TAG = "BiometricPrompt/DialogView";
|
||||
|
||||
public static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility";
|
||||
public static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility";
|
||||
public static final String KEY_CONFIRM_ENABLED = "key_confirm_enabled";
|
||||
public static final String KEY_STATE = "key_state";
|
||||
public static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility";
|
||||
public static final String KEY_ERROR_TEXT_STRING = "key_error_text_string";
|
||||
public static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary";
|
||||
public static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color";
|
||||
public static final String KEY_DIALOG_SIZE = "key_dialog_size";
|
||||
|
||||
private static final int ANIMATION_DURATION_SHOW = 250; // ms
|
||||
private static final int ANIMATION_DURATION_AWAY = 350; // ms
|
||||
|
||||
protected static final int MSG_RESET_MESSAGE = 1;
|
||||
|
||||
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;
|
||||
protected static final int STATE_AUTHENTICATED = 4;
|
||||
|
||||
// Dialog layout/animation
|
||||
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
|
||||
@VisibleForTesting static final int SIZE_UNKNOWN = 0;
|
||||
@VisibleForTesting static final int SIZE_SMALL = 1;
|
||||
@VisibleForTesting static final int SIZE_GROWING = 2;
|
||||
@VisibleForTesting static final int SIZE_BIG = 3;
|
||||
@IntDef({SIZE_UNKNOWN, SIZE_SMALL, SIZE_GROWING, SIZE_BIG})
|
||||
@interface DialogSize {}
|
||||
|
||||
@VisibleForTesting final WakefulnessLifecycle mWakefulnessLifecycle;
|
||||
private final AccessibilityManager mAccessibilityManager;
|
||||
private final IBinder mWindowToken = new Binder();
|
||||
private final Interpolator mLinearOutSlowIn;
|
||||
private final WindowManager mWindowManager;
|
||||
private final UserManager mUserManager;
|
||||
private final DevicePolicyManager mDevicePolicyManager;
|
||||
private final float mAnimationTranslationOffset;
|
||||
private final int mErrorColor;
|
||||
private final float mDialogWidth;
|
||||
protected final AuthDialogCallback mCallback;
|
||||
private final DialogOutlineProvider mOutlineProvider = new DialogOutlineProvider();
|
||||
|
||||
protected final ViewGroup mLayout;
|
||||
protected final LinearLayout mDialog;
|
||||
@VisibleForTesting final TextView mTitleText;
|
||||
@VisibleForTesting final TextView mSubtitleText;
|
||||
@VisibleForTesting final TextView mDescriptionText;
|
||||
@VisibleForTesting final ImageView mBiometricIcon;
|
||||
@VisibleForTesting final TextView mErrorText;
|
||||
@VisibleForTesting final Button mPositiveButton;
|
||||
@VisibleForTesting final Button mNegativeButton;
|
||||
@VisibleForTesting final Button mTryAgainButton;
|
||||
|
||||
protected final int mTextColor;
|
||||
|
||||
private Bundle mBundle;
|
||||
private Bundle mRestoredState;
|
||||
private String mOpPackageName;
|
||||
|
||||
private int mState = STATE_IDLE;
|
||||
private boolean mWasForceRemoved;
|
||||
private boolean mSkipIntro;
|
||||
protected boolean mRequireConfirmation;
|
||||
private int mUserId; // used to determine if we should show work background
|
||||
private @DialogSize int mSize;
|
||||
private float mIconOriginalY;
|
||||
|
||||
private boolean mCompletedAnimatingIn;
|
||||
private boolean mPendingDismissDialog;
|
||||
|
||||
protected abstract int getHintStringResourceId();
|
||||
protected abstract int getAuthenticatedAccessibilityResourceId();
|
||||
protected abstract int getIconDescriptionResourceId();
|
||||
protected abstract int getDelayAfterAuthenticatedDurationMs();
|
||||
protected abstract boolean shouldGrayAreaDismissDialog();
|
||||
protected abstract void handleResetMessage();
|
||||
protected abstract void updateIcon(int oldState, int newState);
|
||||
protected abstract boolean supportsSmallDialog();
|
||||
|
||||
private final Runnable mShowAnimationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mLayout.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(ANIMATION_DURATION_SHOW)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
mDialog.animate()
|
||||
.translationY(0)
|
||||
.setDuration(ANIMATION_DURATION_SHOW)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.withEndAction(() -> onDialogAnimatedIn())
|
||||
.start();
|
||||
}
|
||||
};
|
||||
|
||||
@VisibleForTesting
|
||||
final WakefulnessLifecycle.Observer mWakefulnessObserver =
|
||||
new WakefulnessLifecycle.Observer() {
|
||||
@Override
|
||||
public void onStartedGoingToSleep() {
|
||||
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
|
||||
}
|
||||
};
|
||||
|
||||
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 = Utils.dpToPixels(mContext, IMPLICIT_Y_PADDING);
|
||||
return mDialog.getHeight() - mBiometricIcon.getHeight() - 2 * (int) padding;
|
||||
}
|
||||
|
||||
void setOutlineY(float y) {
|
||||
mY = y;
|
||||
}
|
||||
}
|
||||
|
||||
protected Handler mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch(msg.what) {
|
||||
case MSG_RESET_MESSAGE:
|
||||
handleResetMessage();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unhandled message: " + msg.what);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the dialog with specified parameters.
|
||||
*/
|
||||
public static class Builder {
|
||||
public static final int TYPE_FINGERPRINT = BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
public static final int TYPE_FACE = BiometricAuthenticator.TYPE_FACE;
|
||||
|
||||
private Context mContext;
|
||||
private AuthDialogCallback mCallback;
|
||||
private Bundle mBundle;
|
||||
private boolean mRequireConfirmation;
|
||||
private int mUserId;
|
||||
private String mOpPackageName;
|
||||
private boolean mSkipIntro;
|
||||
|
||||
public Builder(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Builder setCallback(AuthDialogCallback callback) {
|
||||
mCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBiometricPromptBundle(Bundle bundle) {
|
||||
mBundle = bundle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRequireConfirmation(boolean requireConfirmation) {
|
||||
mRequireConfirmation = requireConfirmation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUserId(int userId) {
|
||||
mUserId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setOpPackageName(String opPackageName) {
|
||||
mOpPackageName = opPackageName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSkipIntro(boolean skipIntro) {
|
||||
mSkipIntro = skipIntro;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BiometricDialogView build(int type) {
|
||||
return build(type, new Injector());
|
||||
}
|
||||
|
||||
public BiometricDialogView build(int type, Injector injector) {
|
||||
BiometricDialogView dialog;
|
||||
if (type == TYPE_FINGERPRINT) {
|
||||
dialog = new FingerprintDialogView(mContext, mCallback, injector);
|
||||
} else if (type == TYPE_FACE) {
|
||||
dialog = new FaceDialogView(mContext, mCallback, injector);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
dialog.setBundle(mBundle);
|
||||
dialog.setRequireConfirmation(mRequireConfirmation);
|
||||
dialog.setUserId(mUserId);
|
||||
dialog.setOpPackageName(mOpPackageName);
|
||||
dialog.setSkipIntro(mSkipIntro);
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Injector {
|
||||
public WakefulnessLifecycle getWakefulnessLifecycle() {
|
||||
return Dependency.get(WakefulnessLifecycle.class);
|
||||
}
|
||||
}
|
||||
|
||||
protected BiometricDialogView(Context context, AuthDialogCallback callback, Injector injector) {
|
||||
super(context);
|
||||
mWakefulnessLifecycle = injector.getWakefulnessLifecycle();
|
||||
|
||||
mCallback = callback;
|
||||
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
|
||||
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
|
||||
mWindowManager = mContext.getSystemService(WindowManager.class);
|
||||
mUserManager = mContext.getSystemService(UserManager.class);
|
||||
mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
|
||||
mAnimationTranslationOffset = getResources()
|
||||
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
|
||||
mErrorColor = getResources().getColor(R.color.biometric_dialog_error);
|
||||
mTextColor = getResources().getColor(R.color.biometric_dialog_gray);
|
||||
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
mWindowManager.getDefaultDisplay().getMetrics(metrics);
|
||||
mDialogWidth = Math.min(metrics.widthPixels, metrics.heightPixels);
|
||||
|
||||
// Create the dialog
|
||||
LayoutInflater factory = LayoutInflater.from(getContext());
|
||||
mLayout = (ViewGroup) factory.inflate(R.layout.biometric_dialog, this, false);
|
||||
addView(mLayout);
|
||||
|
||||
mLayout.setOnKeyListener(new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode != KeyEvent.KEYCODE_BACK) {
|
||||
return false;
|
||||
}
|
||||
if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
mBiometricIcon.setContentDescription(
|
||||
getResources().getString(getIconDescriptionResourceId()));
|
||||
|
||||
setDismissesDialog(space);
|
||||
setDismissesDialog(leftSpace);
|
||||
setDismissesDialog(rightSpace);
|
||||
|
||||
mNegativeButton.setOnClickListener((View v) -> {
|
||||
if (mState == STATE_PENDING_CONFIRMATION || mState == STATE_AUTHENTICATED) {
|
||||
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
|
||||
} else {
|
||||
animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
|
||||
}
|
||||
});
|
||||
|
||||
mPositiveButton.setOnClickListener((View v) -> {
|
||||
updateState(STATE_AUTHENTICATED);
|
||||
mHandler.postDelayed(() -> {
|
||||
animateAway(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
|
||||
}, getDelayAfterAuthenticatedDurationMs());
|
||||
});
|
||||
|
||||
mTryAgainButton.setOnClickListener((View v) -> {
|
||||
handleResetMessage();
|
||||
updateState(STATE_AUTHENTICATING);
|
||||
showTryAgainButton(false /* show */);
|
||||
|
||||
mPositiveButton.setVisibility(View.VISIBLE);
|
||||
mPositiveButton.setEnabled(false);
|
||||
|
||||
mCallback.onTryAgainPressed();
|
||||
});
|
||||
|
||||
// Must set these in order for the back button events to be received.
|
||||
mLayout.setFocusableInTouchMode(true);
|
||||
mLayout.requestFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
|
||||
|
||||
final ImageView backgroundView = mLayout.findViewById(R.id.background);
|
||||
if (mUserManager.isManagedProfile(mUserId)) {
|
||||
final Drawable image = getResources().getDrawable(R.drawable.work_challenge_background,
|
||||
mContext.getTheme());
|
||||
image.setColorFilter(mDevicePolicyManager.getOrganizationColorForUser(mUserId),
|
||||
PorterDuff.Mode.DARKEN);
|
||||
backgroundView.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
backgroundView.setImageDrawable(image);
|
||||
} else {
|
||||
backgroundView.setImageDrawable(null);
|
||||
backgroundView.setBackgroundColor(R.color.biometric_dialog_dim_color);
|
||||
}
|
||||
|
||||
mNegativeButton.setVisibility(View.VISIBLE);
|
||||
mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
|
||||
|
||||
if (RotationUtils.getRotation(mContext) != RotationUtils.ROTATION_NONE) {
|
||||
mDialog.getLayoutParams().width = (int) mDialogWidth;
|
||||
}
|
||||
|
||||
if (mRestoredState == null) {
|
||||
updateState(STATE_AUTHENTICATING);
|
||||
final int hint = getHintStringResourceId();
|
||||
if (hint != 0) {
|
||||
mErrorText.setText(hint);
|
||||
mErrorText.setContentDescription(mContext.getString(hint));
|
||||
mErrorText.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mErrorText.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
announceAccessibilityEvent();
|
||||
} else {
|
||||
updateState(mState);
|
||||
}
|
||||
|
||||
CharSequence titleText = mBundle.getCharSequence(BiometricPrompt.KEY_TITLE);
|
||||
|
||||
mTitleText.setVisibility(View.VISIBLE);
|
||||
mTitleText.setText(titleText);
|
||||
|
||||
final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
|
||||
if (TextUtils.isEmpty(subtitleText)) {
|
||||
mSubtitleText.setVisibility(View.GONE);
|
||||
announceAccessibilityEvent();
|
||||
} else {
|
||||
mSubtitleText.setVisibility(View.VISIBLE);
|
||||
mSubtitleText.setText(subtitleText);
|
||||
}
|
||||
|
||||
final CharSequence descriptionText =
|
||||
mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
|
||||
if (TextUtils.isEmpty(descriptionText)) {
|
||||
mDescriptionText.setVisibility(View.GONE);
|
||||
announceAccessibilityEvent();
|
||||
} else {
|
||||
mDescriptionText.setVisibility(View.VISIBLE);
|
||||
mDescriptionText.setText(descriptionText);
|
||||
}
|
||||
|
||||
if (requiresConfirmation() && mRestoredState == null) {
|
||||
mPositiveButton.setVisibility(View.VISIBLE);
|
||||
mPositiveButton.setEnabled(false);
|
||||
}
|
||||
|
||||
if (mWasForceRemoved || mSkipIntro) {
|
||||
// Show the dialog immediately
|
||||
mLayout.animate().cancel();
|
||||
mDialog.animate().cancel();
|
||||
mDialog.setAlpha(1.0f);
|
||||
mDialog.setTranslationY(0);
|
||||
mLayout.setAlpha(1.0f);
|
||||
mCompletedAnimatingIn = true;
|
||||
} else {
|
||||
// Dim the background and slide the dialog up
|
||||
mDialog.setTranslationY(mAnimationTranslationOffset);
|
||||
mLayout.setAlpha(0f);
|
||||
postOnAnimation(mShowAnimationRunnable);
|
||||
}
|
||||
mWasForceRemoved = false;
|
||||
mSkipIntro = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (getSize() != 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 (getSize() == SIZE_SMALL) {
|
||||
updateSize(SIZE_SMALL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If we don't require confirmation, show the small dialog first (until errors occur).
|
||||
if (!requiresConfirmation() && supportsSmallDialog()) {
|
||||
updateSize(SIZE_SMALL);
|
||||
} else {
|
||||
updateSize(SIZE_BIG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateSize(@DialogSize int newSize) {
|
||||
final float padding = Utils.dpToPixels(mContext, IMPLICIT_Y_PADDING);
|
||||
final float iconSmallPositionY = mDialog.getHeight() - mBiometricIcon.getHeight() - padding;
|
||||
|
||||
if (newSize == SIZE_SMALL) {
|
||||
if (!supportsSmallDialog()) {
|
||||
Log.e(TAG, "Small dialog unsupported");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
announceAccessibilityEvent();
|
||||
} 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(Utils.dpToPixels(mContext, 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;
|
||||
}
|
||||
}
|
||||
|
||||
private void setDismissesDialog(View v) {
|
||||
v.setClickable(true);
|
||||
v.setOnClickListener(v1 -> {
|
||||
if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) {
|
||||
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void animateAway(@AuthDialogCallback.DismissedReason int reason) {
|
||||
animateAway(true /* sendReason */, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the dialog away
|
||||
* @param reason one of the {@link AuthDialogCallback} codes
|
||||
*/
|
||||
private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) {
|
||||
if (!mCompletedAnimatingIn) {
|
||||
Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
|
||||
mPendingDismissDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// This is where final cleanup should occur.
|
||||
final Runnable endActionRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWindowManager.removeView(BiometricDialogView.this);
|
||||
// Set the icons / text back to normal state
|
||||
handleResetMessage();
|
||||
showTryAgainButton(false /* show */);
|
||||
updateState(STATE_IDLE);
|
||||
if (sendReason) {
|
||||
mCallback.onDismissed(reason);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
postOnAnimation(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mLayout.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(ANIMATION_DURATION_AWAY)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
mDialog.animate()
|
||||
.translationY(mAnimationTranslationOffset)
|
||||
.setDuration(ANIMATION_DURATION_AWAY)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.withEndAction(endActionRunnable)
|
||||
.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the intro animation
|
||||
*/
|
||||
private void setSkipIntro(boolean skip) {
|
||||
mSkipIntro = skip;
|
||||
}
|
||||
|
||||
private void setBundle(Bundle bundle) {
|
||||
mBundle = bundle;
|
||||
}
|
||||
|
||||
private void setRequireConfirmation(boolean requireConfirmation) {
|
||||
mRequireConfirmation = requireConfirmation;
|
||||
}
|
||||
|
||||
protected boolean requiresConfirmation() {
|
||||
return mRequireConfirmation;
|
||||
}
|
||||
|
||||
private void setUserId(int userId) {
|
||||
mUserId = userId;
|
||||
}
|
||||
|
||||
private void setOpPackageName(String opPackageName) {
|
||||
mOpPackageName = opPackageName;
|
||||
}
|
||||
|
||||
// Shows an error/help message
|
||||
protected void showTemporaryMessage(String message) {
|
||||
mHandler.removeMessages(MSG_RESET_MESSAGE);
|
||||
mErrorText.setText(message);
|
||||
mErrorText.setTextColor(mErrorColor);
|
||||
mErrorText.setContentDescription(message);
|
||||
mErrorText.setVisibility(View.VISIBLE);
|
||||
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE),
|
||||
BiometricPrompt.HIDE_DIALOG_DELAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(WindowManager wm, @Nullable Bundle savedState) {
|
||||
if (savedState != null) {
|
||||
restoreState(savedState);
|
||||
}
|
||||
wm.addView(this, getLayoutParams(mWindowToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* Force remove the window, cancelling any animation that's happening. This should only be
|
||||
* called if we want to quickly show the dialog again (e.g. on rotation). Calling this method
|
||||
* will cause the dialog to show without an animation the next time it's attached.
|
||||
*/
|
||||
@Override
|
||||
public void dismissWithoutCallback(boolean animate) {
|
||||
if (animate) {
|
||||
animateAway(false /* sendReason */, 0 /* reason */);
|
||||
} else {
|
||||
mLayout.animate().cancel();
|
||||
mDialog.animate().cancel();
|
||||
mWindowManager.removeView(BiometricDialogView.this);
|
||||
mWasForceRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismissFromSystemServer() {
|
||||
animateAway(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded() {
|
||||
announceForAccessibility(getResources().getText(getAuthenticatedAccessibilityResourceId()));
|
||||
|
||||
if (requiresConfirmation()) {
|
||||
updateState(STATE_PENDING_CONFIRMATION);
|
||||
} else {
|
||||
mHandler.postDelayed(() -> {
|
||||
animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED);
|
||||
}, getDelayAfterAuthenticatedDurationMs());
|
||||
|
||||
updateState(STATE_AUTHENTICATED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed(String message) {
|
||||
updateState(STATE_ERROR);
|
||||
showTemporaryMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transient help message (acquire) is received, dialog stays showing. Sensor stays in
|
||||
* "authenticating" state.
|
||||
* @param message
|
||||
*/
|
||||
@Override
|
||||
public void onHelp(String message) {
|
||||
updateState(STATE_ERROR);
|
||||
showTemporaryMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard error is received, dialog will be dismissed soon.
|
||||
* @param error
|
||||
*/
|
||||
@Override
|
||||
public void onError(String 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);
|
||||
}
|
||||
|
||||
updateState(STATE_ERROR);
|
||||
showTemporaryMessage(error);
|
||||
showTryAgainButton(false /* show */);
|
||||
|
||||
mHandler.postDelayed(() -> {
|
||||
animateAway(AuthDialogCallback.DISMISSED_ERROR);
|
||||
}, BiometricPrompt.HIDE_DIALOG_DELAY);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveState(Bundle bundle) {
|
||||
bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility());
|
||||
bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility());
|
||||
bundle.putBoolean(KEY_CONFIRM_ENABLED, mPositiveButton.isEnabled());
|
||||
bundle.putInt(KEY_STATE, mState);
|
||||
bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility());
|
||||
bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText());
|
||||
bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE));
|
||||
bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor());
|
||||
bundle.putInt(KEY_DIALOG_SIZE, mSize);
|
||||
}
|
||||
|
||||
public void restoreState(Bundle bundle) {
|
||||
mRestoredState = bundle;
|
||||
|
||||
// Keep in mind that this happens before onAttachedToWindow()
|
||||
mSize = bundle.getInt(KEY_DIALOG_SIZE);
|
||||
|
||||
final int tryAgainVisibility = bundle.getInt(KEY_TRY_AGAIN_VISIBILITY);
|
||||
mTryAgainButton.setVisibility(tryAgainVisibility);
|
||||
final int confirmVisibility = bundle.getInt(KEY_CONFIRM_VISIBILITY);
|
||||
mPositiveButton.setVisibility(confirmVisibility);
|
||||
final boolean confirmEnabled = bundle.getBoolean(KEY_CONFIRM_ENABLED);
|
||||
mPositiveButton.setEnabled(confirmEnabled);
|
||||
mState = bundle.getInt(KEY_STATE);
|
||||
mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
|
||||
mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
|
||||
final int errorTextVisibility = bundle.getInt(KEY_ERROR_TEXT_VISIBILITY);
|
||||
mErrorText.setVisibility(errorTextVisibility);
|
||||
if (errorTextVisibility == View.INVISIBLE || tryAgainVisibility == View.INVISIBLE
|
||||
|| confirmVisibility == View.INVISIBLE) {
|
||||
announceAccessibilityEvent();
|
||||
}
|
||||
mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR));
|
||||
if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) {
|
||||
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE),
|
||||
BiometricPrompt.HIDE_DIALOG_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOpPackageName() {
|
||||
return mOpPackageName;
|
||||
}
|
||||
|
||||
protected void updateState(int newState) {
|
||||
if (newState == STATE_PENDING_CONFIRMATION) {
|
||||
mHandler.removeMessages(MSG_RESET_MESSAGE);
|
||||
mErrorText.setTextColor(mTextColor);
|
||||
mErrorText.setText(R.string.biometric_dialog_tap_confirm);
|
||||
mErrorText.setContentDescription(
|
||||
getResources().getString(R.string.biometric_dialog_tap_confirm));
|
||||
mErrorText.setVisibility(View.VISIBLE);
|
||||
announceAccessibilityEvent();
|
||||
mPositiveButton.setVisibility(View.VISIBLE);
|
||||
mPositiveButton.setEnabled(true);
|
||||
} else if (newState == STATE_AUTHENTICATED) {
|
||||
mPositiveButton.setVisibility(View.GONE);
|
||||
mNegativeButton.setVisibility(View.GONE);
|
||||
mErrorText.setVisibility(View.INVISIBLE);
|
||||
announceAccessibilityEvent();
|
||||
}
|
||||
|
||||
if (newState == STATE_PENDING_CONFIRMATION || newState == STATE_AUTHENTICATED) {
|
||||
mNegativeButton.setText(R.string.cancel);
|
||||
mNegativeButton.setContentDescription(getResources().getString(R.string.cancel));
|
||||
}
|
||||
|
||||
updateIcon(mState, newState);
|
||||
mState = newState;
|
||||
}
|
||||
|
||||
protected @DialogSize int getSize() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
protected void showTryAgainButton(boolean show) {
|
||||
if (show && getSize() == 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 {
|
||||
if (show) {
|
||||
mTryAgainButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTryAgainButton.setVisibility(View.GONE);
|
||||
announceAccessibilityEvent();
|
||||
}
|
||||
}
|
||||
|
||||
if (show) {
|
||||
mPositiveButton.setVisibility(View.GONE);
|
||||
announceAccessibilityEvent();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onDialogAnimatedIn() {
|
||||
mCompletedAnimatingIn = true;
|
||||
|
||||
if (mPendingDismissDialog) {
|
||||
Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
|
||||
animateAway(false /* sendReason */, 0);
|
||||
mPendingDismissDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param windowToken token for the window
|
||||
* @return
|
||||
*/
|
||||
public static WindowManager.LayoutParams getLayoutParams(IBinder windowToken) {
|
||||
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
|
||||
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
|
||||
lp.setTitle("BiometricDialogView");
|
||||
lp.token = windowToken;
|
||||
return lp;
|
||||
}
|
||||
|
||||
// Every time a view becomes invisible we need to announce an accessibility event.
|
||||
// This is due to an issue in the framework, b/132298701 recommended this workaround.
|
||||
protected void announceAccessibilityEvent() {
|
||||
if (!mAccessibilityManager.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
AccessibilityEvent event = AccessibilityEvent.obtain();
|
||||
event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
|
||||
event.setContentChangeTypes(CONTENT_CHANGE_TYPE_SUBTREE);
|
||||
mDialog.sendAccessibilityEventUnchecked(event);
|
||||
mDialog.notifySubtreeAccessibilityStateChanged(mDialog, mDialog,
|
||||
CONTENT_CHANGE_TYPE_SUBTREE);
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Animatable2;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.hardware.biometrics.BiometricPrompt;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* This class loads the view for the system-provided dialog. The view consists of:
|
||||
* Application Icon, Title, Subtitle, Description, Biometric Icon, Error/Help message area,
|
||||
* and positive/negative buttons.
|
||||
*/
|
||||
public class FaceDialogView extends BiometricDialogView {
|
||||
|
||||
private static final String TAG = "BiometricPrompt/FaceDialogView";
|
||||
|
||||
private static final String KEY_DIALOG_ANIMATED_IN = "key_dialog_animated_in";
|
||||
|
||||
private static final int HIDE_DIALOG_DELAY = 500; // ms
|
||||
|
||||
private IconController mIconController;
|
||||
private boolean mDialogAnimatedIn;
|
||||
|
||||
/**
|
||||
* Class that handles the biometric icon animations.
|
||||
*/
|
||||
private final class IconController extends Animatable2.AnimationCallback {
|
||||
|
||||
private boolean mLastPulseDirection; // false = dark to light, true = light to dark
|
||||
|
||||
int mState;
|
||||
|
||||
IconController() {
|
||||
mState = STATE_IDLE;
|
||||
}
|
||||
|
||||
public void animateOnce(int iconRes) {
|
||||
animateIcon(iconRes, false);
|
||||
}
|
||||
|
||||
public void showStatic(int iconRes) {
|
||||
mBiometricIcon.setImageDrawable(mContext.getDrawable(iconRes));
|
||||
}
|
||||
|
||||
public void startPulsing() {
|
||||
mLastPulseDirection = false;
|
||||
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true);
|
||||
}
|
||||
|
||||
public void showIcon(int iconRes) {
|
||||
final Drawable drawable = mContext.getDrawable(iconRes);
|
||||
mBiometricIcon.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
private void animateIcon(int iconRes, boolean repeat) {
|
||||
final AnimatedVectorDrawable icon =
|
||||
(AnimatedVectorDrawable) mContext.getDrawable(iconRes);
|
||||
mBiometricIcon.setImageDrawable(icon);
|
||||
icon.forceAnimationOnUI();
|
||||
if (repeat) {
|
||||
icon.registerAnimationCallback(this);
|
||||
}
|
||||
icon.start();
|
||||
}
|
||||
|
||||
private void pulseInNextDirection() {
|
||||
int iconRes = mLastPulseDirection ? R.drawable.face_dialog_pulse_dark_to_light
|
||||
: R.drawable.face_dialog_pulse_light_to_dark;
|
||||
animateIcon(iconRes, true /* repeat */);
|
||||
mLastPulseDirection = !mLastPulseDirection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Drawable drawable) {
|
||||
super.onAnimationEnd(drawable);
|
||||
|
||||
if (mState == STATE_AUTHENTICATING) {
|
||||
// Still authenticating, pulse the icon
|
||||
pulseInNextDirection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable mErrorToIdleAnimationRunnable = () -> {
|
||||
updateState(STATE_IDLE);
|
||||
mErrorText.setVisibility(View.INVISIBLE);
|
||||
announceAccessibilityEvent();
|
||||
};
|
||||
|
||||
protected FaceDialogView(Context context, AuthDialogCallback callback, Injector injector) {
|
||||
super(context, callback, injector);
|
||||
mIconController = new IconController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveState(Bundle bundle) {
|
||||
super.onSaveState(bundle);
|
||||
bundle.putBoolean(KEY_DIALOG_ANIMATED_IN, mDialogAnimatedIn);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void handleResetMessage() {
|
||||
mErrorText.setTextColor(mTextColor);
|
||||
mErrorText.setVisibility(View.INVISIBLE);
|
||||
announceAccessibilityEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(Bundle bundle) {
|
||||
super.restoreState(bundle);
|
||||
mDialogAnimatedIn = bundle.getBoolean(KEY_DIALOG_ANIMATED_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed(String message) {
|
||||
super.onAuthenticationFailed(message);
|
||||
showTryAgainButton(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getHintStringResourceId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getAuthenticatedAccessibilityResourceId() {
|
||||
if (mRequireConfirmation) {
|
||||
return com.android.internal.R.string.face_authenticated_confirmation_required;
|
||||
} else {
|
||||
return com.android.internal.R.string.face_authenticated_no_confirmation_required;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getIconDescriptionResourceId() {
|
||||
return R.string.accessibility_face_dialog_face_icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateIcon(int oldState, int newState) {
|
||||
mIconController.mState = newState;
|
||||
|
||||
if (newState == STATE_AUTHENTICATING) {
|
||||
mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
|
||||
if (mDialogAnimatedIn) {
|
||||
mIconController.startPulsing();
|
||||
} else {
|
||||
mIconController.showIcon(R.drawable.face_dialog_pulse_dark_to_light);
|
||||
}
|
||||
mBiometricIcon.setContentDescription(mContext.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticating));
|
||||
} else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
|
||||
mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
|
||||
mBiometricIcon.setContentDescription(mContext.getString(
|
||||
R.string.biometric_dialog_face_icon_description_confirmed));
|
||||
} else if (oldState == STATE_ERROR && newState == STATE_IDLE) {
|
||||
mIconController.animateOnce(R.drawable.face_dialog_error_to_idle);
|
||||
mBiometricIcon.setContentDescription(mContext.getString(
|
||||
R.string.biometric_dialog_face_icon_description_idle));
|
||||
} else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) {
|
||||
mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
|
||||
mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
|
||||
mBiometricIcon.setContentDescription(mContext.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticated));
|
||||
} else if (newState == STATE_ERROR) {
|
||||
// It's easier to only check newState and gate showing the animation on the
|
||||
// mErrorToIdleAnimationRunnable as a proxy, than add a ton of extra state. For example,
|
||||
// we may go from error -> error due to configuration change which is valid and we
|
||||
// should show the animation, or we can go from error -> error by receiving repeated
|
||||
// acquire messages in which case we do not want to repeatedly start the animation.
|
||||
if (!mHandler.hasCallbacks(mErrorToIdleAnimationRunnable)) {
|
||||
mIconController.animateOnce(R.drawable.face_dialog_dark_to_error);
|
||||
mHandler.postDelayed(mErrorToIdleAnimationRunnable,
|
||||
BiometricPrompt.HIDE_DIALOG_DELAY);
|
||||
}
|
||||
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
|
||||
mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
|
||||
mBiometricIcon.setContentDescription(mContext.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticated));
|
||||
} else if (newState == STATE_PENDING_CONFIRMATION) {
|
||||
mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
|
||||
mIconController.animateOnce(R.drawable.face_dialog_wink_from_dark);
|
||||
mBiometricIcon.setContentDescription(mContext.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticated));
|
||||
} else if (newState == STATE_IDLE) {
|
||||
mIconController.showStatic(R.drawable.face_dialog_idle_static);
|
||||
mBiometricIcon.setContentDescription(mContext.getString(
|
||||
R.string.biometric_dialog_face_icon_description_idle));
|
||||
} else {
|
||||
Log.w(TAG, "Unknown animation from " + oldState + " -> " + newState);
|
||||
}
|
||||
|
||||
// Note that this must be after the newState == STATE_ERROR check above since this affects
|
||||
// the logic.
|
||||
if (oldState == STATE_ERROR && newState == STATE_ERROR) {
|
||||
// Keep the error icon and text around for a while longer if we keep receiving
|
||||
// STATE_ERROR
|
||||
mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
|
||||
mHandler.postDelayed(mErrorToIdleAnimationRunnable, BiometricPrompt.HIDE_DIALOG_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsSmallDialog() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogAnimatedIn() {
|
||||
super.onDialogAnimatedIn();
|
||||
mDialogAnimatedIn = true;
|
||||
mIconController.startPulsing();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDelayAfterAuthenticatedDurationMs() {
|
||||
return HIDE_DIALOG_DELAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldGrayAreaDismissDialog() {
|
||||
if (getSize() == SIZE_SMALL) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* This class loads the view for the system-provided dialog. The view consists of:
|
||||
* Application Icon, Title, Subtitle, Description, Biometric Icon, Error/Help message area,
|
||||
* and positive/negative buttons.
|
||||
*/
|
||||
public class FingerprintDialogView extends BiometricDialogView {
|
||||
|
||||
private static final String TAG = "BiometricPrompt/FingerprintDialogView";
|
||||
|
||||
protected FingerprintDialogView(Context context, AuthDialogCallback callback,
|
||||
Injector injector) {
|
||||
super(context, callback, injector);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleResetMessage() {
|
||||
updateState(STATE_AUTHENTICATING);
|
||||
mErrorText.setText(getHintStringResourceId());
|
||||
mErrorText.setTextColor(mTextColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getHintStringResourceId() {
|
||||
return R.string.fingerprint_dialog_touch_sensor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getAuthenticatedAccessibilityResourceId() {
|
||||
return com.android.internal.R.string.fingerprint_authenticated;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getIconDescriptionResourceId() {
|
||||
return R.string.accessibility_fingerprint_dialog_fingerprint_icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateIcon(int lastState, int newState) {
|
||||
final Drawable icon = getAnimationForTransition(lastState, newState);
|
||||
if (icon == null) {
|
||||
Log.e(TAG, "Animation not found, " + lastState + " -> " + newState);
|
||||
return;
|
||||
}
|
||||
|
||||
final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
|
||||
? (AnimatedVectorDrawable) icon
|
||||
: null;
|
||||
|
||||
mBiometricIcon.setImageDrawable(icon);
|
||||
|
||||
if (animation != null && shouldAnimateForTransition(lastState, newState)) {
|
||||
animation.forceAnimationOnUI();
|
||||
animation.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsSmallDialog() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean shouldAnimateForTransition(int oldState, int newState) {
|
||||
if (newState == STATE_ERROR) {
|
||||
return true;
|
||||
} else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
|
||||
return true;
|
||||
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
|
||||
// TODO(b/77328470): add animation when fingerprint is authenticated
|
||||
return false;
|
||||
} else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) {
|
||||
// TODO(b/77328470): add animation when fingerprint is authenticated
|
||||
return false;
|
||||
} else if (newState == STATE_AUTHENTICATING) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDelayAfterAuthenticatedDurationMs() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldGrayAreaDismissDialog() {
|
||||
// Fingerprint dialog always dismisses when region outside the dialog is tapped
|
||||
return true;
|
||||
}
|
||||
|
||||
protected Drawable getAnimationForTransition(int oldState, int newState) {
|
||||
int iconRes;
|
||||
if (newState == STATE_ERROR) {
|
||||
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
|
||||
} else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
|
||||
iconRes = R.drawable.fingerprint_dialog_error_to_fp;
|
||||
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
|
||||
// TODO(b/77328470): add animation when fingerprint is authenticated
|
||||
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
|
||||
} else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) {
|
||||
// TODO(b/77328470): add animation when fingerprint is authenticated
|
||||
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
|
||||
} else if (newState == STATE_AUTHENTICATING) {
|
||||
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return mContext.getDrawable(iconRes);
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNotSame;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.Context;
|
||||
import android.hardware.biometrics.BiometricPrompt;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserManager;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableContext;
|
||||
import android.testing.TestableLooper.RunWithLooper;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.keyguard.WakefulnessLifecycle;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@RunWithLooper
|
||||
@SmallTest
|
||||
public class BiometricDialogViewTest extends SysuiTestCase {
|
||||
|
||||
FaceDialogView mFaceDialogView;
|
||||
|
||||
private static final String TITLE = "Title";
|
||||
private static final String SUBTITLE = "Subtitle";
|
||||
private static final String DESCRIPTION = "Description";
|
||||
private static final String NEGATIVE_BUTTON = "Negative Button";
|
||||
|
||||
private static final String TEST_HELP = "Help";
|
||||
|
||||
TestableContext mTestableContext;
|
||||
@Mock
|
||||
private AuthDialogCallback mCallback;
|
||||
@Mock
|
||||
private UserManager mUserManager;
|
||||
@Mock
|
||||
private DevicePolicyManager mDpm;
|
||||
|
||||
private static class Injector extends BiometricDialogView.Injector {
|
||||
@Override
|
||||
public WakefulnessLifecycle getWakefulnessLifecycle() {
|
||||
final WakefulnessLifecycle lifecycle = new WakefulnessLifecycle();
|
||||
lifecycle.dispatchFinishedWakingUp();
|
||||
return lifecycle;
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mTestableContext = spy(mContext);
|
||||
mTestableContext.addMockSystemService(UserManager.class, mUserManager);
|
||||
mTestableContext.addMockSystemService(DevicePolicyManager.class, mDpm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContentStates_confirmationRequired_authenticated() {
|
||||
mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
|
||||
true /* requireConfirmation */);
|
||||
mFaceDialogView.onAttachedToWindow();
|
||||
|
||||
// When starting authentication
|
||||
assertEquals(View.VISIBLE, mFaceDialogView.mTitleText.getVisibility());
|
||||
assertEquals(View.VISIBLE, mFaceDialogView.mSubtitleText.getVisibility());
|
||||
assertEquals(View.VISIBLE, mFaceDialogView.mDescriptionText.getVisibility());
|
||||
assertEquals(View.INVISIBLE, mFaceDialogView.mErrorText.getVisibility());
|
||||
assertEquals(View.VISIBLE, mFaceDialogView.mPositiveButton.getVisibility());
|
||||
assertEquals(View.VISIBLE, mFaceDialogView.mNegativeButton.getVisibility());
|
||||
assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility());
|
||||
|
||||
// Contents are as expected
|
||||
assertTrue(TITLE.contentEquals(mFaceDialogView.mTitleText.getText()));
|
||||
assertTrue(SUBTITLE.contentEquals(mFaceDialogView.mSubtitleText.getText()));
|
||||
assertTrue(DESCRIPTION.contentEquals(mFaceDialogView.mDescriptionText.getText()));
|
||||
assertTrue(mFaceDialogView.mPositiveButton.getText().toString()
|
||||
.contentEquals(mContext.getString(R.string.biometric_dialog_confirm)));
|
||||
assertTrue(NEGATIVE_BUTTON.contentEquals(mFaceDialogView.mNegativeButton.getText()));
|
||||
assertTrue(mFaceDialogView.mTryAgainButton.getText().toString()
|
||||
.contentEquals(mContext.getString(R.string.biometric_dialog_try_again)));
|
||||
|
||||
// When help message is received
|
||||
mFaceDialogView.onHelp(TEST_HELP);
|
||||
assertEquals(mFaceDialogView.mErrorText.getVisibility(), View.VISIBLE);
|
||||
assertTrue(TEST_HELP.contentEquals(mFaceDialogView.mErrorText.getText()));
|
||||
|
||||
// When authenticated, confirm button comes out
|
||||
mFaceDialogView.onAuthenticationSucceeded();
|
||||
assertEquals(View.VISIBLE, mFaceDialogView.mPositiveButton.getVisibility());
|
||||
assertEquals(true, mFaceDialogView.mPositiveButton.isEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContentStates_confirmationNotRequired_authenticated() {
|
||||
mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
|
||||
false /* requireConfirmation */);
|
||||
mFaceDialogView.onAttachedToWindow();
|
||||
mFaceDialogView.updateSize(FaceDialogView.SIZE_SMALL);
|
||||
|
||||
assertEquals(View.INVISIBLE, mFaceDialogView.mTitleText.getVisibility());
|
||||
assertNotSame(View.VISIBLE, mFaceDialogView.mSubtitleText.getVisibility());
|
||||
assertNotSame(View.VISIBLE, mFaceDialogView.mDescriptionText.getVisibility());
|
||||
assertEquals(View.INVISIBLE, mFaceDialogView.mErrorText.getVisibility());
|
||||
assertEquals(View.GONE, mFaceDialogView.mPositiveButton.getVisibility());
|
||||
assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility());
|
||||
assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContentStates_confirmationNotRequired_help() {
|
||||
mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
|
||||
false /* requireConfirmation */);
|
||||
mFaceDialogView.onAttachedToWindow();
|
||||
|
||||
mFaceDialogView.onHelp(TEST_HELP);
|
||||
assertEquals(mFaceDialogView.mErrorText.getVisibility(), View.VISIBLE);
|
||||
assertTrue(TEST_HELP.contentEquals(mFaceDialogView.mErrorText.getText()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBack_sendsUserCanceled() {
|
||||
// TODO: Need robolectric framework to wait for handler to complete
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScreenOff_sendsUserCanceled() {
|
||||
// TODO: Need robolectric framework to wait for handler to complete
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreState_contentStatesCorrect() {
|
||||
mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
|
||||
false /* requireConfirmation */);
|
||||
mFaceDialogView.onAttachedToWindow();
|
||||
mFaceDialogView.onAuthenticationFailed(TEST_HELP);
|
||||
|
||||
final Bundle bundle = new Bundle();
|
||||
mFaceDialogView.onSaveState(bundle);
|
||||
|
||||
mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
|
||||
false /* requireConfirmation */);
|
||||
mFaceDialogView.restoreState(bundle);
|
||||
mFaceDialogView.onAttachedToWindow();
|
||||
|
||||
assertEquals(View.VISIBLE, mFaceDialogView.mTryAgainButton.getVisibility());
|
||||
}
|
||||
|
||||
private FaceDialogView buildFaceDialogView(Context context, AuthDialogCallback callback,
|
||||
boolean requireConfirmation) {
|
||||
return (FaceDialogView) new BiometricDialogView.Builder(context)
|
||||
.setCallback(callback)
|
||||
.setBiometricPromptBundle(createTestDialogBundle())
|
||||
.setRequireConfirmation(requireConfirmation)
|
||||
.setUserId(0)
|
||||
.setOpPackageName("test_package")
|
||||
.build(BiometricDialogView.Builder.TYPE_FACE, new Injector());
|
||||
}
|
||||
|
||||
private Bundle createTestDialogBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
bundle.putCharSequence(BiometricPrompt.KEY_TITLE, TITLE);
|
||||
bundle.putCharSequence(BiometricPrompt.KEY_SUBTITLE, SUBTITLE);
|
||||
bundle.putCharSequence(BiometricPrompt.KEY_DESCRIPTION, DESCRIPTION);
|
||||
bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, NEGATIVE_BUTTON);
|
||||
|
||||
// RequireConfirmation is a hint to BiometricService. This can be forced to be required
|
||||
// by user settings, and should be tested in BiometricService.
|
||||
bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user