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

import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;

import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MAX_VALUE;
import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MIN_VALUE;

import android.annotation.IntDef;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.MathUtils;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.SeekBar;

import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.server.accessibility.Flags;
import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView;
import com.android.systemui.res.R;
import com.android.systemui.util.settings.SecureSettings;

import com.google.android.material.materialswitch.MaterialSwitch;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;

/**
 * Class to set value about WindowManificationSettings.
 */
class WindowMagnificationSettings implements MagnificationGestureDetector.OnGestureListener {
    private static final String TAG = "WindowMagnificationSettings";
    private final Context mContext;
    private final WindowManager mWindowManager;
    private final SecureSettings mSecureSettings;

    private final Runnable mWindowInsetChangeRunnable;
    private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;

    @VisibleForTesting
    final LayoutParams mParams;
    @VisibleForTesting
    final Rect mDraggableWindowBounds = new Rect();
    private boolean mIsVisible = false;
    private final MagnificationGestureDetector mGestureDetector;
    private boolean mSingleTapDetected = false;

    private SeekBarWithIconButtonsView mZoomSeekbar;
    private LinearLayout mAllowDiagonalScrollingView;
    private MaterialSwitch mAllowDiagonalScrollingSwitch;
    private LinearLayout mMagnifyKeyboardView;
    private MaterialSwitch mMagnifyKeyboardSwitch;
    private LinearLayout mMagnifyTypingView;
    private MaterialSwitch mMagnifyTypingSwitch;
    private LinearLayout mSettingView;
    private ImageButton mSmallButton;
    private ImageButton mMediumButton;
    private ImageButton mLargeButton;
    private Button mDoneButton;
    private Button mEditButton;
    private ImageButton mFullScreenButton;
    private int mLastSelectedButtonIndex = MagnificationSize.DEFAULT;
    private boolean mAllowDiagonalScrolling = false;

    /**
     * Amount by which magnification scale changes compared to seekbar in settings.
     * magnitude = 10 means, for every 1 scale increase, 10 progress increase in seekbar.
     */
    private int mSeekBarMagnitude;
    private float mScale = SCALE_MIN_VALUE;

    private WindowMagnificationSettingsCallback mCallback;

