/* * 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 android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.PromptInfo; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.text.method.ScrollingMovementMethod; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.R; import com.airbnb.lottie.LottieAnimationView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** * Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers. */ public abstract class AuthBiometricView extends LinearLayout implements AuthBiometricViewAdapter { private static final String TAG = "AuthBiometricView"; /** * Authentication hardware idle. */ public static final int STATE_IDLE = 0; /** * UI animating in, authentication hardware active. */ public static final int STATE_AUTHENTICATING_ANIMATING_IN = 1; /** * UI animated in, authentication hardware active. */ public static final int STATE_AUTHENTICATING = 2; /** * UI animated in, authentication hardware active. */ public static final int STATE_HELP = 3; /** * Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle. */ public static final int STATE_ERROR = 4; /** * Authenticated, waiting for user confirmation. Authentication hardware idle. */ public static final int STATE_PENDING_CONFIRMATION = 5; /** * Authenticated, dialog animating away soon. */ public static final int STATE_AUTHENTICATED = 6; @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_HELP, STATE_ERROR, STATE_PENDING_CONFIRMATION, STATE_AUTHENTICATED}) @interface BiometricState {} /** * Callback to the parent when a user action has occurred. */ public interface Callback { int ACTION_AUTHENTICATED = 1; int ACTION_USER_CANCELED = 2; int ACTION_BUTTON_NEGATIVE = 3; int ACTION_BUTTON_TRY_AGAIN = 4; int ACTION_ERROR = 5; int ACTION_USE_DEVICE_CREDENTIAL = 6; int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7; int ACTION_AUTHENTICATED_AND_CONFIRMED = 8; /** * When an action has occurred. The caller will only invoke this when the callback should * be propagated. e.g. the caller will handle any necessary delay. * @param action */ void onAction(int action); } private final Handler mHandler; private final AccessibilityManager mAccessibilityManager; private final LockPatternUtils mLockPatternUtils; protected final int mTextColorError; protected final int mTextColorHint; private AuthPanelController mPanelController; private PromptInfo mPromptInfo; private boolean mRequireConfirmation; private int mUserId; private int mEffectiveUserId; private @AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN; private TextView mTitleView; private TextView mSubtitleView; private TextView mDescriptionView; private View mIconHolderView; protected LottieAnimationView mIconViewOverlay; protected LottieAnimationView mIconView; protected TextView mIndicatorView; @VisibleForTesting @NonNull AuthIconController mIconController; @VisibleForTesting int mAnimationDurationShort = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS; @VisibleForTesting int mAnimationDurationLong = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS; @VisibleForTesting int mAnimationDurationHideDialog = BiometricPrompt.HIDE_DIALOG_DELAY; // Negative button position, exclusively for the app-specified behavior @VisibleForTesting Button mNegativeButton; // Negative button position, exclusively for cancelling auth after passive auth success @VisibleForTesting Button mCancelButton; // Negative button position, shown if device credentials are allowed @VisibleForTesting Button mUseCredentialButton; // Positive button position, @VisibleForTesting Button mConfirmButton; @VisibleForTesting Button mTryAgainButton; // Measurements when biometric view is showing text, buttons, etc. @Nullable @VisibleForTesting AuthDialog.LayoutParams mLayoutParams; private Callback mCallback; @BiometricState private int mState; private float mIconOriginalY; protected boolean mDialogSizeAnimating; protected Bundle mSavedState; private final Runnable mResetErrorRunnable; private final Runnable mResetHelpRunnable; private Animator.AnimatorListener mJankListener; private final boolean mUseCustomBpSize; private final int mCustomBpWidth; private final int mCustomBpHeight; private final OnClickListener mBackgroundClickListener = (view) -> { if (mState == STATE_AUTHENTICATED) { Log.w(TAG, "Ignoring background click after authenticated"); return; } else if (mSize == AuthDialog.SIZE_SMALL) { Log.w(TAG, "Ignoring background click during small dialog"); return; } else if (mSize == AuthDialog.SIZE_LARGE) { Log.w(TAG, "Ignoring background click during large dialog"); return; } mCallback.onAction(Callback.ACTION_USER_CANCELED); }; public AuthBiometricView(Context context) { this(context, null); } public AuthBiometricView(Context context, AttributeSet attrs) { super(context, attrs); mHandler = new Handler(Looper.getMainLooper()); mTextColorError = getResources().getColor( R.color.biometric_dialog_error, context.getTheme()); mTextColorHint = getResources().getColor( R.color.biometric_dialog_gray, context.getTheme()); mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mLockPatternUtils = new LockPatternUtils(context); mResetErrorRunnable = () -> { updateState(getStateForAfterError()); handleResetAfterError(); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); }; mResetHelpRunnable = () -> { updateState(STATE_AUTHENTICATING); handleResetAfterHelp(); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); }; mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size); mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width); mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height); } /** Delay after authentication is confirmed, before the dialog should be animated away. */ protected int getDelayAfterAuthenticatedDurationMs() { return 0; } /** State that the dialog/icon should be in after showing a help message. */ protected int getStateForAfterError() { return STATE_IDLE; } /** Invoked when the error message is being cleared. */ protected void handleResetAfterError() {} /** Invoked when the help message is being cleared. */ protected void handleResetAfterHelp() {} /** True if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}. */ protected boolean supportsSmallDialog() { return false; } /** The string to show when the user must tap to confirm via the button or icon. */ @StringRes protected int getConfirmationPrompt() { return R.string.biometric_dialog_tap_confirm; } /** True if require confirmation will be honored when set via the API. */ protected boolean supportsRequireConfirmation() { return false; } /** True if confirmation will be required even if it was not supported/requested. */ protected boolean forceRequireConfirmation(@Modality int modality) { return false; } /** Ignore all events from this (secondary) modality except successful authentication. */ protected boolean ignoreUnsuccessfulEventsFrom(@Modality int modality, String unsuccessfulReason) { return false; } /** Create the controller for managing the icons transitions during the prompt.*/ @NonNull protected abstract AuthIconController createIconController(); @Override public AuthIconController getLegacyIconController() { return mIconController; } @Override public void cancelAnimation() { animate().cancel(); } @Override public View asView() { return this; } @Override public boolean isCoex() { return false; } void setPanelController(AuthPanelController panelController) { mPanelController = panelController; } void setPromptInfo(PromptInfo promptInfo) { mPromptInfo = promptInfo; } void setCallback(Callback callback) { mCallback = callback; } void setBackgroundView(View backgroundView) { backgroundView.setOnClickListener(mBackgroundClickListener); } void setUserId(int userId) { mUserId = userId; } void setEffectiveUserId(int effectiveUserId) { mEffectiveUserId = effectiveUserId; } void setRequireConfirmation(boolean requireConfirmation) { mRequireConfirmation = requireConfirmation && supportsRequireConfirmation(); } void setJankListener(Animator.AnimatorListener jankListener) { mJankListener = jankListener; } private void updatePaddings(int size) { final Insets navBarInsets = Utils.getNavbarInsets(mContext); if (size != AuthDialog.SIZE_LARGE) { if (mPanelController.getPosition() == AuthPanelController.POSITION_LEFT) { setPadding(navBarInsets.left, 0, 0, 0); } else if (mPanelController.getPosition() == AuthPanelController.POSITION_RIGHT) { setPadding(0, 0, navBarInsets.right, 0); } else { setPadding(0, 0, 0, navBarInsets.bottom); } } else { setPadding(0, 0, 0, 0); } } @VisibleForTesting final void updateSize(@AuthDialog.DialogSize int newSize) { Log.v(TAG, "Current size: " + mSize + " New size: " + newSize); updatePaddings(newSize); if (newSize == AuthDialog.SIZE_SMALL) { mTitleView.setVisibility(View.GONE); mSubtitleView.setVisibility(View.GONE); mDescriptionView.setVisibility(View.GONE); mIndicatorView.setVisibility(View.GONE); mNegativeButton.setVisibility(View.GONE); mUseCredentialButton.setVisibility(View.GONE); final float iconPadding = getResources() .getDimension(R.dimen.biometric_dialog_icon_padding); mIconHolderView.setY(getHeight() - mIconHolderView.getHeight() - iconPadding); // Subtract the vertical padding from the new height since it's only used to create // extra space between the other elements, and not part of the actual icon. final int newHeight = mIconHolderView.getHeight() + 2 * (int) iconPadding - mIconHolderView.getPaddingTop() - mIconHolderView.getPaddingBottom(); mPanelController.updateForContentDimensions(mLayoutParams.mMediumWidth, newHeight, 0 /* animateDurationMs */); mSize = newSize; } else if (mSize == AuthDialog.SIZE_SMALL && newSize == AuthDialog.SIZE_MEDIUM) { if (mDialogSizeAnimating) { return; } mDialogSizeAnimating = true; // Animate the icon back to original position final ValueAnimator iconAnimator = ValueAnimator.ofFloat(mIconHolderView.getY(), mIconOriginalY); iconAnimator.addUpdateListener((animation) -> { mIconHolderView.setY((float) animation.getAnimatedValue()); }); // Animate the text final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1); opacityAnimator.addUpdateListener((animation) -> { final float opacity = (float) animation.getAnimatedValue(); mTitleView.setAlpha(opacity); mIndicatorView.setAlpha(opacity); mNegativeButton.setAlpha(opacity); mCancelButton.setAlpha(opacity); mTryAgainButton.setAlpha(opacity); if (!TextUtils.isEmpty(mSubtitleView.getText())) { mSubtitleView.setAlpha(opacity); } if (!TextUtils.isEmpty(mDescriptionView.getText())) { mDescriptionView.setAlpha(opacity); } }); // Choreograph together final AnimatorSet as = new AnimatorSet(); as.setDuration(mAnimationDurationShort); as.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); mTitleView.setVisibility(View.VISIBLE); mIndicatorView.setVisibility(View.VISIBLE); if (isDeviceCredentialAllowed()) { mUseCredentialButton.setVisibility(View.VISIBLE); } else { mNegativeButton.setVisibility(View.VISIBLE); } if (supportsManualRetry()) { mTryAgainButton.setVisibility(View.VISIBLE); } if (!TextUtils.isEmpty(mSubtitleView.getText())) { mSubtitleView.setVisibility(View.VISIBLE); } if (!TextUtils.isEmpty(mDescriptionView.getText())) { mDescriptionView.setVisibility(View.VISIBLE); } } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mSize = newSize; mDialogSizeAnimating = false; Utils.notifyAccessibilityContentChanged(mAccessibilityManager, AuthBiometricView.this); } }); if (mJankListener != null) { as.addListener(mJankListener); } as.play(iconAnimator).with(opacityAnimator); as.start(); // Animate the panel mPanelController.updateForContentDimensions(mLayoutParams.mMediumWidth, mLayoutParams.mMediumHeight, AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS); } else if (newSize == AuthDialog.SIZE_MEDIUM) { mPanelController.updateForContentDimensions(mLayoutParams.mMediumWidth, mLayoutParams.mMediumHeight, 0 /* animateDurationMs */); mSize = newSize; } else if (newSize == AuthDialog.SIZE_LARGE) { 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(mAnimationDurationLong); translationAnimator.addUpdateListener((animation) -> { final float translation = (float) animation.getAnimatedValue(); biometricView.setTranslationY(translation); }); translationAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (biometricView.getParent() instanceof ViewGroup) { ((ViewGroup) biometricView.getParent()).removeView(biometricView); } mSize = newSize; } }); // Opacity to 0 in half duration final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0); opacityAnimator.setDuration(mAnimationDurationLong / 2); opacityAnimator.addUpdateListener((animation) -> { final float opacity = (float) animation.getAnimatedValue(); biometricView.setAlpha(opacity); }); mPanelController.setUseFullScreen(true); mPanelController.updateForContentDimensions( mPanelController.getContainerWidth(), mPanelController.getContainerHeight(), mAnimationDurationLong); // Start the animations together AnimatorSet as = new AnimatorSet(); List animators = new ArrayList<>(); animators.add(translationAnimator); animators.add(opacityAnimator); if (mJankListener != null) { as.addListener(mJankListener); } as.playTogether(animators); as.setDuration(mAnimationDurationLong * 2 / 3); as.start(); } else { Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize); } Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } protected boolean supportsManualRetry() { return false; } /** * Updates mIconView animation on updates to fold state, device rotation, or rear display mode * @param animation new asset to use for iconw */ public void updateIconViewAnimation(int animation) { mIconView.setAnimation(animation); } public void updateState(@BiometricState int newState) { Log.d(TAG, "newState: " + newState); mIconController.updateState(mState, newState); switch (newState) { case STATE_AUTHENTICATING_ANIMATING_IN: case STATE_AUTHENTICATING: removePendingAnimations(); if (mRequireConfirmation) { mConfirmButton.setEnabled(false); mConfirmButton.setVisibility(View.VISIBLE); } break; case STATE_AUTHENTICATED: removePendingAnimations(); if (mSize != AuthDialog.SIZE_SMALL) { mConfirmButton.setVisibility(View.GONE); mNegativeButton.setVisibility(View.GONE); mUseCredentialButton.setVisibility(View.GONE); mCancelButton.setVisibility(View.GONE); mIndicatorView.setVisibility(View.INVISIBLE); } announceForAccessibility(getResources() .getString(R.string.biometric_dialog_authenticated)); if (mState == STATE_PENDING_CONFIRMATION) { mHandler.postDelayed(() -> mCallback.onAction( Callback.ACTION_AUTHENTICATED_AND_CONFIRMED), getDelayAfterAuthenticatedDurationMs()); } else { mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_AUTHENTICATED), getDelayAfterAuthenticatedDurationMs()); } break; case STATE_PENDING_CONFIRMATION: removePendingAnimations(); mNegativeButton.setVisibility(View.GONE); mCancelButton.setVisibility(View.VISIBLE); mUseCredentialButton.setVisibility(View.GONE); // forced confirmations (multi-sensor) use the icon view as the confirm button mConfirmButton.setEnabled(mRequireConfirmation); mConfirmButton.setVisibility(mRequireConfirmation ? View.VISIBLE : View.GONE); mIndicatorView.setTextColor(mTextColorHint); mIndicatorView.setText(getConfirmationPrompt()); mIndicatorView.setVisibility(View.VISIBLE); break; case STATE_ERROR: if (mSize == AuthDialog.SIZE_SMALL) { updateSize(AuthDialog.SIZE_MEDIUM); } break; default: Log.w(TAG, "Unhandled state: " + newState); break; } Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); mState = newState; } public void onOrientationChanged() { // Update padding and AuthPanel outline by calling updateSize when the orientation changed. updateSize(mSize); } public void onDialogAnimatedIn(boolean fingerprintWasStarted) { updateState(STATE_AUTHENTICATING); } public void onAuthenticationSucceeded(@Modality int modality) { removePendingAnimations(); if (mRequireConfirmation || forceRequireConfirmation(modality)) { updateState(STATE_PENDING_CONFIRMATION); } else { updateState(STATE_AUTHENTICATED); } } /** * Notify the view that auth has failed. * * @param modality sensor modality that failed * @param failureReason message */ public void onAuthenticationFailed( @Modality int modality, @Nullable String failureReason) { if (ignoreUnsuccessfulEventsFrom(modality, failureReason)) { return; } showTemporaryMessage(failureReason, mResetErrorRunnable); updateState(STATE_ERROR); } /** * Notify the view that an error occurred. * * @param modality sensor modality that failed * @param error message */ public void onError(@Modality int modality, String error) { if (ignoreUnsuccessfulEventsFrom(modality, error)) { return; } showTemporaryMessage(error, mResetErrorRunnable); updateState(STATE_ERROR); mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_ERROR), mAnimationDurationHideDialog); } /** * Show a help message to the user. * * @param modality sensor modality * @param help message */ public void onHelp(@Modality int modality, String help) { if (ignoreUnsuccessfulEventsFrom(modality, help)) { return; } if (mSize != AuthDialog.SIZE_MEDIUM) { Log.w(TAG, "Help received in size: " + mSize); return; } if (TextUtils.isEmpty(help)) { Log.w(TAG, "Ignoring blank help message"); return; } showTemporaryMessage(help, mResetHelpRunnable); updateState(STATE_HELP); } public void onSaveState(@NonNull Bundle outState) { outState.putInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY, mConfirmButton.getVisibility()); outState.putInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility()); outState.putInt(AuthDialog.KEY_BIOMETRIC_STATE, mState); outState.putString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING, mIndicatorView.getText() != null ? mIndicatorView.getText().toString() : ""); outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING, mHandler.hasCallbacks(mResetErrorRunnable)); outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING, mHandler.hasCallbacks(mResetHelpRunnable)); outState.putInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE, mSize); } /** * Invoked after inflation but before being attached to window. * @param savedState */ public void restoreState(@Nullable Bundle savedState) { mSavedState = savedState; } private void setTextOrHide(TextView view, CharSequence charSequence) { if (TextUtils.isEmpty(charSequence)) { view.setVisibility(View.GONE); } else { view.setText(charSequence); } Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } // Remove all pending icon and text animations private void removePendingAnimations() { mHandler.removeCallbacks(mResetHelpRunnable); mHandler.removeCallbacks(mResetErrorRunnable); } private void showTemporaryMessage(String message, Runnable resetMessageRunnable) { removePendingAnimations(); mIndicatorView.setText(message); mIndicatorView.setTextColor(mTextColorError); mIndicatorView.setVisibility(View.VISIBLE); // select to enable marquee unless a screen reader is enabled mIndicatorView.setSelected(!mAccessibilityManager.isEnabled() || !mAccessibilityManager.isTouchExplorationEnabled()); mHandler.postDelayed(resetMessageRunnable, mAnimationDurationHideDialog); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (mSavedState != null) { updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); } } @Override protected void onFinishInflate() { super.onFinishInflate(); mTitleView = findViewById(R.id.title); mSubtitleView = findViewById(R.id.subtitle); mDescriptionView = findViewById(R.id.description); mIconViewOverlay = findViewById(R.id.biometric_icon_overlay); mIconView = findViewById(R.id.biometric_icon); mIconHolderView = findViewById(R.id.biometric_icon_frame); mIndicatorView = findViewById(R.id.indicator); // Negative-side (left) buttons mNegativeButton = findViewById(R.id.button_negative); mCancelButton = findViewById(R.id.button_cancel); mUseCredentialButton = findViewById(R.id.button_use_credential); // Positive-side (right) buttons mConfirmButton = findViewById(R.id.button_confirm); mTryAgainButton = findViewById(R.id.button_try_again); mNegativeButton.setOnClickListener((view) -> { mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE); }); mCancelButton.setOnClickListener((view) -> { mCallback.onAction(Callback.ACTION_USER_CANCELED); }); mUseCredentialButton.setOnClickListener((view) -> { startTransitionToCredentialUI(false /* isError */); }); mConfirmButton.setOnClickListener((view) -> { updateState(STATE_AUTHENTICATED); }); mTryAgainButton.setOnClickListener((view) -> { updateState(STATE_AUTHENTICATING); mCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN); mTryAgainButton.setVisibility(View.GONE); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); }); mIconController = createIconController(); if (mIconController.getActsAsConfirmButton()) { mIconViewOverlay.setOnClickListener((view)->{ if (mState == STATE_PENDING_CONFIRMATION) { updateState(STATE_AUTHENTICATED); } }); mIconView.setOnClickListener((view) -> { if (mState == STATE_PENDING_CONFIRMATION) { updateState(STATE_AUTHENTICATED); } }); } } /** * Kicks off the animation process and invokes the callback. * * @param isError if this was triggered due to an error and not a user action (unused, * previously for haptics). */ @Override public void startTransitionToCredentialUI(boolean isError) { updateSize(AuthDialog.SIZE_LARGE); mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mTitleView.setText(mPromptInfo.getTitle()); // setSelected could make marquee work mTitleView.setSelected(true); mSubtitleView.setSelected(true); // make description view become scrollable mDescriptionView.setMovementMethod(new ScrollingMovementMethod()); if (isDeviceCredentialAllowed()) { final CharSequence credentialButtonText; @Utils.CredentialType final int credentialType = Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId); switch (credentialType) { case Utils.CREDENTIAL_PIN: credentialButtonText = getResources().getString(R.string.biometric_dialog_use_pin); break; case Utils.CREDENTIAL_PATTERN: credentialButtonText = getResources().getString(R.string.biometric_dialog_use_pattern); break; case Utils.CREDENTIAL_PASSWORD: default: credentialButtonText = getResources().getString(R.string.biometric_dialog_use_password); break; } mNegativeButton.setVisibility(View.GONE); mUseCredentialButton.setText(credentialButtonText); mUseCredentialButton.setVisibility(View.VISIBLE); } else { mNegativeButton.setText(mPromptInfo.getNegativeButtonText()); } setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle()); setTextOrHide(mDescriptionView, mPromptInfo.getDescription()); if (mSavedState == null) { updateState(STATE_AUTHENTICATING_ANIMATING_IN); } else { // Restore as much state as possible first updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); // Restore positive button(s) state mConfirmButton.setVisibility( mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY)); if (mConfirmButton.getVisibility() == View.GONE) { setRequireConfirmation(false); } mTryAgainButton.setVisibility( mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mIconController.setDeactivated(true); // Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once // the new dialog is restored. mHandler.removeCallbacksAndMessages(null /* all */); } /** * Contains all of the testable logic that should be invoked when {@link #onMeasure(int, int)} * is invoked. In addition, this allows subclasses to implement custom measuring logic while * allowing the base class to have common code to apply the custom measurements. * * @param width Width to constrain the measurements to. * @param height Height to constrain the measurements to. * @return See {@link AuthDialog.LayoutParams} */ @NonNull AuthDialog.LayoutParams onMeasureInternal(int width, int height) { int totalHeight = 0; final int numChildren = getChildCount(); for (int i = 0; i < numChildren; i++) { final View child = getChildAt(i); if (child.getId() == R.id.space_above_icon || child.getId() == R.id.space_below_icon || child.getId() == R.id.button_bar) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.biometric_icon_frame) { final View iconView = findViewById(R.id.biometric_icon); child.measure( MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.biometric_icon) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); } else { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); } if (child.getVisibility() != View.GONE) { totalHeight += child.getMeasuredHeight(); } } return new AuthDialog.LayoutParams(width, totalHeight); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); if (mUseCustomBpSize) { width = mCustomBpWidth; height = mCustomBpHeight; } else { width = Math.min(width, height); } mLayoutParams = onMeasureInternal(width, height); final Insets navBarInsets = Utils.getNavbarInsets(mContext); final int navBarHeight = navBarInsets.bottom; final int navBarWidth; if (mPanelController.getPosition() == AuthPanelController.POSITION_LEFT) { navBarWidth = navBarInsets.left; } else if (mPanelController.getPosition() == AuthPanelController.POSITION_RIGHT) { navBarWidth = navBarInsets.right; } else { navBarWidth = 0; } // The actual auth dialog w/h should include navigation bar size. if (navBarWidth != 0 || navBarHeight != 0) { mLayoutParams = new AuthDialog.LayoutParams( mLayoutParams.mMediumWidth + navBarWidth, mLayoutParams.mMediumHeight + navBarInsets.bottom); } setMeasuredDimension(mLayoutParams.mMediumWidth, mLayoutParams.mMediumHeight); } @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); // Start with initial size only once. Subsequent layout changes don't matter since we // only care about the initial icon position. if (mIconOriginalY == 0) { mIconOriginalY = mIconHolderView.getY(); if (mSavedState == null) { updateSize(!mRequireConfirmation && supportsSmallDialog() ? AuthDialog.SIZE_SMALL : AuthDialog.SIZE_MEDIUM); } else { updateSize(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE)); // Restore indicator text state only after size has been restored final String indicatorText = mSavedState.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING); if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING)) { onHelp(TYPE_NONE, indicatorText); } else if (mSavedState.getBoolean( AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)) { onAuthenticationFailed(TYPE_NONE, indicatorText); } } } } private boolean isDeviceCredentialAllowed() { return Utils.isDeviceCredentialAllowed(mPromptInfo); } public LottieAnimationView getIconView() { return mIconView; } @AuthDialog.DialogSize int getSize() { return mSize; } /** If authentication has successfully occurred and the view is done. */ boolean isAuthenticated() { return mState == STATE_AUTHENTICATED; } /** If authentication is currently in progress. */ boolean isAuthenticating() { return mState == STATE_AUTHENTICATING; } }