chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/menu_button/MenuButtonCoordinator.java

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.toolbar.menu_button;

import static android.view.View.LAYOUT_DIRECTION_RTL;

import android.animation.Animator;
import android.app.Activity;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Build.VERSION;
import android.view.View;
import android.view.View.OnKeyListener;

import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.TooltipCompat;

import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.theme.ThemeColorProvider;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.menu_button.MenuButtonProperties.ShowBadgeProperty;
import org.chromium.chrome.browser.toolbar.menu_button.MenuButtonProperties.ThemeProperty;
import org.chromium.chrome.browser.ui.appmenu.AppMenuButtonHelper;
import org.chromium.chrome.browser.ui.appmenu.AppMenuCoordinator;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

/**
 * Root component for the app menu button on the toolbar. Owns the MenuButton view and handles
 * changes to its visual state, e.g. showing/hiding the app update badge.
 */
public class MenuButtonCoordinator {
    public interface SetFocusFunction {
        void setFocus(boolean focus, int reason);
    }

    private final Activity mActivity;
    private final PropertyModel mPropertyModel;
    private MenuButtonMediator mMediator;
    private AppMenuButtonHelper mAppMenuButtonHelper;
    private MenuButton mMenuButton;
    private PropertyModelChangeProcessor mChangeProcessor;

    /**
     * @param appMenuCoordinatorSupplier Supplier for the AppMenuCoordinator, which owns all other
     *     app menu MVC components.
     * @param controlsVisibilityDelegate Delegate for forcing persistent display of browser
     *     controls.
     * @param windowAndroid The WindowAndroid instance.
     * @param setUrlBarFocusFunction Function that allows setting focus on the url bar.
     * @param requestRenderRunnable Runnable that requests a re-rendering of the compositor view
     *     containing the app menu button.
     * @param canShowAppUpdateBadge Whether the app menu update badge can be shown if there is a
     *     pending update.
     * @param isInOverviewModeSupplier Supplier of overview mode state.
     * @param themeColorProvider Provider of theme color changes.
     * @param menuButtonStateSupplier Supplier of the menu button state.
     * @param onMenuButtonClicked Runnable to run on menu button click.
     * @param menuButtonId Resource id that should be used to locate the underlying view.
     */
    public MenuButtonCoordinator(
            OneshotSupplier<AppMenuCoordinator> appMenuCoordinatorSupplier,
            BrowserStateBrowserControlsVisibilityDelegate controlsVisibilityDelegate,
            WindowAndroid windowAndroid,
            SetFocusFunction setUrlBarFocusFunction,
            Runnable requestRenderRunnable,
            boolean canShowAppUpdateBadge,
            Supplier<Boolean> isInOverviewModeSupplier,
            ThemeColorProvider themeColorProvider,
            Supplier<MenuButtonState> menuButtonStateSupplier,
            Runnable onMenuButtonClicked,
            @IdRes int menuButtonId) {
        mActivity = windowAndroid.getActivity().get();
        mMenuButton = mActivity.findViewById(menuButtonId);
        mPropertyModel =
                new PropertyModel.Builder(MenuButtonProperties.ALL_KEYS)
                        .with(
                                MenuButtonProperties.SHOW_UPDATE_BADGE,
                                new ShowBadgeProperty(false, false))
                        .with(
                                MenuButtonProperties.THEME,
                                new ThemeProperty(
                                        themeColorProvider.getTint(),
                                        themeColorProvider.getBrandedColorScheme()))
                        .with(MenuButtonProperties.IS_VISIBLE, true)
                        .with(MenuButtonProperties.STATE_SUPPLIER, menuButtonStateSupplier)
                        .build();
        mMediator =
                new MenuButtonMediator(
                        mPropertyModel,
                        canShowAppUpdateBadge,
                        () -> mActivity.isFinishing() || mActivity.isDestroyed(),
                        requestRenderRunnable,
                        themeColorProvider,
                        isInOverviewModeSupplier,
                        controlsVisibilityDelegate,
                        setUrlBarFocusFunction,
                        appMenuCoordinatorSupplier,
                        windowAndroid,
                        menuButtonStateSupplier,
                        onMenuButtonClicked);
        mMediator
                .getMenuButtonHelperSupplier()
                .addObserver((helper) -> mAppMenuButtonHelper = helper);
        if (mMenuButton != null) {
            mChangeProcessor =
                    PropertyModelChangeProcessor.create(
                            mPropertyModel, mMenuButton, new MenuButtonViewBinder());

            // Set tooltip text for menu button.
            if (VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                TooltipCompat.setTooltipText(
                        mMenuButton,
                        mActivity
                                .getResources()
                                .getString(R.string.accessibility_toolbar_btn_menu));
            }
        }
    }

