12/n: Add LockPatternView for setDeviceCredentialAllowed(true)
Includes lock icon, title, subtitle, description, lock pattern view.
Corner radius and padding animates nicely from !=0 --> 0.
Support for password/pin will come in a subsequent CL.
Unit tests for AuthCredentialView will be added when
password/pin are implemented.
Support for persisting across configuration changes
and landscape view will also be added in a subsequent
change.
Test: BiometricPromptDemo with the following:
1) Confirm pattern, callback received
2) Rejected, error string shown
3) Lockout (5 attempts), countdown string shown,
pattern view disabled until countdown is over
4) Cancel pattern auth, callback received
Test: atest BiometricServiceTest
Test: atest com.android.systemui.biometrics
Change-Id: Idc01e33be0074a6c8a43f60b172a4391bfbe5e8a
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 4c4a745..18d2747 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -147,8 +147,12 @@
return BiometricPrompt.HIDE_DIALOG_DELAY;
}
- public int getAnimationDuration() {
- return AuthDialog.ANIMATE_DURATION_MS;
+ public int getMediumToLargeAnimationDurationMs() {
+ return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
+ }
+
+ public int getAnimateCredentialStartDelayMs() {
+ return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS;
}
}
@@ -159,7 +163,7 @@
private final int mTextColorHint;
private AuthPanelController mPanelController;
- private Bundle mBundle;
+ private Bundle mBiometricPromptBundle;
private boolean mRequireConfirmation;
private int mUserId;
@AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN;
@@ -265,7 +269,7 @@
}
public void setBiometricPromptBundle(Bundle bundle) {
- mBundle = bundle;
+ mBiometricPromptBundle = bundle;
}
public void setCallback(Callback callback) {
@@ -300,7 +304,7 @@
final int newHeight = mIconView.getHeight() + 2 * (int) iconPadding;
mPanelController.updateForContentDimensions(mMediumWidth, newHeight,
- false /* animate */);
+ 0 /* animateDurationMs */);
mSize = newSize;
} else if (mSize == AuthDialog.SIZE_SMALL && newSize == AuthDialog.SIZE_MEDIUM) {
@@ -318,7 +322,6 @@
// Animate the text
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1);
- opacityAnimator.setDuration(mInjector.getAnimationDuration());
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
mTitleView.setAlpha(opacity);
@@ -336,7 +339,7 @@
// Choreograph together
final AnimatorSet as = new AnimatorSet();
- as.setDuration(mInjector.getAnimationDuration());
+ as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
as.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -367,41 +370,51 @@
as.start();
// Animate the panel
mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
- true /* animate */);
+ AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
} else if (newSize == AuthDialog.SIZE_MEDIUM) {
mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
- false /* animate */);
+ 0 /* animateDurationMs */);
mSize = newSize;
} else if (newSize == AuthDialog.SIZE_LARGE) {
- final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
- opacityAnimator.setDuration(mInjector.getAnimationDuration());
- opacityAnimator.addUpdateListener((animation) -> {
- final float opacity = (float) animation.getAnimatedValue();
- mTitleView.setAlpha(opacity);
- mSubtitleView.setAlpha(opacity);
- mDescriptionView.setAlpha(opacity);
- mIconView.setAlpha(opacity);
- mIndicatorView.setAlpha(opacity);
- mNegativeButton.setAlpha(opacity);
+ final float translationY = getResources().getDimension(
+ R.dimen.biometric_dialog_medium_to_large_translation_offset);
+ final AuthBiometricView biometricView = this;
+
+ // Translate at full duration
+ final ValueAnimator translationAnimator = ValueAnimator.ofFloat(
+ biometricView.getY(), biometricView.getY() - translationY);
+ translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs());
+ translationAnimator.addUpdateListener((animation) -> {
+ final float translation = (float) animation.getAnimatedValue();
+ biometricView.setTranslationY(translation);
});
- opacityAnimator.addListener(new AnimatorListenerAdapter() {
+ translationAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
- AuthBiometricView view = AuthBiometricView.this;
- if (view.getParent() != null) {
- ((ViewGroup) view.getParent()).removeView(view);
+ if (biometricView.getParent() != null) {
+ ((ViewGroup) biometricView.getParent()).removeView(biometricView);
}
mSize = newSize;
}
});
+ // Opacity to 0 in half duration
+ final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
+ opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2);
+ opacityAnimator.addUpdateListener((animation) -> {
+ final float opacity = (float) animation.getAnimatedValue();
+ biometricView.setAlpha(opacity);
+ });
+
mPanelController.setUseFullScreen(true);
mPanelController.updateForContentDimensions(
mPanelController.getContainerWidth(),
mPanelController.getContainerHeight(),
- true /* animate */);
- opacityAnimator.start();
+ mInjector.getMediumToLargeAnimationDurationMs());
+ AnimatorSet as = new AnimatorSet();
+ as.play(translationAnimator).with(opacityAnimator);
+ as.start();
} else {
Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
}
@@ -572,7 +585,10 @@
} else {
if (isDeviceCredentialAllowed()) {
updateSize(AuthDialog.SIZE_LARGE);
- mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
+ mHandler.postDelayed(() -> {
+ mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
+ }, mInjector.getAnimateCredentialStartDelayMs());
+
} else {
mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
}
@@ -603,7 +619,7 @@
*/
@VisibleForTesting
void onAttachedToWindowInternal() {
- setText(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE));
+ setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE));
final String negativeText;
if (isDeviceCredentialAllowed()) {
@@ -628,12 +644,14 @@
}
} else {
- negativeText = mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT);
+ negativeText = mBiometricPromptBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT);
}
setText(mNegativeButton, negativeText);
- setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE));
- setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
+ setTextOrHide(mSubtitleView,
+ mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE));
+ setTextOrHide(mDescriptionView,
+ mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
if (mSavedState == null) {
updateState(STATE_AUTHENTICATING_ANIMATING_IN);
@@ -730,6 +748,6 @@
}
private boolean isDeviceCredentialAllowed() {
- return mBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
+ return mBiometricPromptBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 6781fd2..db23e62 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -37,6 +37,7 @@
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -78,12 +79,14 @@
private final AuthPanelController mPanelController;
private final Interpolator mLinearOutSlowIn;
@VisibleForTesting final BiometricCallback mBiometricCallback;
+ private final CredentialCallback mCredentialCallback;
- private final ViewGroup mContainerView;
+ @VisibleForTesting final FrameLayout mFrameLayout;
private final AuthBiometricView mBiometricView;
+ @VisibleForTesting AuthCredentialView mCredentialView;
private final ImageView mBackgroundView;
- private final ScrollView mScrollView;
+ private final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
@@ -156,7 +159,7 @@
public void onAction(int action) {
switch (action) {
case AuthBiometricView.Callback.ACTION_AUTHENTICATED:
- animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED);
+ animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
break;
case AuthBiometricView.Callback.ACTION_USER_CANCELED:
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
@@ -171,7 +174,8 @@
animateAway(AuthDialogCallback.DISMISSED_ERROR);
break;
case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
- Log.v(TAG, "ACTION_USE_DEVICE_CREDENTIAL");
+ mConfig.mCallback.onDeviceCredentialPressed();
+ showCredentialView();
break;
default:
Log.e(TAG, "Unhandled action: " + action);
@@ -179,6 +183,13 @@
}
}
+ final class CredentialCallback implements AuthCredentialView.Callback {
+ @Override
+ public void onCredentialMatched() {
+ animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
+ }
+ }
+
@VisibleForTesting
AuthContainerView(Config config) {
super(config.mContext);
@@ -191,12 +202,13 @@
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
mBiometricCallback = new BiometricCallback();
+ mCredentialCallback = new CredentialCallback();
final LayoutInflater factory = LayoutInflater.from(mContext);
- mContainerView = (ViewGroup) factory.inflate(
+ mFrameLayout = (FrameLayout) factory.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
- mPanelView = mContainerView.findViewById(R.id.panel);
+ mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
// TODO: Update with new controllers if multi-modal authentication can occur simultaneously
@@ -210,11 +222,11 @@
Log.e(TAG, "Unsupported modality mask: " + config.mModalityMask);
mBiometricView = null;
mBackgroundView = null;
- mScrollView = null;
+ mBiometricScrollView = null;
return;
}
- mBackgroundView = mContainerView.findViewById(R.id.background);
+ mBackgroundView = mFrameLayout.findViewById(R.id.background);
UserManager userManager = mContext.getSystemService(UserManager.class);
DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
@@ -234,9 +246,9 @@
mBiometricView.setBackgroundView(mBackgroundView);
mBiometricView.setUserId(mConfig.mUserId);
- mScrollView = mContainerView.findViewById(R.id.scrollview);
- mScrollView.addView(mBiometricView);
- addView(mContainerView);
+ mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
+ mBiometricScrollView.addView(mBiometricView);
+ addView(mFrameLayout);
setOnKeyListener((v, keyCode, event) -> {
if (keyCode != KeyEvent.KEYCODE_BACK) {
@@ -252,6 +264,16 @@
requestFocus();
}
+ private void showCredentialView() {
+ final LayoutInflater factory = LayoutInflater.from(mContext);
+ mCredentialView = (AuthCredentialView) factory.inflate(
+ R.layout.auth_credential_view, null, false);
+ mCredentialView.setUser(mConfig.mUserId);
+ mCredentialView.setCallback(mCredentialCallback);
+ mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
+ mFrameLayout.addView(mCredentialView);
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -270,7 +292,7 @@
// The background panel and content are different views since we need to be able to
// animate them separately in other places.
mPanelView.setY(mTranslationY);
- mScrollView.setY(mTranslationY);
+ mBiometricScrollView.setY(mTranslationY);
setAlpha(0f);
postOnAnimation(() -> {
@@ -281,7 +303,7 @@
.withLayer()
.withEndAction(this::onDialogAnimatedIn)
.start();
- mScrollView.animate()
+ mBiometricScrollView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setInterpolator(mLinearOutSlowIn)
@@ -396,12 +418,20 @@
.withLayer()
.withEndAction(endActionRunnable)
.start();
- mScrollView.animate()
+ mBiometricScrollView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
+ if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
+ mCredentialView.animate()
+ .translationY(mTranslationY)
+ .setDuration(ANIMATION_DURATION_AWAY_MS)
+ .setInterpolator(mLinearOutSlowIn)
+ .withLayer()
+ .start();
+ }
animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION_AWAY_MS)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index d10a3fe..eb87834 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -105,6 +105,15 @@
}
@Override
+ public void onDeviceCredentialPressed() {
+ try {
+ mReceiver.onDeviceCredentialPressed();
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException when handling credential button", e);
+ }
+ }
+
+ @Override
public void onDismissed(@DismissedReason int reason) {
switch (reason) {
case AuthDialogCallback.DISMISSED_USER_CANCELED:
@@ -116,11 +125,12 @@
break;
case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE:
- sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
+ sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
break;
- case AuthDialogCallback.DISMISSED_AUTHENTICATED:
- sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
+ case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED:
+ sendResultAndCleanUp(
+ BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
break;
case AuthDialogCallback.DISMISSED_ERROR:
@@ -131,6 +141,10 @@
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
break;
+ case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED:
+ sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED);
+ break;
+
default:
Log.e(TAG, "Unhandled reason: " + reason);
break;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
new file mode 100644
index 0000000..dabe84b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
@@ -0,0 +1,261 @@
+/*
+ * 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 android.content.Context;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.widget.LockPatternChecker;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockPatternView;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Shows Pin, Pattern, or Password for
+ * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)}
+ */
+public class AuthCredentialView extends LinearLayout {
+
+ private static final int ERROR_DURATION_MS = 3000;
+
+ private final AccessibilityManager mAccessibilityManager;
+ private final LockPatternUtils mLockPatternUtils;
+ private final Handler mHandler;
+
+ private LockPatternView mLockPatternView;
+ private int mUserId;
+ private AsyncTask<?, ?, ?> mPendingLockCheck;
+ private Callback mCallback;
+ private ErrorTimer mErrorTimer;
+ private Bundle mBiometricPromptBundle;
+
+ private TextView mTitleView;
+ private TextView mSubtitleView;
+ private TextView mDescriptionView;
+ private TextView mErrorView;
+
+ interface Callback {
+ void onCredentialMatched();
+ }
+
+ private static class ErrorTimer extends CountDownTimer {
+ private final TextView mErrorView;
+ private final Context mContext;
+
+ /**
+ * @param millisInFuture The number of millis in the future from the call
+ * to {@link #start()} until the countdown is done and {@link
+ * #onFinish()}
+ * is called.
+ * @param countDownInterval The interval along the way to receive
+ * {@link #onTick(long)} callbacks.
+ */
+ public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
+ TextView errorView) {
+ super(millisInFuture, countDownInterval);
+ mErrorView = errorView;
+ mContext = context;
+ }
+
+ @Override
+ public void onTick(long millisUntilFinished) {
+ final int secondsCountdown = (int) (millisUntilFinished / 1000);
+ mErrorView.setText(mContext.getString(
+ R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
+ }
+
+ @Override
+ public void onFinish() {
+ mErrorView.setText("");
+ }
+ }
+
+ private class UnlockPatternListener implements LockPatternView.OnPatternListener {
+
+ @Override
+ public void onPatternStart() {
+
+ }
+
+ @Override
+ public void onPatternCleared() {
+
+ }
+
+ @Override
+ public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
+
+ }
+
+ @Override
+ public void onPatternDetected(List<LockPatternView.Cell> pattern) {
+ if (mPendingLockCheck != null) {
+ mPendingLockCheck.cancel(false);
+ }
+
+ mLockPatternView.setEnabled(false);
+
+ if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
+ // Pattern size is less than the minimum, do not count it as a failed attempt.
+ onPatternChecked(false /* matched */, 0 /* timeoutMs */);
+ return;
+ }
+
+ mPendingLockCheck = LockPatternChecker.checkPattern(
+ mLockPatternUtils,
+ pattern,
+ mUserId,
+ this::onPatternChecked);
+ }
+
+ private void onPatternChecked(boolean matched, int timeoutMs) {
+ mLockPatternView.setEnabled(true);
+
+ if (matched) {
+ mClearErrorRunnable.run();
+ mCallback.onCredentialMatched();
+ } else {
+ if (timeoutMs > 0) {
+ mHandler.removeCallbacks(mClearErrorRunnable);
+ mLockPatternView.setEnabled(false);
+ long deadline = mLockPatternUtils.setLockoutAttemptDeadline(mUserId, timeoutMs);
+ mErrorTimer = new ErrorTimer(mContext,
+ deadline - SystemClock.elapsedRealtime(),
+ LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
+ mErrorView) {
+ @Override
+ public void onFinish() {
+ mClearErrorRunnable.run();
+ mLockPatternView.setEnabled(true);
+ }
+ };
+ mErrorTimer.start();
+ } else {
+ showError(getResources().getString(R.string.biometric_dialog_wrong_pattern));
+ }
+ }
+ }
+ }
+
+ private final Runnable mClearErrorRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mErrorView.setText("");
+ }
+ };
+
+ public AuthCredentialView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mHandler = new Handler(Looper.getMainLooper());
+ mLockPatternUtils = new LockPatternUtils(mContext);
+ mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
+ }
+
+ private void showError(String error) {
+ mHandler.removeCallbacks(mClearErrorRunnable);
+ mErrorView.setText(error);
+ mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
+ }
+
+ private void setTextOrHide(TextView view, String string) {
+ if (TextUtils.isEmpty(string)) {
+ view.setVisibility(View.GONE);
+ } else {
+ view.setText(string);
+ }
+
+ Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
+ }
+
+ private void setText(TextView view, String string) {
+ view.setText(string);
+ }
+
+ void setUser(int user) {
+ mUserId = user;
+ }
+
+ void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ void setBiometricPromptBundle(Bundle bundle) {
+ mBiometricPromptBundle = bundle;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE));
+ setTextOrHide(mSubtitleView,
+ mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE));
+ setTextOrHide(mDescriptionView,
+ mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
+
+ setTranslationY(getResources()
+ .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
+ setAlpha(0);
+
+ postOnAnimation(() -> {
+ animate().translationY(0)
+ .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
+ .alpha(1.f)
+ .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+ .withLayer()
+ .start();
+ });
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mErrorTimer != null) {
+ mErrorTimer.cancel();
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTitleView = findViewById(R.id.title);
+ mSubtitleView = findViewById(R.id.subtitle);
+ mDescriptionView = findViewById(R.id.description);
+ mErrorView = findViewById(R.id.error);
+ mLockPatternView = findViewById(R.id.lockPattern);
+ mLockPatternView.setOnPatternListener(new UnlockPatternListener());
+ mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(mUserId));
+ mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index edb2953..29b84bb 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -40,17 +40,38 @@
String KEY_BIOMETRIC_DIALOG_SIZE = "size";
int SIZE_UNKNOWN = 0;
+ /**
+ * Minimal UI, showing only biometric icon.
+ */
int SIZE_SMALL = 1;
+ /**
+ * Normal-sized biometric UI, showing title, icon, buttons, etc.
+ */
int SIZE_MEDIUM = 2;
+ /**
+ * Full-screen credential UI.
+ */
int SIZE_LARGE = 3;
@Retention(RetentionPolicy.SOURCE)
@IntDef({SIZE_UNKNOWN, SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE})
@interface DialogSize {}
/**
- * Animation duration, e.g. small to medium dialog, icon translation, etc.
+ * Animation duration, from small to medium dialog, including back panel, icon translation, etc
*/
- int ANIMATE_DURATION_MS = 150;
+ int ANIMATE_SMALL_TO_MEDIUM_DURATION_MS = 150;
+ /**
+ * Animation duration from medium to large dialog, including biometric fade out, back panel, etc
+ */
+ int ANIMATE_MEDIUM_TO_LARGE_DURATION_MS = 450;
+ /**
+ * Delay before notifying {@link AuthCredentialView} to start animating in.
+ */
+ int ANIMATE_CREDENTIAL_START_DELAY_MS = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS * 2 / 3;
+ /**
+ * Animation duration when sliding in credential UI
+ */
+ int ANIMATE_CREDENTIAL_INITIAL_DURATION_MS = 150;
/**
* Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index 70752f5..12bb122 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
@@ -27,17 +27,18 @@
int DISMISSED_USER_CANCELED = 1;
int DISMISSED_BUTTON_NEGATIVE = 2;
int DISMISSED_BUTTON_POSITIVE = 3;
-
- int DISMISSED_AUTHENTICATED = 4;
+ int DISMISSED_BIOMETRIC_AUTHENTICATED = 4;
int DISMISSED_ERROR = 5;
int DISMISSED_BY_SYSTEM_SERVER = 6;
+ int DISMISSED_CREDENTIAL_AUTHENTICATED = 7;
@IntDef({DISMISSED_USER_CANCELED,
DISMISSED_BUTTON_NEGATIVE,
DISMISSED_BUTTON_POSITIVE,
- DISMISSED_AUTHENTICATED,
+ DISMISSED_BIOMETRIC_AUTHENTICATED,
DISMISSED_ERROR,
- DISMISSED_BY_SYSTEM_SERVER})
+ DISMISSED_BY_SYSTEM_SERVER,
+ DISMISSED_CREDENTIAL_AUTHENTICATED})
@interface DismissedReason {}
/**
@@ -50,4 +51,9 @@
* Invoked when the "try again" button is clicked
*/
void onTryAgainPressed();
+
+ /**
+ * Invoked when the "use password" button is clicked
+ */
+ void onDeviceCredentialPressed();
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index 60c9ca4..27040f0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -32,12 +32,10 @@
public class AuthPanelController extends ViewOutlineProvider {
private static final String TAG = "BiometricPrompt/AuthPanelController";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private final Context mContext;
private final View mPanelView;
- private final float mCornerRadius;
- private final int mBiometricMargin;
private boolean mUseFullScreen;
@@ -47,19 +45,24 @@
private int mContentWidth;
private int mContentHeight;
+ private float mCornerRadius;
+ private int mMargin;
+
@Override
public void getOutline(View view, Outline outline) {
final int left = (mContainerWidth - mContentWidth) / 2;
final int right = mContainerWidth - left;
- final int margin = mUseFullScreen ? 0 : mBiometricMargin;
- final float cornerRadius = mUseFullScreen ? 0 : mCornerRadius;
-
+ // If the content fits within the container, shrink the height to wrap the content.
+ // Otherwise, set the outline to be the display size minus the margin - the content within
+ // is scrollable.
final int top = mContentHeight < mContainerHeight
- ? mContainerHeight - mContentHeight - margin
- : margin;
- final int bottom = mContainerHeight - margin;
- outline.setRoundRect(left, top, right, bottom, cornerRadius);
+ ? mContainerHeight - mContentHeight - mMargin
+ : mMargin;
+
+ // TODO(b/139954942) Likely don't need to "+1" after we resolve the navbar styling.
+ final int bottom = mContainerHeight - mMargin + 1;
+ outline.setRoundRect(left, top, right, bottom, mCornerRadius);
}
public void setContainerDimensions(int containerWidth, int containerHeight) {
@@ -74,11 +77,12 @@
mUseFullScreen = fullScreen;
}
- public void updateForContentDimensions(int contentWidth, int contentHeight, boolean animate) {
+ public void updateForContentDimensions(int contentWidth, int contentHeight,
+ int animateDurationMs) {
if (DEBUG) {
Log.v(TAG, "Content Width: " + contentWidth
+ " Height: " + contentHeight
- + " Animate: " + animate);
+ + " Animate: " + animateDurationMs);
}
if (mContainerWidth == 0 || mContainerHeight == 0) {
@@ -86,7 +90,24 @@
return;
}
- if (animate) {
+ if (animateDurationMs > 0) {
+ // Animate margin
+ final int margin = mUseFullScreen ? 0 : (int) mContext.getResources()
+ .getDimension(R.dimen.biometric_dialog_border_padding);
+ ValueAnimator marginAnimator = ValueAnimator.ofInt(mMargin, margin);
+ marginAnimator.addUpdateListener((animation) -> {
+ mMargin = (int) animation.getAnimatedValue();
+ });
+
+ // Animate corners
+ final float cornerRadius = mUseFullScreen ? 0 : mContext.getResources()
+ .getDimension(R.dimen.biometric_dialog_corner_size);
+ ValueAnimator cornerAnimator = ValueAnimator.ofFloat(mCornerRadius, cornerRadius);
+ cornerAnimator.addUpdateListener((animation) -> {
+ mCornerRadius = (float) animation.getAnimatedValue();
+ });
+
+ // Animate height
ValueAnimator heightAnimator = ValueAnimator.ofInt(mContentHeight, contentHeight);
heightAnimator.addUpdateListener((animation) -> {
mContentHeight = (int) animation.getAnimatedValue();
@@ -94,14 +115,16 @@
});
heightAnimator.start();
+ // Animate width
ValueAnimator widthAnimator = ValueAnimator.ofInt(mContentWidth, contentWidth);
widthAnimator.addUpdateListener((animation) -> {
mContentWidth = (int) animation.getAnimatedValue();
});
+ // Play together
AnimatorSet as = new AnimatorSet();
- as.setDuration(AuthDialog.ANIMATE_DURATION_MS);
- as.play(heightAnimator).with(widthAnimator);
+ as.setDuration(animateDurationMs);
+ as.playTogether(cornerAnimator, heightAnimator, widthAnimator, marginAnimator);
as.start();
} else {
mContentWidth = contentWidth;
@@ -123,7 +146,7 @@
mPanelView = panelView;
mCornerRadius = context.getResources()
.getDimension(R.dimen.biometric_dialog_corner_size);
- mBiometricMargin = (int) context.getResources()
+ mMargin = (int) context.getResources()
.getDimension(R.dimen.biometric_dialog_border_padding);
mPanelView.setOutlineProvider(this);
mPanelView.setClipToOutline(true);