chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToggleTabStackButtonCoordinator.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.top;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.CallbackController;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutStateProvider.LayoutStateObserver;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.CurrentTabObserver;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.TabSwitcherDrawable;
import org.chromium.chrome.browser.user_education.IPHCommandBuilder;
import org.chromium.chrome.browser.user_education.UserEducationHelper;
import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter.HighlightParams;
import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter.HighlightShape;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.ui.base.ViewUtils;
import org.chromium.url.GURL;

/**
 * Root component for the tab switcher button on the toolbar. Intended to own the {@link
 * ToggleTabStackButton}, but currently it only manages some signals around the tab switcher button.
 * TODO(crbug.com/40588354): Finish converting HomeButton to MVC and move more logic into this
 * class.
 */
public class ToggleTabStackButtonCoordinator {
    private final CallbackController mCallbackController = new CallbackController();
    private final Context mContext;
    @NonNull private ToggleTabStackButton mToggleTabStackButton;
    private final UserEducationHelper mUserEducationHelper;
    private final Supplier<Boolean> mIsIncognitoSupplier;
    private final OneshotSupplier<Boolean> mPromoShownOneshotSupplier;
    private final CurrentTabObserver mPageLoadObserver;
    private final ObservableSupplier<TabModelSelector> mTabModelSelectorSupplier;

    private LayoutStateProvider mLayoutStateProvider;
    private LayoutStateObserver mLayoutStateObserver;
    @VisibleForTesting boolean mIphBeingShown;
    // Non-null when tab declutter is enabled and initWithNative is called.
    private @Nullable ObservableSupplier<Integer> mArchivedTabCountSupplier;
    private @Nullable Runnable mArchivedTabsIphShownCallback;
    private @Nullable Runnable mArchivedTabsIphDismissedCallback;
    private @Nullable Callback<Integer> mArchivedTabCountObserver = this::maybeShowDeclutterIph;

    /**
     * @param context The Android context used for various view operations.
     * @param toggleTabStackButton The concrete {@link ToggleTabStackButton} class for this MVC
     *     component.
     * @param userEducationHelper Helper class for showing in-product help text bubbles.
     * @param isIncognitoSupplier Supplier for whether the current tab is incognito.
     * @param promoShownOneshotSupplier Potentially delayed information about if a promo was shown.
     * @param layoutStateProviderSupplier Allows observing layout state.
     * @param activityTabSupplier Supplier of the activity tab.
     * @param tabModelSelectorSupplier Supplier for @{@link TabModelSelector}.
     */
    public ToggleTabStackButtonCoordinator(
            Context context,
            ToggleTabStackButton toggleTabStackButton,
            UserEducationHelper userEducationHelper,
            Supplier<Boolean> isIncognitoSupplier,
            OneshotSupplier<Boolean> promoShownOneshotSupplier,
            OneshotSupplier<LayoutStateProvider> layoutStateProviderSupplier,
            ObservableSupplier<Tab> activityTabSupplier,
            ObservableSupplier<TabModelSelector> tabModelSelectorSupplier) {
        mContext = context;
        mToggleTabStackButton = toggleTabStackButton;
        mUserEducationHelper = userEducationHelper;
        mIsIncognitoSupplier = isIncognitoSupplier;
        mPromoShownOneshotSupplier = promoShownOneshotSupplier;
        mTabModelSelectorSupplier = tabModelSelectorSupplier;

        layoutStateProviderSupplier.onAvailable(
                mCallbackController.makeCancelable(this::setLayoutStateProvider));

        mPageLoadObserver =
                new CurrentTabObserver(
                        activityTabSupplier,
                        new EmptyTabObserver() {
                            @Override
                            public void onPageLoadFinished(Tab tab, GURL url) {
                                handlePageLoadFinished();
                            }
                        },
                        /* swapCallback= */ null);
    }

