/*
 * Copyright (C) 2014 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.keyguard;

import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.systemBars;
import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;

import static androidx.constraintlayout.widget.ConstraintSet.BOTTOM;
import static androidx.constraintlayout.widget.ConstraintSet.CHAIN_SPREAD;
import static androidx.constraintlayout.widget.ConstraintSet.END;
import static androidx.constraintlayout.widget.ConstraintSet.LEFT;
import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT;
import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
import static androidx.constraintlayout.widget.ConstraintSet.RIGHT;
import static androidx.constraintlayout.widget.ConstraintSet.START;
import static androidx.constraintlayout.widget.ConstraintSet.TOP;
import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;

import static com.android.app.animation.InterpolatorsAndroidX.DECELERATE_QUINT;
import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;

import static java.lang.Integer.max;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.LayerDrawable;
import android.os.UserManager;
import android.provider.Settings;
import android.transition.TransitionManager;
import android.util.AttributeSet;
import android.util.Log;
import android.util.MathUtils;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.window.BackEvent;
import android.window.OnBackAnimationCallback;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;

import com.android.app.animation.Interpolators;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.UserIcons;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.settingslib.Utils;
import com.android.settingslib.drawable.CircleFramedDrawable;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.classifier.FalsingA11yDelegate;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.res.R;
import com.android.systemui.shade.TouchLogger;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.user.data.source.UserRecord;
import com.android.systemui.util.settings.GlobalSettings;

import java.util.ArrayList;
import java.util.List;

/** Determines how the bouncer is displayed to the user. */
public class KeyguardSecurityContainer extends ConstraintLayout {
    static final int USER_TYPE_PRIMARY = 1;
    static final int USER_TYPE_WORK_PROFILE = 2;
    static final int USER_TYPE_SECONDARY_USER = 3;

    @IntDef({MODE_UNINITIALIZED, MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER})
    public @interface Mode {}
    static final int MODE_UNINITIALIZED = -1;
    static final int MODE_DEFAULT = 0;
    static final int MODE_ONE_HANDED = 1;
    static final int MODE_USER_SWITCHER = 2;

    // Bouncer is dismissed due to no security.
    static final int BOUNCER_DISMISS_NONE_SECURITY = 0;
    // Bouncer is dismissed due to pin, password or pattern entered.
    static final int BOUNCER_DISMISS_PASSWORD = 1;
    // Bouncer is dismissed due to biometric (face, fingerprint or iris) authenticated.
    static final int BOUNCER_DISMISS_BIOMETRIC = 2;
    // Bouncer is dismissed due to extended access granted.
    static final int BOUNCER_DISMISS_EXTENDED_ACCESS = 3;
    // Bouncer is dismissed due to sim card unlock code entered.
    static final int BOUNCER_DISMISS_SIM = 4;

    private static final String TAG = "KeyguardSecurityView";

    // Make the view move slower than the finger, as if the spring were applying force.
    private static final float TOUCH_Y_MULTIPLIER = 0.25f;
    // How much you need to drag the bouncer to trigger an auth retry (in dps.)
    private static final float MIN_DRAG_SIZE = 10;
    // How much to scale the default slop by, to avoid accidental drags.
    private static final float SLOP_SCALE = 4f;
    @VisibleForTesting
    // How much the view scales down to during back gestures.
    static final float MIN_BACK_SCALE = 0.9f;
    @VisibleForTesting
    KeyguardSecurityViewFlipper mSecurityViewFlipper;
    private GlobalSettings mGlobalSettings;
    private FalsingManager mFalsingManager;
    private UserSwitcherController mUserSwitcherController;
    private FalsingA11yDelegate mFalsingA11yDelegate;
    private AlertDialog mAlertDialog;
    private boolean mSwipeUpToRetry;

    private final ViewConfiguration mViewConfiguration;
    private final SpringAnimation mSpringAnimation;
    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
    private final List<Gefingerpoken> mMotionEventListeners = new ArrayList<>();
    private final GestureDetector mDoubleTapDetector;

    private float mLastTouchY = -1;
    private int mActivePointerId = -1;
    private boolean mIsDragging;
    private float mStartTouchY = -1;
    private boolean mDisappearAnimRunning;
    private SwipeListener mSwipeListener;
    private ViewMode mViewMode = new DefaultViewMode();
    private boolean mIsInteractable;
    protected ViewMediatorCallback mViewMediatorCallback;
    /*
     * Using MODE_UNINITIALIZED to mean the view mode is set to DefaultViewMode, but init() has not
     * yet been called on it. This will happen when the ViewController is initialized.
     */
    private @Mode int mCurrentMode = MODE_UNINITIALIZED;
    private int mWidth = -1;