    @VisibleForTesting
    ContentObserver mMagnificationCapabilityObserver;
    @VisibleForTesting
    ContentObserver mMagnifyTypingObserver;
    @VisibleForTesting
    ContentObserver mMagnifyKeyboardObserver;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            MagnificationSize.CUSTOM,
            MagnificationSize.SMALL,
            MagnificationSize.MEDIUM,
            MagnificationSize.LARGE,
            MagnificationSize.FULLSCREEN,
            MagnificationSize.DEFAULT
    })
    /** Denotes the Magnification size type. */
    public @interface MagnificationSize {
        int CUSTOM = 0;
        int SMALL = 1;
        int MEDIUM = 2;
        int LARGE = 3;
        int FULLSCREEN = 4;
        int DEFAULT = MEDIUM;
    }

    @VisibleForTesting
    WindowMagnificationSettings(Context context, WindowMagnificationSettingsCallback callback,
            SfVsyncFrameCallbackProvider sfVsyncFrameProvider, SecureSettings secureSettings,
            WindowManager windowManager) {
        mContext = context;
        mWindowManager = windowManager;
        mSfVsyncFrameProvider = sfVsyncFrameProvider;
        mCallback = callback;
        mSecureSettings = secureSettings;

        mAllowDiagonalScrolling = mSecureSettings.getIntForUser(
                Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 1,
                UserHandle.USER_CURRENT) == 1;

        mParams = createLayoutParams(context);
        mWindowInsetChangeRunnable = this::onWindowInsetChanged;

        inflateView();

        mGestureDetector = new MagnificationGestureDetector(context,
                context.getMainThreadHandler(), this);

        mMagnificationCapabilityObserver = new ContentObserver(
                mContext.getMainThreadHandler()) {
            @Override
            public void onChange(boolean selfChange) {
                mSettingView.post(() -> {
                    updateUIControlsIfNeeded();
                });
            }
        };

        if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
            mMagnifyTypingObserver = new ContentObserver(
                    mContext.getMainThreadHandler()) {
                @Override
                public void onChange(boolean selfChange) {
                    mSettingView.post(() -> {
                        updateMagnifyTypingUI();
                    });
                }
            };

            mMagnifyKeyboardObserver = new ContentObserver(
                    mContext.getMainThreadHandler()) {
                @Override
                public void onChange(boolean selfChange) {
                    mSettingView.post(() -> {
                        updateMagnifyKeyboardUI();
                    });
                }
            };
        }
    }

    private class ZoomSeekbarChangeListener implements
            SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            // Notify the service to update the magnifier scale only when the progress changed is
            // triggered by user interaction on seekbar.
            if (!fromUser) {
                return;
            }
            final float scale = transformProgressToScale(progress);
            // We don't need to update the persisted scale when the seekbar progress is
            // changing. The update should be triggered when the changing is ended.
            mCallback.onMagnifierScale(scale, /* updatePersistence= */ false);
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            // Do nothing
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            // Do nothing
        }

        @Override
        public void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control) {
            // Update the Settings persisted scale only when user interaction with seekbar ends.
            final int progress = seekBar.getProgress();
            final float scale = transformProgressToScale(progress);
            mCallback.onMagnifierScale(scale, /* updatePersistence= */ true);
        }

        private float transformProgressToScale(float progress) {
            return (progress / (float) mSeekBarMagnitude) + SCALE_MIN_VALUE;
        }
    }

    private final AccessibilityDelegate mPanelDelegate = new AccessibilityDelegate() {
        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
            super.onInitializeAccessibilityNodeInfo(host, info);

            info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up,
                    mContext.getString(R.string.accessibility_control_move_up)));
            info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down,
                    mContext.getString(R.string.accessibility_control_move_down)));
            info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left,
                    mContext.getString(R.string.accessibility_control_move_left)));
            info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right,
                    mContext.getString(R.string.accessibility_control_move_right)));
        }

        @Override
        public boolean performAccessibilityAction(View host, int action, Bundle args) {
            if (performA11yAction(host, action)) {
                return true;
            }
            return super.performAccessibilityAction(host, action, args);
        }

        private boolean performA11yAction(View view, int action) {
            final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
            if (action == R.id.accessibility_action_move_up) {
                moveButton(0, -windowBounds.height());
            } else if (action == R.id.accessibility_action_move_down) {
                moveButton(0, windowBounds.height());
            } else if (action == R.id.accessibility_action_move_left) {
                moveButton(-windowBounds.width(), 0);
            } else if (action == R.id.accessibility_action_move_right) {
                moveButton(windowBounds.width(), 0);
            } else {
                return false;
            }
            return true;
        }
    };

    private boolean onTouch(View v, MotionEvent event) {
        if (!mIsVisible) {
            return false;
        }
        return mGestureDetector.onTouch(v, event);
    }

    private View.OnClickListener mButtonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            int id = view.getId();
            if (id == R.id.magnifier_small_button) {
                setMagnifierSize(MagnificationSize.SMALL);
            } else if (id == R.id.magnifier_medium_button) {
                setMagnifierSize(MagnificationSize.MEDIUM);
            } else if (id == R.id.magnifier_large_button) {
                setMagnifierSize(MagnificationSize.LARGE);
            } else if (id == R.id.magnifier_full_button) {
                setMagnifierSize(MagnificationSize.FULLSCREEN);
            } else if (id == R.id.magnifier_edit_button) {
                editMagnifierSizeMode(true);
            } else if (id == R.id.magnifier_done_button) {
                hideSettingPanel();
            }
        }
    };

    @Override
    public boolean onSingleTap(View view) {
        mSingleTapDetected = true;
        return true;
    }

    @Override
    public boolean onDrag(View v, int offsetX, int offsetY) {
        moveButton(offsetX, offsetY);
        return true;
    }

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

    @Override
    public boolean onFinish() {
        if (!mSingleTapDetected) {
            showSettingPanel();
        }
        mSingleTapDetected = false;
        return true;
    }

    @VisibleForTesting
    public ViewGroup getSettingView() {
        return mSettingView;
    }

    private void moveButton(int offsetX, int offsetY) {
        mSfVsyncFrameProvider.postFrameCallback(l -> {
            mParams.x += offsetX;
            mParams.y += offsetY;
            updateButtonViewLayoutIfNeeded();
        });
    }

    public void hideSettingPanel() {
        hideSettingPanel(true);
    }

    public void hideSettingPanel(boolean resetPosition) {
        if (!mIsVisible) {
            return;
        }

        // Unregister observer before removing view
        mSecureSettings.unregisterContentObserverSync(mMagnificationCapabilityObserver);
        if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
            mSecureSettings.unregisterContentObserverSync(mMagnifyTypingObserver);
            mSecureSettings.unregisterContentObserverSync(mMagnifyKeyboardObserver);
        }
        mWindowManager.removeView(mSettingView);
        mIsVisible = false;
        if (resetPosition) {
            mParams.x = 0;
            mParams.y = 0;
        }

        mContext.unregisterReceiver(mScreenOffReceiver);
        mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false);
    }

    public void toggleSettingsPanelVisibility() {
        if (!mIsVisible) {
            showSettingPanel();
        } else {
            hideSettingPanel();
        }
    }

    public void showSettingPanel() {
        showSettingPanel(true);
    }

    public boolean isSettingPanelShowing() {
        return mIsVisible;
    }

    public void setScaleSeekbar(float scale) {
        int index = (int) ((scale - SCALE_MIN_VALUE) * mSeekBarMagnitude);
        if (index < 0) {
            index = 0;
        } else if (index > mZoomSeekbar.getMax()) {
            index = mZoomSeekbar.getMax();
        }
        mZoomSeekbar.setProgress(index);
    }

    private void transitToMagnificationMode(int mode) {
        mCallback.onModeSwitch(mode);
    }

    /**
     * Shows the panel for magnification settings.
     * When the panel is going to be visible by calling this method, the layout position can be
     * reset depending on the flag.
     *
     * @param resetPosition if the panel position needs to be reset
     */
    private void showSettingPanel(boolean resetPosition) {
        if (!mIsVisible) {
            updateUIControlsIfNeeded();
            updatePanelSize(mContext);
            if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
                updateMagnifyTypingUI();
                updateMagnifyKeyboardUI();
            }
            setScaleSeekbar(getMagnificationScale());
            if (resetPosition) {
                mDraggableWindowBounds.set(getDraggableWindowBounds());
                mParams.x = mDraggableWindowBounds.right;
                mParams.y = mDraggableWindowBounds.bottom;
            }

            mWindowManager.addView(mSettingView, mParams);

            mSecureSettings.registerContentObserverForUserSync(
                    Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
                    mMagnificationCapabilityObserver,
                    UserHandle.USER_CURRENT);

            if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
                mSecureSettings.registerContentObserverForUserSync(
                        Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED,
                        mMagnifyTypingObserver,
                        UserHandle.USER_CURRENT);

                mSecureSettings.registerContentObserverForUserSync(
                        Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME,
                        mMagnifyKeyboardObserver,
                        UserHandle.USER_CURRENT);
            }

            // Exclude magnification switch button from system gesture area.
            setSystemGestureExclusion();
            mIsVisible = true;
            mCallback.onSettingsPanelVisibilityChanged(/* shown= */ true);
        }
        mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
    }

    private int getMagnificationMode() {
        // If current capability is window mode, we would like the default value of the mode to
        // be WINDOW, otherwise, the default value would be FULLSCREEN.
        int defaultValue =
                (getMagnificationCapability() == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)
                        ? ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW
                        : ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;

        return mSecureSettings.getIntForUser(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
                defaultValue,
                UserHandle.USER_CURRENT);
    }

    private int getMagnificationCapability() {
        return mSecureSettings.getIntForUser(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
                ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
                UserHandle.USER_CURRENT);
    }

    @VisibleForTesting
    boolean isDiagonalScrollingEnabled() {
        return mAllowDiagonalScrolling;
    }

    @VisibleForTesting
    boolean isMagnifyTypingEnabled() {
        return mSecureSettings.getIntForUser(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED,
                1, UserHandle.USER_CURRENT) == 1;
    }

    @VisibleForTesting
    boolean isMagnifyKeyboardEnabled() {
        return mSecureSettings.getIntForUser(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME,
                AccessibilityUtils.getMagnificationMagnifyKeyboardDefaultValue(mContext),
                UserHandle.USER_CURRENT) == 1;
    }

    /**
     * Only called from outside to notify the controlling magnifier scale changed
     *
     * @param scale The new controlling magnifier scale
     */
    public void setMagnificationScale(float scale) {
        mScale = scale;

        if (isSettingPanelShowing()) {
            setScaleSeekbar(scale);
        }
    }

    private float getMagnificationScale() {
        return mScale;
    }

    private void updateUIControlsIfNeeded() {
        int capability = getMagnificationCapability();
        int selectedButtonIndex = mLastSelectedButtonIndex;
        WindowMagnificationFrameSizePrefs windowMagnificationFrameSizePrefs =
                new WindowMagnificationFrameSizePrefs(mContext);
        switch (capability) {
            case ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW:
                mEditButton.setVisibility(View.VISIBLE);
                mAllowDiagonalScrollingView.setVisibility(View.VISIBLE);
                mFullScreenButton.setVisibility(View.GONE);
                if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
                    mMagnifyTypingView.setVisibility(View.VISIBLE);
                    mMagnifyKeyboardView.setVisibility(View.GONE);
                }
                if (selectedButtonIndex == MagnificationSize.FULLSCREEN) {
                    selectedButtonIndex =
                            windowMagnificationFrameSizePrefs.getIndexForCurrentDensity();
                }
                break;

            case ACCESSIBILITY_MAGNIFICATION_MODE_ALL:
                int mode = getMagnificationMode();
                mFullScreenButton.setVisibility(View.VISIBLE);
                if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) {
                    // set the edit button visibility to View.INVISIBLE to keep the height, to
                    // prevent the size title from too close to the size buttons
                    mEditButton.setVisibility(View.INVISIBLE);
                    mAllowDiagonalScrollingView.setVisibility(View.GONE);
                    if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
                        mMagnifyTypingView.setVisibility(View.VISIBLE);
                        mMagnifyKeyboardView.setVisibility(View.VISIBLE);
                    }
                    // force the fullscreen button showing
                    selectedButtonIndex = MagnificationSize.FULLSCREEN;
                } else { // mode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW
                    mEditButton.setVisibility(View.VISIBLE);
                    mAllowDiagonalScrollingView.setVisibility(View.VISIBLE);
                    if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
                        mMagnifyTypingView.setVisibility(View.VISIBLE);
                        mMagnifyKeyboardView.setVisibility(View.GONE);
                    }
                    selectedButtonIndex =
                            windowMagnificationFrameSizePrefs.getIndexForCurrentDensity();
                }
                break;

            case ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN:
                // We will never fall into this case since we never show settings panel when
                // capability equals to ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN.
                // Currently, the case follows the UI controls when capability equals to
                // ACCESSIBILITY_MAGNIFICATION_MODE_ALL and mode equals to
                // ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, but we could also consider to
                // remove the whole icon button selections int the future since they are no use
                // for fullscreen only capability.

                mFullScreenButton.setVisibility(View.VISIBLE);
                // set the edit button visibility to View.INVISIBLE to keep the height, to
                // prevent the size title from too close to the size buttons
                mEditButton.setVisibility(View.INVISIBLE);
                mAllowDiagonalScrollingView.setVisibility(View.GONE);
                // force the fullscreen button showing
                selectedButtonIndex = MagnificationSize.FULLSCREEN;
                break;

            default:
                break;
        }

        updateSelectedButton(selectedButtonIndex);
        mSettingView.requestLayout();
    }

    private void updateMagnifyTypingUI() {
        mMagnifyTypingSwitch.setChecked(isMagnifyTypingEnabled());
        mSettingView.requestLayout();
    }

    private void updateMagnifyKeyboardUI() {
        mMagnifyKeyboardSwitch.setChecked(isMagnifyKeyboardEnabled());
        mSettingView.requestLayout();
    }

    private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            hideSettingPanel();
        }
    };

    void inflateView() {
        mSettingView = (LinearLayout) View.inflate(mContext,
                R.layout.window_magnification_settings_view, null);

        mSettingView.setFocusable(true);
        mSettingView.setFocusableInTouchMode(true);
        mSettingView.setOnTouchListener(this::onTouch);

        mSettingView.setAccessibilityDelegate(mPanelDelegate);

        mSmallButton = mSettingView.findViewById(R.id.magnifier_small_button);
        mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button);
        mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button);
        mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button);
        mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button);
        mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button);

        mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider);
        mZoomSeekbar.setMax((int) (mZoomSeekbar.getChangeMagnitude()
                * (SCALE_MAX_VALUE - SCALE_MIN_VALUE)));
        mSeekBarMagnitude = mZoomSeekbar.getChangeMagnitude();
        setScaleSeekbar(mScale);
        mZoomSeekbar.setOnSeekBarWithIconButtonsChangeListener(new ZoomSeekbarChangeListener());

        mAllowDiagonalScrollingView =
                (LinearLayout) mSettingView.findViewById(R.id.magnifier_horizontal_lock_view);
        mAllowDiagonalScrollingSwitch =
                (MaterialSwitch) mSettingView.findViewById(R.id.magnifier_horizontal_lock_switch);
        mAllowDiagonalScrollingSwitch.setChecked(mAllowDiagonalScrolling);
        mAllowDiagonalScrollingSwitch.setOnCheckedChangeListener(
                (view, isChecked) -> setDiagonalScrolling(isChecked));

        if (Flags.enableMagnificationMagnifyNavBarAndIme()) {
            mMagnifyTypingView =
                    (LinearLayout) mSettingView.findViewById(R.id.magnifier_typing_view);
            mMagnifyTypingSwitch =
                    (MaterialSwitch) mSettingView.findViewById(R.id.magnifier_typing_switch);
            mMagnifyTypingSwitch.setChecked(isMagnifyTypingEnabled());
            mMagnifyTypingSwitch.setOnCheckedChangeListener(
                    (view, isChecked) -> setMagnifyTyping(isChecked));

            mMagnifyKeyboardView =
                    (LinearLayout) mSettingView.findViewById(R.id.magnifier_keyboard_view);
            mMagnifyKeyboardSwitch =
                    (MaterialSwitch) mSettingView.findViewById(R.id.magnifier_keyboard_switch);
            mMagnifyKeyboardSwitch.setChecked(isMagnifyKeyboardEnabled());
            mMagnifyKeyboardSwitch.setOnCheckedChangeListener(
                    (view, isChecked) -> setMagnifyKeyboard(isChecked));
        }

        mSmallButton.setOnClickListener(mButtonClickListener);
        mMediumButton.setOnClickListener(mButtonClickListener);
        mLargeButton.setOnClickListener(mButtonClickListener);
        mDoneButton.setOnClickListener(mButtonClickListener);
        mFullScreenButton.setOnClickListener(mButtonClickListener);
        mEditButton.setOnClickListener(mButtonClickListener);

        mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
            // Adds a pending post check to avoiding redundant calculation because this callback
            // is sent frequently when the switch icon window dragged by the users.
            if (mSettingView.isAttachedToWindow()
                    && !mSettingView.getHandler().hasCallbacks(mWindowInsetChangeRunnable)) {
                mSettingView.getHandler().post(mWindowInsetChangeRunnable);
            }
            return v.onApplyWindowInsets(insets);
        });

        updateSelectedButton(mLastSelectedButtonIndex);
    }

    void onConfigurationChanged(int configDiff) {
        if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0
                || (configDiff & ActivityInfo.CONFIG_ASSETS_PATHS) != 0
                || (configDiff & ActivityInfo.CONFIG_FONT_SCALE) != 0
                || (configDiff & ActivityInfo.CONFIG_LOCALE) != 0
                || (configDiff & ActivityInfo.CONFIG_DENSITY) != 0
                || (configDiff & ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT) != 0) {
            // We listen to following config changes to trigger layout inflation:
            // CONFIG_UI_MODE: theme change
            // CONFIG_ASSETS_PATHS: wallpaper change
            // CONFIG_FONT_SCALE: font size change
            // CONFIG_LOCALE: language change
            // CONFIG_DENSITY: display size change
            // CONFIG_FONT_WEIGHT_ADJUSTMENT: bold text setting change
            mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext);

            boolean showSettingPanelAfterConfigChange = mIsVisible;
            hideSettingPanel(/* resetPosition= */ false);
            inflateView();
            if (showSettingPanelAfterConfigChange) {
                showSettingPanel(/* resetPosition= */ false);
            }
            return;
        }

        if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0
                || (configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
            // update the panel size to fit in the new screen size
            updatePanelSize(mContext);
            mDraggableWindowBounds.set(getDraggableWindowBounds());
            // reset the panel position to the right-bottom corner
            mParams.x = mDraggableWindowBounds.right;
            mParams.y = mDraggableWindowBounds.bottom;
            updateButtonViewLayoutIfNeeded();
        }
    }

    private void onWindowInsetChanged() {
        final Rect newBounds = getDraggableWindowBounds();
        if (mDraggableWindowBounds.equals(newBounds)) {
            return;
        }
        mDraggableWindowBounds.set(newBounds);
    }

    @VisibleForTesting
    void updateButtonViewLayoutIfNeeded() {
        if (mIsVisible) {
            mParams.x = MathUtils.constrain(mParams.x, mDraggableWindowBounds.left,
                    mDraggableWindowBounds.right);
            mParams.y = MathUtils.constrain(mParams.y, mDraggableWindowBounds.top,
                    mDraggableWindowBounds.bottom);
            mWindowManager.updateViewLayout(mSettingView, mParams);
        }
    }

    public void editMagnifierSizeMode(boolean enable) {
        setEditMagnifierSizeMode(enable);
        updateSelectedButton(MagnificationSize.CUSTOM);
        hideSettingPanel();
    }

    private void setMagnifierSize(@MagnificationSize int index) {
        if (index == MagnificationSize.FULLSCREEN) {
            // transit to fullscreen magnifier if needed
            transitToMagnificationMode(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
        } else if (index != MagnificationSize.CUSTOM) {
            // update the window magnifier size
            mCallback.onSetMagnifierSize(index);
            // transit to window magnifier if needed
            transitToMagnificationMode(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
        } else {
            return;
        }

        updateSelectedButton(index);
    }

    private void setDiagonalScrolling(boolean enabled) {
        mSecureSettings.putIntForUser(
                Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, enabled ? 1 : 0,
                UserHandle.USER_CURRENT);

        mCallback.onSetDiagonalScrolling(enabled);
    }

    private void setMagnifyTyping(boolean enabled) {
        mSecureSettings.putIntForUser(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED, enabled ? 1 : 0,
                UserHandle.USER_CURRENT);

        mCallback.onSetMagnifyTyping(enabled);
    }

    private void setMagnifyKeyboard(boolean enabled) {
        mSecureSettings.putIntForUser(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME, enabled ? 1 : 0,
                UserHandle.USER_CURRENT);

        mCallback.onSetMagnifyKeyboard(enabled);
    }

    private void setEditMagnifierSizeMode(boolean enable) {
        mCallback.onEditMagnifierSizeMode(enable);
    }

    private void updatePanelSize(Context context) {
        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();

        // The magnification settings panel width is limited to the minimum of
        //     1. display width
        //     2. panel done button width + left and right padding
        // So we can directly calculate the proper panel width at runtime
        int displayWidth = windowMetrics.getBounds().width();
        int contentWidth = context.getResources()
                .getDimensionPixelSize(R.dimen.magnification_setting_button_done_width);
        int padding = context.getResources()
                .getDimensionPixelSize(R.dimen.magnification_setting_background_padding);
        mParams.width = Math.min(displayWidth, contentWidth + 2 * padding);

        // The magnification settings panel's max height is
        //   display height - (insets height for system bar / cutout)
        Rect windowBounds =  new Rect(windowMetrics.getBounds());
        Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
        windowBounds.inset(windowInsets);
        int availableHeight = windowBounds.height();
        // Re-measure the view with the new screen height constraint
        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
                mParams.width, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
                availableHeight, View.MeasureSpec.AT_MOST);
        mSettingView.measure(widthMeasureSpec, heightMeasureSpec);
        // Use the newly calculated height as the params height
        mParams.height = mSettingView.getMeasuredHeight();
    }

    private LayoutParams createLayoutParams(Context context) {
        final LayoutParams params = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT,
                LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
                LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSPARENT);
        params.gravity = Gravity.TOP | Gravity.START;
        params.accessibilityTitle = getAccessibilityWindowTitle(context);
        params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
        return params;
    }

    private Rect getDraggableWindowBounds() {
        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
        final Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
        final Rect boundRect = new Rect(windowMetrics.getBounds());
        boundRect.offsetTo(0, 0);
        // inset with the currently decided panel size
        boundRect.inset(0, 0, mParams.width, mParams.height);
        boundRect.inset(windowInsets);
        return boundRect;
    }

    private static String getAccessibilityWindowTitle(Context context) {
        return context.getString(com.android.internal.R.string.android_system_label);
    }

    private void setSystemGestureExclusion() {
        mSettingView.post(() -> {
            mSettingView.setSystemGestureExclusionRects(
                    Collections.singletonList(
                            new Rect(0, 0, mSettingView.getWidth(), mSettingView.getHeight())));
        });
    }

    void updateSelectedButton(@MagnificationSize int index) {
        // Clear the state of last selected button
        if (mLastSelectedButtonIndex == MagnificationSize.SMALL) {
            mSmallButton.setSelected(false);
        } else if (mLastSelectedButtonIndex == MagnificationSize.MEDIUM) {
            mMediumButton.setSelected(false);
        } else if (mLastSelectedButtonIndex == MagnificationSize.LARGE) {
            mLargeButton.setSelected(false);
        } else if (mLastSelectedButtonIndex == MagnificationSize.FULLSCREEN) {
            mFullScreenButton.setSelected(false);
        }

        // Set the state for selected button
        if (index == MagnificationSize.SMALL) {
            mSmallButton.setSelected(true);
        } else if (index == MagnificationSize.MEDIUM) {
            mMediumButton.setSelected(true);
        } else if (index == MagnificationSize.LARGE) {
            mLargeButton.setSelected(true);
        } else if (index == MagnificationSize.FULLSCREEN) {
            mFullScreenButton.setSelected(true);
        }

        mLastSelectedButtonIndex = index;
    }
}
