/*
 * 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<Animator> 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;
    }
}