    /**
     * Update the state of AppMenu components that need to know if the current page is loading, e.g.
     * the stop/reload button.
     * @param isLoading Whether the current page is loading.
     */
    public void updateReloadingState(boolean isLoading) {
        if (mMediator == null) return;
        mMediator.updateReloadingState(isLoading);
    }

    /** Disables the menu button, removing it from the view hierarchy and destroying it. */
    public void disableMenuButton() {
        if (mMenuButton != null) {
            UiUtils.removeViewFromParent(mMenuButton);
            destroy();
        }
    }

    /**
     * Set the underlying MenuButton view. Use only if the MenuButton instance isn't available at
     * construction time, e.g. if it's lazily inflated. This should only be called once, unless
     * switching the active toolbar.
     * @param menuButton The underlying MenuButton view.
     */
    public void setMenuButton(MenuButton menuButton) {
        assert menuButton != null;
        mMenuButton = menuButton;

        if (mChangeProcessor != null) {
            mChangeProcessor.destroy();
        }
        mChangeProcessor =
                PropertyModelChangeProcessor.create(
                        mPropertyModel, menuButton, new MenuButtonViewBinder());
    }

    /**
     * Handle the key press event on the menu button.
     * @return Whether the app menu was shown as a result of this action.
     */
    public boolean onEnterKeyPress() {
        if (mAppMenuButtonHelper == null || mMenuButton == null) return false;
        return mAppMenuButtonHelper.onEnterKeyPress(mMenuButton.getImageButton());
    }

    /**
     * @return Whether the menu button is present and visible.
     */
    public boolean isVisible() {
        return mMenuButton != null && mMenuButton.getVisibility() == View.VISIBLE;
    }

    /**
     * Get the underlying MenuButton view. Present for legacy reasons only; don't add new usages.
     */
    @Deprecated
    public MenuButton getMenuButton() {
        return mMenuButton;
    }

    /**
     * @param isClickable Whether the underlying MenuButton view should be clickable.
     */
    public void setClickable(boolean isClickable) {
        if (mMediator == null) return;
        mMediator.setClickable(isClickable);
    }

    /**
     * Sets the on key listener for the underlying menu button.
     * @param onKeyListener Listener for key events.
     */
    public void setOnKeyListener(OnKeyListener onKeyListener) {
        if (mMenuButton == null) return;
        mMenuButton.setOnKeyListener(onKeyListener);
    }

    public void destroy() {
        if (mMediator != null) {
            mMediator.destroy();
            mMediator = null;
        }

        if (mChangeProcessor != null) {
            mChangeProcessor.destroy();
            mChangeProcessor = null;
        }

        mMenuButton = null;
        mAppMenuButtonHelper = null;
    }

    /** @return Observer for menu state change. */
    public @Nullable Runnable getStateObserver() {
        return mMediator != null ? mMediator::updateStateChanged : null;
    }

    @Nullable
    public ObservableSupplier<AppMenuButtonHelper> getMenuButtonHelperSupplier() {
        if (mMediator == null) return null;
        return mMediator.getMenuButtonHelperSupplier();
    }

    /**
     * Set the visibility of the MenuButton controlled by this coordinator.
     *
     * @param visible Visibility state, true for visible and false for hidden.
     */
    public void setVisibility(boolean visible) {
        if (mMediator == null) return;
        mMediator.setVisibility(visible);
    }

    /**
     * Draws the current visual state of this component for the purposes of rendering the tab
     * switcher animation, setting the alpha to fade the view by the appropriate amount.
     * @param root Root view for the menu button; used to position the canvas that's drawn on.
     * @param canvas Canvas to draw to.
     * @param alpha Integer (0-255) alpha level to draw at.
     */
    public void drawTabSwitcherAnimationOverlay(View root, Canvas canvas, int alpha) {
        canvas.save();
        ViewUtils.translateCanvasToView(root, mMenuButton, canvas);
        mMenuButton.drawTabSwitcherAnimationOverlay(canvas, alpha);
        canvas.restore();
    }

    /**
     * Creates an animator for the MenuButton during the process offocusing or unfocusing the
     * UrlBar. The animation translate and fades the button into/out of view.
     * @return The Animator object for the MenuButton.
     * @param isFocusingUrl Whether the animation is for focusing the URL, meaning the button is
     *         fading out of view, or un-focusing, meaning it's fading into view.
     */
    public Animator getUrlFocusingAnimator(boolean isFocusingUrl) {
        return mMediator.getUrlFocusingAnimator(
                isFocusingUrl,
                mMenuButton != null && mMenuButton.getLayoutDirection() == LAYOUT_DIRECTION_RTL);
    }

    /** Returns whether the menu button is currently showing an update badge. */
    public boolean isShowingUpdateBadge() {
        return mPropertyModel.get(MenuButtonProperties.SHOW_UPDATE_BADGE).mShowUpdateBadge;
    }
}