    /**
     * This callback is used to animate KeyguardSecurityContainer and its child views based on
     * the interaction with the ime. After
     * {@link WindowInsetsAnimation.Callback#onPrepare(WindowInsetsAnimation)},
     * {@link #onApplyWindowInsets} is called where we
     * set the bottom padding to be the height of the keyboard. We use this padding to determine
     * the delta of vertical distance for y-translation animations.
     * Note that bottom padding is not set when the disappear animation is started because
     * we are deferring the y translation logic to the animator in
     * {@link KeyguardPasswordView#startDisappearAnimation(Runnable)}
     */
    private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback =
            new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {

                private final Rect mInitialBounds = new Rect();
                private final Rect mFinalBounds = new Rect();

                @Override
                public void onPrepare(WindowInsetsAnimation animation) {
                    mSecurityViewFlipper.getBoundsOnScreen(mInitialBounds);
                }

                @Override
                public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation,
                        WindowInsetsAnimation.Bounds bounds) {
                    if (!mDisappearAnimRunning) {
                        beginJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR);
                    } else {
                        beginJankInstrument(
                                InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR);
                    }
                    mSecurityViewFlipper.getBoundsOnScreen(mFinalBounds);
                    return bounds;
                }

                @Override
                public WindowInsets onProgress(WindowInsets windowInsets,
                        List<WindowInsetsAnimation> list) {
                    float start = mDisappearAnimRunning
                            ? -(mFinalBounds.bottom - mInitialBounds.bottom)
                            : mInitialBounds.bottom - mFinalBounds.bottom;
                    float end = mDisappearAnimRunning
                            ? -((mFinalBounds.bottom - mInitialBounds.bottom) * 0.75f)
                            : 0f;
                    int translationY = 0;
                    float interpolatedFraction = 1f;
                    for (WindowInsetsAnimation animation : list) {
                        if ((animation.getTypeMask() & WindowInsets.Type.ime()) == 0) {
                            continue;
                        }
                        interpolatedFraction = animation.getInterpolatedFraction();
                        final int paddingBottom = (int) MathUtils.lerp(
                                start, end,
                                interpolatedFraction);
                        translationY += paddingBottom;
                    }

                    float alpha = mDisappearAnimRunning
                            ? 1 - interpolatedFraction
                            : Math.max(interpolatedFraction, getAlpha());
                    updateChildren(translationY, alpha);

                    return windowInsets;
                }

                @Override
                public void onEnd(WindowInsetsAnimation animation) {
                    if (!mDisappearAnimRunning) {
                        endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR);
                    } else {
                        endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR);
                        setAlpha(0f);
                    }
                    updateChildren(0 /* translationY */, 1f /* alpha */);
                }
            };

    private final OnBackAnimationCallback mBackCallback = new OnBackAnimationCallback() {
        @Override
        public void onBackCancelled() {
            // TODO(b/259608500): Remove once back API auto animates progress to 0 on cancel.
            resetScale();
        }

        @Override
        public void onBackInvoked() { }

        @Override
        public void onBackProgressed(BackEvent event) {
            float progress = event.getProgress();
            // TODO(b/263819310): Update the interpolator to match spec.
            float scale = MIN_BACK_SCALE
                    +  (1 - MIN_BACK_SCALE) * (1 - DECELERATE_QUINT.getInterpolation(progress));
            setScale(scale);
        }
    };
    /**
     * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture.
     */
    @NonNull
    OnBackAnimationCallback getBackCallback() {
        return mBackCallback;
    }

    public interface SwipeListener {
        void onSwipeUp();
        /** */
        void onSwipeDown();
    }

    @VisibleForTesting
    public enum BouncerUiEvent implements UiEventLogger.UiEventEnum {
        @UiEvent(doc = "Default UiEvent used for variable initialization.")
        UNKNOWN(0),

        @UiEvent(doc = "Bouncer is dismissed using extended security access.")
        BOUNCER_DISMISS_EXTENDED_ACCESS(413),

        @UiEvent(doc = "Bouncer is dismissed using biometric.")
        BOUNCER_DISMISS_BIOMETRIC(414),

        @UiEvent(doc = "Bouncer is dismissed without security access.")
        BOUNCER_DISMISS_NONE_SECURITY(415),

        @UiEvent(doc = "Bouncer is dismissed using password security.")
        BOUNCER_DISMISS_PASSWORD(416),

        @UiEvent(doc = "Bouncer is dismissed using sim security access.")
        BOUNCER_DISMISS_SIM(417),

        @UiEvent(doc = "Bouncer is successfully unlocked using password.")
        BOUNCER_PASSWORD_SUCCESS(418),

        @UiEvent(doc = "An attempt to unlock bouncer using password has failed.")
        BOUNCER_PASSWORD_FAILURE(419);

        private final int mId;

        BouncerUiEvent(int id) {
            mId = id;
        }

        @Override
        public int getId() {
            return mId;
        }
    }

    public KeyguardSecurityContainer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public KeyguardSecurityContainer(Context context) {
        this(context, null, 0);
    }

    public KeyguardSecurityContainer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mSpringAnimation = new SpringAnimation(this, DynamicAnimation.TRANSLATION_Y);
        mViewConfiguration = ViewConfiguration.get(context);
        mDoubleTapDetector = new GestureDetector(context, new DoubleTapListener());

        // Add additional top padding.
        setPadding(getPaddingLeft(), getPaddingTop() + getResources().getDimensionPixelSize(
                        R.dimen.keyguard_security_container_padding_top), getPaddingRight(),
                getPaddingBottom());
    }

    void onResume(SecurityMode securityMode, boolean faceAuthEnabled) {
        mSecurityViewFlipper.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback);
        updateBiometricRetry(securityMode, faceAuthEnabled);
    }

    void initMode(@Mode int mode, GlobalSettings globalSettings, FalsingManager falsingManager,
            UserSwitcherController userSwitcherController,
            UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback,
            FalsingA11yDelegate falsingA11yDelegate) {
        if (mCurrentMode == mode) return;
        Log.i(TAG, "Switching mode from " + modeToString(mCurrentMode) + " to "
                + modeToString(mode));
        mCurrentMode = mode;
        mViewMode.onDestroy();

        switch (mode) {
            case MODE_ONE_HANDED:
                mViewMode = new OneHandedViewMode();
                break;
            case MODE_USER_SWITCHER:
                mViewMode = new UserSwitcherViewMode(userSwitcherCallback);
                break;
            default:
                mViewMode = new DefaultViewMode();
        }
        mGlobalSettings = globalSettings;
        mFalsingManager = falsingManager;
        mFalsingA11yDelegate = falsingA11yDelegate;
        mUserSwitcherController = userSwitcherController;
        setupViewMode();
    }

    private String modeToString(@Mode int mode) {
        switch (mode) {
            case MODE_UNINITIALIZED:
                return "Uninitialized";
            case MODE_DEFAULT:
                return "Default";
            case MODE_ONE_HANDED:
                return "OneHanded";
            case MODE_USER_SWITCHER:
                return "UserSwitcher";
            default:
                throw new IllegalArgumentException("mode: " + mode + " not supported");
        }
    }

    private void setupViewMode() {
        if (mSecurityViewFlipper == null || mGlobalSettings == null
                || mFalsingManager == null || mUserSwitcherController == null) {
            return;
        }

        mViewMode.init(this, mGlobalSettings, mSecurityViewFlipper, mFalsingManager,
                mUserSwitcherController, mFalsingA11yDelegate);
    }

    @Mode int getMode() {
        return mCurrentMode;
    }

    /**
     * The position of the container can be adjusted based upon a touch at location x. This has
     * been used in one-handed mode to make sure the bouncer appears on the side of the display
     * that the user last interacted with.
     */
    void updatePositionByTouchX(float x) {
        mViewMode.updatePositionByTouchX(x);
    }

    public boolean isSidedSecurityMode() {
        return mViewMode instanceof SidedSecurityMode;
    }

    /** Returns whether the inner SecurityViewFlipper is left-aligned when in sided mode. */
    public boolean isSecurityLeftAligned() {
        return mViewMode instanceof SidedSecurityMode
                && ((SidedSecurityMode) mViewMode).isLeftAligned();
    }

    /**
     * Returns whether the touch happened on the other side of security (like bouncer) when in
     * sided mode.
     */
    public boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev) {
        return mViewMode instanceof SidedSecurityMode
                && ((SidedSecurityMode) mViewMode).isTouchOnTheOtherSideOfSecurity(ev);
    }

    public void onPause() {
        if (mAlertDialog != null) {
            mAlertDialog.dismiss();
            mAlertDialog = null;
        }
        mSecurityViewFlipper.setWindowInsetsAnimationCallback(null);
        mViewMode.reset();
    }

    /** Set true if the view can be interacted with */
    public void setInteractable(boolean isInteractable) {
        mIsInteractable = isInteractable;
    }

    @Override
    public boolean shouldDelayChildPressedState() {
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!mIsInteractable) {
            return true;
        }

        boolean result =  mMotionEventListeners.stream().anyMatch(
                listener -> listener.onInterceptTouchEvent(event))
                || super.onInterceptTouchEvent(event);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                int pointerIndex = event.getActionIndex();
                mStartTouchY = event.getY(pointerIndex);
                mActivePointerId = event.getPointerId(pointerIndex);
                mVelocityTracker.clear();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mIsDragging) {
                    return true;
                }
                if (!mSwipeUpToRetry) {
                    return false;
                }
                // Avoid dragging the pattern view
                if (mSecurityViewFlipper.getSecurityView() != null
                        && mSecurityViewFlipper.getSecurityView().disallowInterceptTouch(event)) {
                    return false;
                }
                int index = event.findPointerIndex(mActivePointerId);
                float touchSlop = mViewConfiguration.getScaledTouchSlop() * SLOP_SCALE;
                if (index != -1 && mStartTouchY - event.getY(index) > touchSlop) {
                    mIsDragging = true;
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsDragging = false;
                break;
        }
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();

        boolean result =  mMotionEventListeners.stream()
                .anyMatch(listener -> listener.onTouchEvent(event))
                || super.onTouchEvent(event);

        // double tap detector should be called after listeners handle touches as listeners are
        // helping with ignoring falsing. Otherwise falsing will be activated for some double taps
        mDoubleTapDetector.onTouchEvent(event);

        switch (action) {
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(event);
                int pointerIndex = event.findPointerIndex(mActivePointerId);
                if (pointerIndex != -1) {
                    float y = event.getY(pointerIndex);
                    if (mLastTouchY != -1) {
                        float dy = y - mLastTouchY;
                        setTranslationY(getTranslationY() + dy * TOUCH_Y_MULTIPLIER);
                    }
                    mLastTouchY = y;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointerId = -1;
                mLastTouchY = -1;
                mIsDragging = false;
                startSpringAnimation(mVelocityTracker.getYVelocity());
                break;
            case MotionEvent.ACTION_POINTER_UP:
                int index = event.getActionIndex();
                int pointerId = event.getPointerId(index);
                if (pointerId == mActivePointerId) {
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = index == 0 ? 1 : 0;
                    mLastTouchY = event.getY(newPointerIndex);
                    mActivePointerId = event.getPointerId(newPointerIndex);
                }
                break;
        }
        if (action == MotionEvent.ACTION_UP) {
            if (-getTranslationY() > TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    MIN_DRAG_SIZE, getResources().getDisplayMetrics())) {
                if (mSwipeListener != null) {
                    mSwipeListener.onSwipeUp();
                }
            } else if (getTranslationY() > TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    MIN_DRAG_SIZE, getResources().getDisplayMetrics())) {
                if (mSwipeListener != null) {
                    mSwipeListener.onSwipeDown();
                }
            }
        }
        return true;
    }

    private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            return handleDoubleTap(e);
        }
    }

    @VisibleForTesting boolean handleDoubleTap(MotionEvent e) {
        if (!mIsDragging) {
            mViewMode.handleDoubleTap(e);
            return true;
        }
        return false;
    }

    void addMotionEventListener(Gefingerpoken listener) {
        mMotionEventListeners.add(listener);
    }

    void removeMotionEventListener(Gefingerpoken listener) {
        mMotionEventListeners.remove(listener);
    }

    void setSwipeListener(SwipeListener swipeListener) {
        mSwipeListener = swipeListener;
    }

    private void startSpringAnimation(float startVelocity) {
        mSpringAnimation
                .setStartVelocity(startVelocity)
                .animateToFinalPosition(0);
    }

    /**
     * Runs after a successful authentication only
     */
    public void startDisappearAnimation(SecurityMode securitySelection) {
        mDisappearAnimRunning = true;
        if (securitySelection == SecurityMode.Password
                && mSecurityViewFlipper.getSecurityView() instanceof KeyguardPasswordView) {
            ((KeyguardPasswordView) mSecurityViewFlipper.getSecurityView())
                    .setDisappearAnimationListener(this::setTranslationY);
        } else {
            mViewMode.startDisappearAnimation(securitySelection);
        }
    }

    /**
     * This will run when the bouncer shows in all cases except when the user drags the bouncer up.
     */
    public void startAppearAnimation(SecurityMode securityMode) {
        setTranslationY(0f);
        updateChildren(0 /* translationY */, 1f /* alpha */);
        mViewMode.startAppearAnimation(securityMode);
    }

    private void beginJankInstrument(int cuj) {
        KeyguardInputView securityView = mSecurityViewFlipper.getSecurityView();
        if (securityView == null) return;
        InteractionJankMonitor.getInstance().begin(securityView, cuj);
    }

    private void endJankInstrument(int cuj) {
        InteractionJankMonitor.getInstance().end(cuj);
    }

    private void cancelJankInstrument(int cuj) {
        InteractionJankMonitor.getInstance().cancel(cuj);
    }

    /**
     * Enables/disables swipe up to retry on the bouncer.
     */
    private void updateBiometricRetry(SecurityMode securityMode, boolean faceAuthEnabled) {
        mSwipeUpToRetry = faceAuthEnabled
                && securityMode != SecurityMode.SimPin
                && securityMode != SecurityMode.SimPuk
                && securityMode != SecurityMode.None;
    }

    public CharSequence getTitle() {
        return mSecurityViewFlipper.getTitle();
    }


    @Override
    public void onFinishInflate() {
        super.onFinishInflate();
        mSecurityViewFlipper = findViewById(R.id.view_flipper);
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {

        // Consume bottom insets because we're setting the padding locally (for IME and navbar.)
        int bottomInset = insets.getInsetsIgnoringVisibility(systemBars()).bottom;
        int imeInset = insets.getInsets(ime()).bottom;
        int inset = max(bottomInset, imeInset);
        int paddingBottom = max(inset, getContext().getResources()
                .getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin));
        // If security mode is password, we rely on the animation value of defined in
        // KeyguardPasswordView to determine the y translation animation.
        // This means that we will prevent the WindowInsetsAnimationCallback from setting any y
        // translation values by preventing the setting of the padding here.
        if (!mDisappearAnimRunning) {
            setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), paddingBottom);
        }
        return insets.inset(0, 0, 0, inset);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return TouchLogger.logDispatchTouch(TAG, ev, super.dispatchTouchEvent(ev));
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mViewMediatorCallback != null) {
            mViewMediatorCallback.keyguardDoneDrawing();
        }
    }

    public void setViewMediatorCallback(ViewMediatorCallback viewMediatorCallback) {
        mViewMediatorCallback = viewMediatorCallback;
    }

    private void showDialog(String title, String message) {
        if (mAlertDialog != null) {
            mAlertDialog.dismiss();
        }

        mAlertDialog = new AlertDialog.Builder(mContext)
                .setTitle(title)
                .setMessage(message)
                .setCancelable(false)
                .setNeutralButton(R.string.ok, null)
                .create();
        if (!(mContext instanceof Activity)) {
            mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
        }
        mAlertDialog.show();
    }

    void showTimeoutDialog(int userId, int timeoutMs, LockPatternUtils lockPatternUtils,
            SecurityMode securityMode) {
        int timeoutInSeconds = timeoutMs / 1000;
        int messageId = 0;

        switch (securityMode) {
            case Pattern:
                messageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message;
                break;
            case PIN:
                messageId = R.string.kg_too_many_failed_pin_attempts_dialog_message;
                break;
            case Password:
                messageId = R.string.kg_too_many_failed_password_attempts_dialog_message;
                break;
            // These don't have timeout dialogs.
            case Invalid:
            case None:
            case SimPin:
            case SimPuk:
                break;
        }

        if (messageId != 0) {
            final String message = mContext.getString(messageId,
                    lockPatternUtils.getCurrentFailedPasswordAttempts(userId),
                    timeoutInSeconds);
            showDialog(null, message);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        int width = right - left;
        if (changed && mWidth != width) {
            mWidth = width;
            mViewMode.updateSecurityViewLocation();
        }
    }

    @Override
    protected void onConfigurationChanged(Configuration config) {
        super.onConfigurationChanged(config);
        mViewMode.updateSecurityViewLocation();
    }

    void showAlmostAtWipeDialog(int attempts, int remaining, int userType) {
        String message = null;
        switch (userType) {
            case USER_TYPE_PRIMARY:
                message = mContext.getString(R.string.kg_failed_attempts_almost_at_wipe,
                        attempts, remaining);
                break;
            case USER_TYPE_SECONDARY_USER:
                message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_user,
                        attempts, remaining);
                break;
            case USER_TYPE_WORK_PROFILE:
                message = mContext.getSystemService(DevicePolicyManager.class).getResources()
                        .getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE,
                                () -> mContext.getString(
                                        R.string.kg_failed_attempts_almost_at_erase_profile,
                                        attempts, remaining),
                        attempts, remaining);
                break;
        }
        showDialog(null, message);
    }

    void showWipeDialog(int attempts, int userType) {
        String message = null;
        switch (userType) {
            case USER_TYPE_PRIMARY:
                message = mContext.getString(R.string.kg_failed_attempts_now_wiping,
                        attempts);
                break;
            case USER_TYPE_SECONDARY_USER:
                message = mContext.getString(R.string.kg_failed_attempts_now_erasing_user,
                        attempts);
                break;
            case USER_TYPE_WORK_PROFILE:
                message = mContext.getSystemService(DevicePolicyManager.class).getResources()
                        .getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE,
                                () -> mContext.getString(
                                        R.string.kg_failed_attempts_now_erasing_profile, attempts),
                        attempts);
                break;
        }
        showDialog(null, message);
    }

    public void reset() {
        mViewMode.reset();
        mDisappearAnimRunning = false;
    }

    void reloadColors() {
        mViewMode.reloadColors();
    }

    /** Handles density or font scale changes. */
    void onDensityOrFontScaleChanged() {
        mViewMode.onDensityOrFontScaleChanged();
    }

    void resetScale() {
        setScale(1);
    }

    private void setScale(float scale) {
        setScaleX(scale);
        setScaleY(scale);
    }

    private void updateChildren(int translationY, float alpha) {
        for (int i = 0; i < getChildCount(); ++i) {
            View child = getChildAt(i);
            child.setTranslationY(translationY);
            child.setAlpha(alpha);
        }
    }

    /**
     * Enscapsulates the differences between bouncer modes for the container.
     */
    interface ViewMode {
        default void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                @NonNull KeyguardSecurityViewFlipper viewFlipper,
                @NonNull FalsingManager falsingManager,
                @NonNull UserSwitcherController userSwitcherController,
                @NonNull FalsingA11yDelegate falsingA11yDelegate) {};

        /** Reinitialize the location */
        default void updateSecurityViewLocation() {};

        /** Alter the ViewFlipper position, based upon a touch outside of it */
        default void updatePositionByTouchX(float x) {};

        /** A double tap on the container, outside of the ViewFlipper */
        default void handleDoubleTap(MotionEvent event) {};

        /** Called when the view needs to reset or hides */
        default void reset() {};

        /** Refresh colors */
        default void reloadColors() {};

        /** Handles density or font scale changes. */
        default void onDensityOrFontScaleChanged() {}

        /** On a successful auth, optionally handle how the view disappears */
        default void startDisappearAnimation(SecurityMode securityMode) {};

        /** On notif tap, this animation will run */
        default void startAppearAnimation(SecurityMode securityMode) {};

        /** Called when we are setting a new ViewMode */
        default void onDestroy() {};
    }

    /**
     * Base class for modes which support having on left/right side of the screen, used for large
     * screen devices
     */
    abstract static class SidedSecurityMode implements ViewMode {
        private KeyguardSecurityViewFlipper mViewFlipper;
        private ConstraintLayout mView;
        private GlobalSettings mGlobalSettings;
        private int mDefaultSideSetting;

        public void init(ConstraintLayout v, KeyguardSecurityViewFlipper viewFlipper,
                GlobalSettings globalSettings, boolean leftAlignedByDefault) {
            mView = v;
            mViewFlipper = viewFlipper;
            mGlobalSettings = globalSettings;
            mDefaultSideSetting =
                    leftAlignedByDefault ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
                            : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT;
        }

        /**
         * Determine if a double tap on this view is on the other side. If so, will animate
         * positions and record the preference to always show on this side.
         */
        @Override
        public void handleDoubleTap(MotionEvent event) {
            boolean currentlyLeftAligned = isLeftAligned();
            // Did the tap hit the "other" side of the bouncer?
            if (isTouchOnTheOtherSideOfSecurity(event, currentlyLeftAligned)) {
                boolean willBeLeftAligned = !currentlyLeftAligned;
                updateSideSetting(willBeLeftAligned);

                int keyguardState = willBeLeftAligned
                        ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT
                        : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT;
                SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState);

                updateSecurityViewLocation(willBeLeftAligned, /* animate= */ true);
            }
        }

        private boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev, boolean leftAligned) {
            float x = ev.getX();
            return (leftAligned && (x > mView.getWidth() / 2f))
                    || (!leftAligned && (x < mView.getWidth() / 2f));
        }

        public boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev) {
            return isTouchOnTheOtherSideOfSecurity(ev, isLeftAligned());
        }

        protected abstract void updateSecurityViewLocation(boolean leftAlign, boolean animate);

        boolean isLeftAligned() {
            return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
                    mDefaultSideSetting)
                    == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
        }

        protected void updateSideSetting(boolean leftAligned) {
            mGlobalSettings.putInt(
                    Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
                    leftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
                            : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT);
        }
    }

    /**
     * Default bouncer is centered within the space
     */
    static class DefaultViewMode implements ViewMode {
        private ConstraintLayout mView;
        private KeyguardSecurityViewFlipper mViewFlipper;

        @Override
        public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                @NonNull KeyguardSecurityViewFlipper viewFlipper,
                @NonNull FalsingManager falsingManager,
                @NonNull UserSwitcherController userSwitcherController,
                @NonNull FalsingA11yDelegate falsingA11yDelegate) {
            mView = v;
            mViewFlipper = viewFlipper;

            // Reset ViewGroup to default positions
            updateSecurityViewGroup();
        }

        private void updateSecurityViewGroup() {
            ConstraintSet constraintSet = new ConstraintSet();
            constraintSet.connect(mViewFlipper.getId(), START, PARENT_ID, START);
            constraintSet.connect(mViewFlipper.getId(), END, PARENT_ID, END);
            constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM);
            constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP);
            constraintSet.constrainHeight(mViewFlipper.getId(), MATCH_CONSTRAINT);
            constraintSet.constrainWidth(mViewFlipper.getId(), MATCH_CONSTRAINT);
            constraintSet.applyTo(mView);
        }
    }

    /**
     * User switcher mode will display both the current user icon as well as
     * a user switcher, in both portrait and landscape modes.
     */
    static class UserSwitcherViewMode extends SidedSecurityMode {
        private ConstraintLayout mView;
        private ViewGroup mUserSwitcherViewGroup;
        private KeyguardSecurityViewFlipper mViewFlipper;
        private TextView mUserSwitcher;
        private FalsingManager mFalsingManager;
        private UserSwitcherController mUserSwitcherController;
        private KeyguardUserSwitcherPopupMenu mPopup;
        private Resources mResources;
        private UserSwitcherController.UserSwitchCallback mUserSwitchCallback =
                this::setupUserSwitcher;

        private UserSwitcherCallback mUserSwitcherCallback;
        private FalsingA11yDelegate mFalsingA11yDelegate;

        UserSwitcherViewMode(UserSwitcherCallback userSwitcherCallback) {
            mUserSwitcherCallback = userSwitcherCallback;
        }

        @Override
        public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                @NonNull KeyguardSecurityViewFlipper viewFlipper,
                @NonNull FalsingManager falsingManager,
                @NonNull UserSwitcherController userSwitcherController,
                @NonNull FalsingA11yDelegate falsingA11yDelegate) {
            init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */false);
            mView = v;
            mViewFlipper = viewFlipper;
            mFalsingManager = falsingManager;
            mUserSwitcherController = userSwitcherController;
            mResources = v.getContext().getResources();
            mFalsingA11yDelegate = falsingA11yDelegate;

            if (mUserSwitcherViewGroup == null) {
                inflateUserSwitcher();
            }
            updateSecurityViewLocation();
            setupUserSwitcher();
            mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback);
        }

        @Override
        public void reset() {
            if (mPopup != null) {
                mPopup.dismiss();
                mPopup = null;
            }
            setupUserSwitcher();
        }

        @Override
        public void reloadColors() {
            TextView header =  (TextView) mView.findViewById(R.id.user_switcher_header);
            if (header != null) {
                header.setTextColor(Utils.getColorAttrDefaultColor(mView.getContext(),
                        android.R.attr.textColorPrimary));
                header.setBackground(mView.getContext().getDrawable(
                        R.drawable.bouncer_user_switcher_header_bg));
                Drawable keyDownDrawable =
                        ((LayerDrawable) header.getBackground().mutate()).findDrawableByLayerId(
                                R.id.user_switcher_key_down);
                keyDownDrawable.setTintList(Utils.getColorAttr(mView.getContext(),
                        android.R.attr.textColorPrimary));
            }
        }

        @Override
        public void onDensityOrFontScaleChanged() {
            mView.removeView(mUserSwitcherViewGroup);
            inflateUserSwitcher();
        }

        @Override
        public void onDestroy() {
            mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
        }

        private Drawable findLargeUserIcon(int userId) {
            Bitmap userIcon = UserManager.get(mView.getContext()).getUserIcon(userId);
            if (userIcon != null) {
                int iconSize =
                        mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_icon_size);
                return CircleFramedDrawable.getInstance(
                    mView.getContext(),
                    Icon.scaleDownIfNecessary(userIcon, iconSize, iconSize)
                );
            }

            return UserIcons.getDefaultUserIcon(mResources, userId, false);
        }

        @Override
        public void startAppearAnimation(SecurityMode securityMode) {
            // IME insets animations handle alpha and translation
            if (securityMode == SecurityMode.Password) {
                return;
            }

            mUserSwitcherViewGroup.setAlpha(0f);
            ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
            int yTrans = mView.getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
            animator.setInterpolator(Interpolators.STANDARD_DECELERATE);
            animator.setDuration(650);
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mUserSwitcherViewGroup.setAlpha(1f);
                    mUserSwitcherViewGroup.setTranslationY(0f);
                }
            });
            animator.addUpdateListener(animation -> {
                float value = (float) animation.getAnimatedValue();
                mUserSwitcherViewGroup.setAlpha(value);
                mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value);
            });
            animator.start();
        }

        @Override
        public void startDisappearAnimation(SecurityMode securityMode) {
            // IME insets animations handle alpha and translation
            if (securityMode == SecurityMode.Password) {
                return;
            }

            int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation);

            AnimatorSet anims = new AnimatorSet();
            ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation);
            ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
                    0f);

            anims.setInterpolator(Interpolators.STANDARD_ACCELERATE);
            anims.playTogether(alphaAnim, yAnim);
            anims.start();
        }

        private void setupUserSwitcher() {
            final UserRecord currentUser = mUserSwitcherController.getCurrentUserRecord();
            if (currentUser == null) {
                Log.e(TAG, "Current user in user switcher is null.");
                return;
            }
            final String currentUserName = mUserSwitcherController.getCurrentUserName();
            Drawable userIcon = findLargeUserIcon(currentUser.info.id);
            ((ImageView) mView.findViewById(R.id.user_icon)).setImageDrawable(userIcon);
            mUserSwitcher.setText(currentUserName);

            KeyguardUserSwitcherAnchor anchor = mView.findViewById(R.id.user_switcher_anchor);
            anchor.setAccessibilityDelegate(mFalsingA11yDelegate);

            BaseUserSwitcherAdapter adapter = new BaseUserSwitcherAdapter(mUserSwitcherController) {
                @Override
                public View getView(int position, View convertView, ViewGroup parent) {
                    UserRecord item = getItem(position);
                    FrameLayout view = (FrameLayout) convertView;
                    if (view == null) {
                        view = (FrameLayout) LayoutInflater.from(parent.getContext()).inflate(
                                R.layout.keyguard_bouncer_user_switcher_item,
                                parent,
                                false);
                    }
                    TextView textView = (TextView) view.getChildAt(0);
                    textView.setText(getName(parent.getContext(), item));
                    Drawable icon = null;
                    if (item.picture != null) {
                        icon = new BitmapDrawable(item.picture);
                    } else {
                        icon = getDrawable(item, view.getContext());
                    }
                    int iconSize = view.getResources().getDimensionPixelSize(
                            R.dimen.bouncer_user_switcher_item_icon_size);
                    int iconPadding = view.getResources().getDimensionPixelSize(
                            R.dimen.bouncer_user_switcher_item_icon_padding);
                    icon.setBounds(0, 0, iconSize, iconSize);
                    textView.setCompoundDrawablePadding(iconPadding);
                    textView.setCompoundDrawablesRelative(icon, null, null, null);

                    if (item == currentUser) {
                        textView.setBackground(view.getContext().getDrawable(
                                R.drawable.bouncer_user_switcher_item_selected_bg));
                    } else {
                        textView.setBackground(null);
                    }
                    textView.setSelected(item == currentUser);
                    view.setEnabled(item.isSwitchToEnabled);
                    UserSwitcherController.setSelectableAlpha(view);
                    return view;
                }

                private Drawable getDrawable(UserRecord item, Context context) {
                    Drawable drawable;
                    if (item.isCurrent && item.isGuest) {
                        drawable = context.getDrawable(R.drawable.ic_avatar_guest_user);
                    } else {
                        drawable = getIconDrawable(context, item);
                    }

                    int iconColor;
                    if (item.isSwitchToEnabled) {
                        iconColor = Utils.getColorAttrDefaultColor(context,
                                com.android.internal.R.attr.colorAccentPrimaryVariant);
                    } else {
                        iconColor = context.getResources().getColor(
                                R.color.kg_user_switcher_restricted_avatar_icon_color,
                                context.getTheme());
                    }
                    drawable.setTint(iconColor);

                    Drawable bg = context.getDrawable(
                            com.android.settingslib.R.drawable.user_avatar_bg);
                    bg.setTintBlendMode(BlendMode.DST);
                    bg.setTint(Utils.getColorAttrDefaultColor(context,
                                com.android.internal.R.attr.colorSurfaceVariant));
                    drawable = new LayerDrawable(new Drawable[]{bg, drawable});
                    return drawable;
                }
            };

            anchor.setOnClickListener((v) -> {
                if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
                mPopup = new KeyguardUserSwitcherPopupMenu(mView.getContext(), mFalsingManager);
                mPopup.setAnchorView(anchor);
                mPopup.setAdapter(adapter);
                mPopup.setOnItemClickListener((parent, view, pos, id) -> {
                    if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
                    if (!view.isEnabled()) return;
                    if (mPopup == null) return;
                    // Subtract one for the header
                    UserRecord user = adapter.getItem(pos - 1);
                    if (user.isManageUsers || user.isAddSupervisedUser) {
                        mUserSwitcherCallback.showUnlockToContinueMessage();
                    }
                    if (!user.isCurrent) {
                        adapter.onUserListItemClicked(user);
                    }
                    mPopup.dismiss();
                    mPopup = null;
                });
                mPopup.show();
            });
        }

        @Override
        public void updateSecurityViewLocation() {
            updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
        }

        public void updateSecurityViewLocation(boolean leftAlign, boolean animate) {
            if (animate) {
                TransitionManager.beginDelayedTransition(mView,
                        new KeyguardSecurityViewTransition());
            }
            int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
            int viewFlipperBottomMargin = mResources.getDimensionPixelSize(
                    R.dimen.bouncer_user_switcher_view_mode_view_flipper_bottom_margin);
            int userSwitcherBottomMargin = mResources.getDimensionPixelSize(
                    R.dimen.bouncer_user_switcher_view_mode_user_switcher_bottom_margin);
            if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                ConstraintSet constraintSet = new ConstraintSet();
                constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP, yTrans);
                constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, mViewFlipper.getId(),
                        TOP, userSwitcherBottomMargin);
                constraintSet.connect(mViewFlipper.getId(), TOP, mUserSwitcherViewGroup.getId(),
                        BOTTOM);
                constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM,
                        viewFlipperBottomMargin);
                constraintSet.centerHorizontally(mViewFlipper.getId(), PARENT_ID);
                constraintSet.centerHorizontally(mUserSwitcherViewGroup.getId(), PARENT_ID);
                constraintSet.setVerticalChainStyle(mViewFlipper.getId(), CHAIN_SPREAD);
                constraintSet.setVerticalChainStyle(mUserSwitcherViewGroup.getId(), CHAIN_SPREAD);
                constraintSet.constrainHeight(mUserSwitcherViewGroup.getId(), WRAP_CONTENT);
                constraintSet.constrainWidth(mUserSwitcherViewGroup.getId(), WRAP_CONTENT);
                constraintSet.constrainHeight(mViewFlipper.getId(), MATCH_CONSTRAINT);
                constraintSet.applyTo(mView);
            } else {
                int startElement =
                        leftAlign ? mViewFlipper.getId() : mUserSwitcherViewGroup.getId();
                int endElement =
                        leftAlign ? mUserSwitcherViewGroup.getId() : mViewFlipper.getId();

                ConstraintSet constraintSet = new ConstraintSet();
                constraintSet.connect(startElement, START, PARENT_ID, START);
                constraintSet.connect(startElement, END, endElement, START);
                constraintSet.connect(endElement, START, startElement, END);
                constraintSet.connect(endElement, END, PARENT_ID, END);
                constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP);
                constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, PARENT_ID, BOTTOM);
                constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP);
                constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM);
                constraintSet.setHorizontalChainStyle(mUserSwitcherViewGroup.getId(), CHAIN_SPREAD);
                constraintSet.setHorizontalChainStyle(mViewFlipper.getId(), CHAIN_SPREAD);
                constraintSet.constrainHeight(mUserSwitcherViewGroup.getId(),
                        MATCH_CONSTRAINT);
                constraintSet.constrainWidth(mUserSwitcherViewGroup.getId(),
                        MATCH_CONSTRAINT);
                constraintSet.constrainWidth(mViewFlipper.getId(), MATCH_CONSTRAINT);
                constraintSet.constrainHeight(mViewFlipper.getId(), MATCH_CONSTRAINT);
                constraintSet.applyTo(mView);
            }
        }

        private void inflateUserSwitcher() {
            LayoutInflater.from(mView.getContext()).inflate(
                    R.layout.keyguard_bouncer_user_switcher,
                    mView,
                    true);
            mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher);
            mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
        }

        interface UserSwitcherCallback {
            void showUnlockToContinueMessage();
        }
    }

    /**
     * Logic to enabled one-handed bouncer mode. Supports animating the bouncer
     * between alternate sides of the display.
     */
    static class OneHandedViewMode extends SidedSecurityMode {
        private ConstraintLayout mView;
        private KeyguardSecurityViewFlipper mViewFlipper;

        @Override
        public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                @NonNull KeyguardSecurityViewFlipper viewFlipper,
                @NonNull FalsingManager falsingManager,
                @NonNull UserSwitcherController userSwitcherController,
                @NonNull FalsingA11yDelegate falsingA11yDelegate) {
            init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */true);
            mView = v;
            mViewFlipper = viewFlipper;

            updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
        }

        /**
         * Moves the bouncer to align with a tap (most likely in the shade), so the bouncer
         * appears on the same side as a touch.
         */
        @Override
        public void updatePositionByTouchX(float x) {
            boolean isTouchOnLeft = x <= mView.getWidth() / 2f;
            updateSideSetting(isTouchOnLeft);
            updateSecurityViewLocation(isTouchOnLeft, /* animate= */false);
        }

        @Override
        public void updateSecurityViewLocation() {
            updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
        }

        protected void updateSecurityViewLocation(boolean leftAlign, boolean animate) {
            if (animate) {
                TransitionManager.beginDelayedTransition(mView,
                        new KeyguardSecurityViewTransition());
            }
            ConstraintSet constraintSet = new ConstraintSet();
            if (leftAlign) {
                constraintSet.connect(mViewFlipper.getId(), LEFT, PARENT_ID, LEFT);
            } else {
                constraintSet.connect(mViewFlipper.getId(), RIGHT, PARENT_ID, RIGHT);
            }
            constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP);
            constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM);
            constraintSet.constrainPercentWidth(mViewFlipper.getId(), 0.5f);
            constraintSet.applyTo(mView);
        }
    }
}