    /**
     * Post native initializations.
     *
     * @param onClickListener @{@link OnClickListener} for view.
     * @param onLongClickListener @{@link OnLongClickListener} for view.
     * @param tabCountSupplier Supplier for current tab count to show in view.
     * @param archivedTabCountSupplier Supplies the current archived tab count, used for displaying
     *     the associated IPH.
     * @param archivedTabsIphShownCallback Callback for when the archived tabs iph is shown.
     */
    public void initializeWithNative(
            OnClickListener onClickListener,
            OnLongClickListener onLongClickListener,
            ObservableSupplier<Integer> tabCountSupplier,
            @Nullable ObservableSupplier<Integer> archivedTabCountSupplier,
            @NonNull Runnable archivedTabsIphShownCallback,
            @NonNull Runnable archivedTabsIphDismissedCallback) {
        mToggleTabStackButton.setOnClickListener(onClickListener);
        mToggleTabStackButton.setOnLongClickListener(onLongClickListener);
        mToggleTabStackButton.setTabCountSupplier(tabCountSupplier, mIsIncognitoSupplier);

        mArchivedTabCountSupplier = archivedTabCountSupplier;
        if (mArchivedTabCountSupplier != null) {
            mArchivedTabCountSupplier.addObserver(mArchivedTabCountObserver);
            mArchivedTabsIphShownCallback = archivedTabsIphShownCallback;
            mArchivedTabsIphDismissedCallback = archivedTabsIphDismissedCallback;
        }
    }

    /** Cleans up callbacks and observers. */
    public void destroy() {
        mCallbackController.destroy();

        mPageLoadObserver.destroy();

        if (mLayoutStateProvider != null) {
            mLayoutStateProvider.removeObserver(mLayoutStateObserver);
            mLayoutStateProvider = null;
            mLayoutStateObserver = null;
        }

        if (mArchivedTabCountSupplier != null) {
            mArchivedTabCountSupplier.removeObserver(mArchivedTabCountObserver);
        }

        mToggleTabStackButton.destroy();
    }

    /** Get container view for drawing, accessibility traversal and animations. */
    public View getContainerView() {
        return mToggleTabStackButton;
    }

    /**
     * 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, mToggleTabStackButton, canvas);
        mToggleTabStackButton.drawTabSwitcherAnimationOverlay(canvas, alpha);
        canvas.restore();
    }

    /** Get tab count on button for texture capture. */
    public int getDrawableTabCount() {
        return ((TabSwitcherDrawable) mToggleTabStackButton.getDrawable()).getTabCount();
    }

    /** Update button with branded color scheme. */
    public void setBrandedColorScheme(int brandedColorScheme) {
        mToggleTabStackButton.setBrandedColorScheme(brandedColorScheme);
    }

    public Supplier<Boolean> getIsIncognitoSupplier() {
        return mIsIncognitoSupplier;
    }

    private void setLayoutStateProvider(LayoutStateProvider layoutStateProvider) {
        assert layoutStateProvider != null;
        assert mLayoutStateProvider == null : "the mLayoutStateProvider should set at most once.";

        mLayoutStateProvider = layoutStateProvider;
        // Make button un-clickable during browser layout transition. Re-enable once transition
        // completes.
        mLayoutStateObserver =
                new LayoutStateObserver() {

                    @Override
                    public void onStartedShowing(@LayoutType int layoutType) {
                        if (layoutType == LayoutType.BROWSING) {
                            setClickable(false);
                        } else if (layoutType == LayoutType.TAB_SWITCHER) {
                            updateTabSwitcherButtonRipple();
                        }
                    }

                    @Override
                    public void onStartedHiding(@LayoutType int layoutType) {
                        if (layoutType == LayoutType.BROWSING) {
                            setClickable(false);
                        }
                    }

                    @Override
                    public void onFinishedShowing(int layoutType) {
                        if (layoutType == LayoutType.BROWSING) {
                            setClickable(true);
                        }
                    }

                    @Override
                    public void onFinishedHiding(int layoutType) {
                        if (layoutType == LayoutType.BROWSING) {
                            setClickable(true);
                        }
                    }
                };
        mLayoutStateProvider.addObserver(mLayoutStateObserver);
    }

    private void setClickable(boolean val) {
        mToggleTabStackButton.setClickable(val);
    }

    @VisibleForTesting
    void handlePageLoadFinished() {
        if (!mToggleTabStackButton.isShown()) return;

        HighlightParams params = new HighlightParams(HighlightShape.CIRCLE);
        params.setBoundsRespectPadding(true);
        IPHCommandBuilder builder = null;
        if (ChromeFeatureList.sTabStripIncognitoMigration.isEnabled()
                && mTabModelSelectorSupplier.hasValue()) {
            TabModelSelector selector = mTabModelSelectorSupplier.get();
            // When in Incognito, show IPH to switch out.
            if (selector.getCurrentModel().isIncognitoBranded()) {
                builder =
                        new IPHCommandBuilder(
                                mContext.getResources(),
                                FeatureConstants.TAB_SWITCHER_BUTTON_SWITCH_INCOGNITO,
                                R.string.iph_tab_switcher_switch_out_of_incognito_text,
                                R.string
                                        .iph_tab_switcher_switch_out_of_incognito_accessibility_text);
            } else if (selector.getModel(true).getCount() > 0) {
                // When in standard model with incognito tabs, show IPH to switch into incognito.
                builder =
                        new IPHCommandBuilder(
                                mContext.getResources(),
                                FeatureConstants.TAB_SWITCHER_BUTTON_SWITCH_INCOGNITO,
                                R.string.iph_tab_switcher_switch_into_incognito_text,
                                R.string.iph_tab_switcher_switch_into_incognito_accessibility_text);
            }
        } else if (!mIsIncognitoSupplier.get()
                && mPromoShownOneshotSupplier.hasValue()
                && !mPromoShownOneshotSupplier.get()) {
            builder =
                    new IPHCommandBuilder(
                            mContext.getResources(),
                            FeatureConstants.TAB_SWITCHER_BUTTON_FEATURE,
                            R.string.iph_tab_switcher_text,
                            R.string.iph_tab_switcher_accessibility_text);
        }

        if (builder != null) {
            mUserEducationHelper.requestShowIPH(
                    builder.setAnchorView(mToggleTabStackButton)
                            .setOnShowCallback(this::handleShowCallback)
                            .setOnDismissCallback(this::handleDismissCallback)
                            .setHighlightParams(params)
                            .build());
        }
    }

    /**
     * Enables or disables the tab switcher ripple depending on whether we are in or out of the tab
     * switcher mode.
     */
    private void updateTabSwitcherButtonRipple() {
        Drawable drawable = mToggleTabStackButton.getBackground();
        // drawable may not be a RippleDrawable if IPH is showing. Ignore that scenario since
        // it is rare.
        if (drawable instanceof RippleDrawable) {
            // Force the ripple to end so the transition looks correct.
            drawable.jumpToCurrentState();
        }
    }

    private void handleShowCallback() {
        mIphBeingShown = true;
    }

    private void handleDismissCallback() {
        mIphBeingShown = false;
    }

    private void maybeShowDeclutterIph(int tabCount) {
        if (!ChromeFeatureList.sAndroidTabDeclutter.isEnabled()) return;
        if (mIsIncognitoSupplier.get()) return;
        if (tabCount == 0) return;

        HighlightParams params = new HighlightParams(HighlightShape.CIRCLE);
        params.setBoundsRespectPadding(true);
        mUserEducationHelper.requestShowIPH(
                new IPHCommandBuilder(
                                mContext.getResources(),
                                FeatureConstants.ANDROID_TAB_DECLUTTER_FEATURE,
                                R.string.iph_android_tab_declutter_text,
                                R.string.iph_android_tab_declutter_accessibility_text)
                        .setAnchorView(mToggleTabStackButton)
                        .setHighlightParams(params)
                        .setOnShowCallback(mArchivedTabsIphShownCallback)
                        .setOnDismissCallback(mArchivedTabsIphDismissedCallback)
                        .build());
    }
}