chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneCoordinator.java

// Copyright 2024 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.tasks.tab_management;

import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.ALL_KEYS;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BROWSER_CONTROLS_STATE_PROVIDER;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.FETCH_VIEW_BY_INDEX_CALLBACK;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.GET_VISIBLE_RANGE_CALLBACK;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_SCROLLING_SUPPLIER_CALLBACK;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.MODE;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
import android.util.Size;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Function;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;

import org.chromium.base.Callback;
import org.chromium.base.TimeUtils.UptimeMillisTimer;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.LazyOneshotSupplier;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.data_sharing.DataSharingTabManager;
import org.chromium.chrome.browser.data_sharing.ui.invitation_dialog.DataSharingInvitationDialogCoordinator;
import org.chromium.chrome.browser.hub.HubFieldTrial;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab_ui.RecyclerViewPosition;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tab_ui.TabSwitcherCustomViewManager;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabGridDialogMediator.DialogController;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.tasks.tab_management.TabListMediator.GridCardOnClickListenerProvider;
import org.chromium.chrome.browser.tasks.tab_management.TabSwitcherMessageManager.MessageUpdateObserver;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.List;

/** Coordinator for a {@link TabSwitcherPaneBase}'s UI. */
public class TabSwitcherPaneCoordinator implements BackPressHandler {
    static final String COMPONENT_NAME = "GridTabSwitcher";

    private final MessageUpdateObserver mMessageUpdateObserver =
            new MessageUpdateObserver() {
                @Override
                public void onAppendedMessage() {
                    updateBottomPadding();
                }

                @Override
                public void onRemovedMessage() {
                    updateBottomPadding();
                }

                @Override
                public void onRemoveAllAppendedMessage() {
                    updateBottomPadding();
                }

                @Override
                public void onRestoreAllAppendedMessage() {
                    updateBottomPadding();
                }

                @Override
                public void onShowPriceWelcomeMessage() {
                    updateBottomPadding();
                }

                @Override
                public void onRemovePriceWelcomeMessage() {
                    updateBottomPadding();
                }

                @Override
                public void onRestorePriceWelcomeMessage() {
                    updateBottomPadding();
                }
            };

    private final TabGridItemTouchHelperCallback.OnLongPressTabItemEventListener
            mLongPressItemEventListener = this::onLongPressOnTabCard;
    private final Activity mActivity;
    private final OneshotSupplier<ProfileProvider> mProfileProviderSupplier;
    private final Callback<Boolean> mOnVisibilityChanged = this::onVisibilityChanged;
    private final ObservableSupplier<Boolean> mIsVisibleSupplier;
    private final ObservableSupplier<Boolean> mIsAnimatingSupplier;
    private final TabSwitcherPaneMediator mMediator;
    private final Supplier<Boolean> mTabGridDialogVisibilitySupplier = this::isTabGridDialogVisible;
    private final MultiThumbnailCardProvider mMultiThumbnailCardProvider;
    private final TabListCoordinator mTabListCoordinator;
    private final PropertyModel mContainerViewModel;
    private final PropertyModelChangeProcessor mContainerViewChangeProcessor;
    private final LazyOneshotSupplier<DialogController> mDialogControllerSupplier;
    private final TabListEditorManager mTabListEditorManager;
    private final ViewGroup mParentView;
    private final TabSwitcherMessageManager mMessageManager;
    private final ModalDialogManager mModalDialogManager;
    private final Runnable mOnDestroyed;
    private final TabListOnScrollListener mTabListOnScrollListener = new TabListOnScrollListener();
    private final OneshotSupplierImpl<ObservableSupplier<Boolean>> mIsScrollingSupplier =
            new OneshotSupplierImpl<>();

    /** Lazily initialized when shown. */
    private @Nullable TabGridDialogCoordinator mTabGridDialogCoordinator;

    private @Nullable DataSharingInvitationDialogCoordinator mDataSharingDialogCoordinator;
    private @Nullable Function<Integer, View> mFetchViewByIndex;
    private @Nullable Supplier<Pair<Integer, Integer>> mGetVisibleIndex;

    /**
     * @param activity The {@link Activity} that hosts the pane.
     * @param profileProviderSupplier The supplier for profiles.
     * @param tabModelFilterSupplier The supplier of the tab model filter fo rthis pane.
     * @param tabContentManager For management of thumbnails.
     * @param tabCreatorManager For creating new tabs.
     * @param browserControlsStateProvider For determining thumbnail size.
     * @param scrimCoordinator The scrim coordinator to use for the tab grid dialog.
     * @param modalDialogManager The modal dialog manager for the activity.
     * @param bottomSheetController The {@link BottomSheetController} for the current activity.
     * @param dataSharingTabManager The {@link} DataSharingTabManager managing communication between
     *     UI and DataSharing services.
     * @param messageManager The {@link TabSwitcherMessageManager} for the message service.
     * @param parentView The view to use as a parent.
     * @param resetHandler The tab list reset handler for the pane.
     * @param isVisibleSupplier The supplier of the pane's visibility.
     * @param isAnimatingSupplier Whether the pane is animating into or out of view.
     * @param onTabClickCallback Callback to invoke when a tab is clicked.
     * @param setHairlineVisibilityCallback Callback to be invoked to show or hide the hairline.
     * @param mode The {@link TabListMode} to use.
     * @param supportsEmptyState Whether empty state UI should be shown when the model is empty.
     * @param onTabGroupCreation Should be run when the UI is used to create a tab group.
     * @param onDestroyed A {@link Runnable} to execute when {@link #destroy()} is invoked.
     */
    public TabSwitcherPaneCoordinator(
            @NonNull Activity activity,
            @NonNull OneshotSupplier<ProfileProvider> profileProviderSupplier,
            @NonNull ObservableSupplier<TabModelFilter> tabModelFilterSupplier,
            @NonNull TabContentManager tabContentManager,
            @NonNull TabCreatorManager tabCreatorManager,
            @NonNull BrowserControlsStateProvider browserControlsStateProvider,
            @NonNull ScrimCoordinator scrimCoordinator,
            @NonNull ModalDialogManager modalDialogManager,
            @NonNull BottomSheetController bottomSheetController,
            @NonNull DataSharingTabManager dataSharingTabManager,
            @NonNull TabSwitcherMessageManager messageManager,
            @NonNull ViewGroup parentView,
            @NonNull TabSwitcherResetHandler resetHandler,
            @NonNull ObservableSupplier<Boolean> isVisibleSupplier,
            @NonNull ObservableSupplier<Boolean> isAnimatingSupplier,
            @NonNull Callback<Integer> onTabClickCallback,
            @NonNull Callback<Boolean> setHairlineVisibilityCallback,
            @TabListMode int mode,
            boolean supportsEmptyState,
            @Nullable Runnable onTabGroupCreation,
            @NonNull Runnable onDestroyed) {
        try (TraceEvent e = TraceEvent.scoped("TabSwitcherPaneCoordinator.constructor")) {
            mProfileProviderSupplier = profileProviderSupplier;
            mIsVisibleSupplier = isVisibleSupplier;
            mIsAnimatingSupplier = isAnimatingSupplier;
            mActivity = activity;
            mModalDialogManager = modalDialogManager;
            mParentView = parentView;
            mOnDestroyed = onDestroyed;
            assert mode != TabListMode.STRIP : "TabListMode.STRIP not supported.";

            ViewGroup coordinatorView = activity.findViewById(R.id.coordinator);

            PropertyModel containerViewModel =
                    new PropertyModel.Builder(ALL_KEYS)
                            .with(BROWSER_CONTROLS_STATE_PROVIDER, browserControlsStateProvider)
                            .with(MODE, mode)
                            .with(FETCH_VIEW_BY_INDEX_CALLBACK, (f) -> mFetchViewByIndex = f)
                            .with(GET_VISIBLE_RANGE_CALLBACK, (f) -> mGetVisibleIndex = f)
                            .with(
                                    IS_SCROLLING_SUPPLIER_CALLBACK,
                                    (f) -> mIsScrollingSupplier.set(f))
                            .build();

            mContainerViewModel = containerViewModel;
            Profile profile = mProfileProviderSupplier.get().getOriginalProfile();
            TabGroupModelFilter filter = (TabGroupModelFilter) tabModelFilterSupplier.get();
            ActionConfirmationManager actionConfirmationManager =
                    new ActionConfirmationManager(profile, mActivity, filter, mModalDialogManager);

            mDialogControllerSupplier =
                    LazyOneshotSupplier.fromSupplier(
                            () -> {
                                mTabGridDialogCoordinator =
                                        new TabGridDialogCoordinator(
                                                activity,
                                                browserControlsStateProvider,
                                                bottomSheetController,
                                                dataSharingTabManager,
                                                tabModelFilterSupplier,
                                                tabContentManager,
                                                tabCreatorManager,
                                                coordinatorView,
                                                resetHandler,
                                                getGridCardOnClickListenerProvider(),
                                                TabSwitcherPaneCoordinator.this
                                                        ::getTabGridDialogAnimationSourceView,
                                                scrimCoordinator,
                                                getTabGroupTitleEditor(),
                                                actionConfirmationManager,
                                                mModalDialogManager);
                                return mTabGridDialogCoordinator.getDialogController();
                            });

            mMediator =
                    new TabSwitcherPaneMediator(
                            resetHandler,
                            tabModelFilterSupplier,
                            mDialogControllerSupplier,
                            containerViewModel,
                            parentView,
                            this::onTabSwitcherShown,
                            isVisibleSupplier,
                            isAnimatingSupplier,
                            onTabClickCallback,
                            this::getNthTabIndexInModel);

            mMultiThumbnailCardProvider =
                    new MultiThumbnailCardProvider(
                            activity,
                            browserControlsStateProvider,
                            tabContentManager,
                            tabModelFilterSupplier);

            var recyclerViewTimer = new UptimeMillisTimer();

            @DrawableRes
            int emptyImageResId =
                    DeviceFormFactor.isNonMultiDisplayContextOnTablet(activity)
                            ? R.drawable.tablet_tab_switcher_empty_state_illustration
                            : R.drawable.phone_tab_switcher_empty_state_illustration;
            TabListCoordinator tabListCoordinator =
                    new TabListCoordinator(
                            mode,
                            activity,
                            browserControlsStateProvider,
                            mModalDialogManager,
                            tabModelFilterSupplier,
                            mMultiThumbnailCardProvider,
                            /* actionOnRelatedTabs= */ true,
                            getGridCardOnClickListenerProvider(),
                            /* dialogHandler= */ null,
                            TabProperties.TabActionState.CLOSABLE,
                            /* selectionDelegateProvider= */ null,
                            this::getPriceWelcomeMessageController,
                            parentView,
                            /* attachToParent= */ true,
                            COMPONENT_NAME,
                            /* onModelTokenChange= */ null,
                            /* hasEmptyView= */ supportsEmptyState,
                            supportsEmptyState ? emptyImageResId : Resources.ID_NULL,
                            supportsEmptyState
                                    ? R.string.tabswitcher_no_tabs_empty_state
                                    : Resources.ID_NULL,
                            supportsEmptyState
                                    ? R.string.tabswitcher_no_tabs_open_to_visit_different_pages
                                    : Resources.ID_NULL,
                            onTabGroupCreation,
                            /* allowDragAndDrop= */ true);
            mTabListCoordinator = tabListCoordinator;
            tabListCoordinator.setOnLongPressTabItemEventListener(mLongPressItemEventListener);

            mTabListOnScrollListener
                    .getYOffsetNonZeroSupplier()
                    .addObserver(setHairlineVisibilityCallback);
            TabListRecyclerView recyclerView = tabListCoordinator.getContainerView();
            recyclerView.setVisibility(View.VISIBLE);
            recyclerView.setBackgroundColor(Color.TRANSPARENT);
            recyclerView.addOnScrollListener(mTabListOnScrollListener);
            mContainerViewChangeProcessor =
                    PropertyModelChangeProcessor.create(
                            containerViewModel, recyclerView, TabListContainerViewBinder::bind);

            RecordHistogram.recordTimesHistogram(
                    "Android.TabSwitcher.SetupRecyclerView.Time",
                    recyclerViewTimer.getElapsedMillis());

            TabListEditorManager tabListEditorManager =
                    new TabListEditorManager(
                            activity,
                            mModalDialogManager,
                            coordinatorView,
                            /* rootView= */ parentView,
                            browserControlsStateProvider,
                            tabModelFilterSupplier,
                            tabContentManager,
                            tabListCoordinator,
                            bottomSheetController,
                            mode,
                            onTabGroupCreation);
            mTabListEditorManager = tabListEditorManager;
            mMediator.setTabListEditorControllerSupplier(
                    mTabListEditorManager.getControllerSupplier());

            mMessageManager = messageManager;
            mMessageManager.registerMessages(tabListCoordinator);

            mOnVisibilityChanged.onResult(isVisibleSupplier.addObserver(mOnVisibilityChanged));
        }
    }

    /** Destroys the coordinator. */
    public void destroy() {
        mMessageManager.removeObserver(mMessageUpdateObserver);
        mMessageManager.unbind(mTabListCoordinator);
        mMediator.destroy();
        mTabListCoordinator.onDestroy();
        mContainerViewChangeProcessor.destroy();
        if (mTabGridDialogCoordinator != null) {
            mTabGridDialogCoordinator.destroy();
        }
        mIsVisibleSupplier.removeObserver(mOnVisibilityChanged);
        mMultiThumbnailCardProvider.destroy();
        mTabListEditorManager.destroy();
        mOnDestroyed.run();
    }

    /** Post native initialization. */
    public void initWithNative() {
        try (TraceEvent e = TraceEvent.scoped("TabSwitcherPaneCoordinator.initWithNative")) {
            ProfileProvider profileProvider = mProfileProviderSupplier.get();
            assert profileProvider != null;
            Profile originalProfile = profileProvider.getOriginalProfile();
            mTabListCoordinator.initWithNative(originalProfile);
            mMultiThumbnailCardProvider.initWithNative(originalProfile);
        }
    }

    /** Shows the tab list editor. */
    public void showTabListEditor() {
        mTabListEditorManager.showTabListEditor();
    }

    /**
     * Resets the UI with the specified tabs.
     *
     * @param tabList The {@link TabList} to show tabs for.
     */
    public void resetWithTabList(@Nullable TabList tabList) {
        List<Tab> tabs = TabModelUtils.convertTabListToListOfTabs(tabList);
        mMessageManager.beforeReset();
        // Quick mode being false here ensures the selected tab's thumbnail gets updated. With Hub
        // the TabListCoordinator no longer triggers thumbnail captures so this shouldn't guard
        // against the large amount of work that is used to.
        mTabListCoordinator.resetWithListOfTabs(
                tabList == null ? null : tabs, /* quickMode= */ false);
        mMessageManager.afterReset(tabs.size());
        mTabListOnScrollListener.postUpdate(mTabListCoordinator.getContainerView());
    }

    /** Performs soft cleanup which removes thumbnails to relieve memory usage. */
    public void softCleanup() {
        mTabListCoordinator.softCleanup();
    }

    /** Performs hard cleanup which saves price drop information. */
    public void hardCleanup() {
        mTabListCoordinator.hardCleanup();
        // TODO(crbug.com/40946413): The pre-fork implementation resets the tab list, this seems
        // suboptimal. Consider not doing this.
        resetWithTabList(null);
    }

    /**
     * Scrolls so that the selected tab in the current model is in the middle of the screen or as
     * close as possible if at the start/end of the recycler view.
     */
    public void setInitialScrollIndexOffset() {
        mMediator.setInitialScrollIndexOffset();
    }

    /** Requests accessibility focus on the current tab. */
    public void requestAccessibilityFocusOnCurrentTab() {
        mMediator.requestAccessibilityFocusOnCurrentTab();
    }

    /** Returns a {@link Supplier} that provides dialog visibility information. */
    public @Nullable Supplier<Boolean> getTabGridDialogVisibilitySupplier() {
        return mTabGridDialogVisibilitySupplier;
    }

    /** Returns a {@link TabSwitcherCustomViewManager.Delegate} for supplying custom views. */
    public @Nullable TabSwitcherCustomViewManager.Delegate
            getTabSwitcherCustomViewManagerDelegate() {
        return mMediator;
    }

    /** Returns the number of elements in the tab switcher's tab list model. */
    public int getTabSwitcherTabListModelSize() {
        return mTabListCoordinator.getTabListModelSize();
    }

    /** Set the tab switcher's RecyclerViewPosition. */
    public void setTabSwitcherRecyclerViewPosition(RecyclerViewPosition position) {
        mTabListCoordinator.setRecyclerViewPosition(position);
    }

    /** Returns the {@link Rect} of the recyclerview in global coordinates. */
    public @NonNull Rect getRecyclerViewRect() {
        return mTabListCoordinator.getRecyclerViewLocation();
    }

    /**
     * @param tabId The tab ID to get a rect for.
     * @return a {@link Rect} for the tab's thumbnail (may be an empty rect if the tab is not
     *     found).
     */
    public @NonNull Rect getTabThumbnailRect(int tabId) {
        TabGridDialogCoordinator dialogCoordinator = mTabGridDialogCoordinator;
        if (dialogCoordinator != null && dialogCoordinator.isVisible()) {
            return dialogCoordinator.getTabThumbnailRect(tabId);
        }
        return mTabListCoordinator.getTabThumbnailRect(tabId);
    }

    /** Returns the {@link Rect} of the recyclerview in global coordinates. */
    public @NonNull Size getThumbnailSize() {
        TabGridDialogCoordinator dialogCoordinator = mTabGridDialogCoordinator;
        if (dialogCoordinator != null && dialogCoordinator.isVisible()) {
            return dialogCoordinator.getThumbnailSize();
        }
        return mTabListCoordinator.getThumbnailSize();
    }

    /**
     * @param tabId The tab ID whose view to wait for.
     * @param r A runnable to be executed on the next layout pass or immediately if one is not
     *     scheduled.
     */
    public void waitForLayoutWithTab(int tabId, Runnable r) {
        TabGridDialogCoordinator dialogCoordinator = mTabGridDialogCoordinator;
        if (dialogCoordinator != null && dialogCoordinator.isVisible()) {
            dialogCoordinator.waitForLayoutWithTab(tabId, r);
            return;
        }
        mTabListCoordinator.waitForLayoutWithTab(tabId, r);
    }

    /**
     * Scrolls to the specified group and animates open a dialog. It is the caller's responsibility
     * to ensure that this pane is showing before calling this.
     *
     * @param tabId The id of any tab in the group.
     */
    public void requestOpenTabGroupDialog(int tabId) {
        mMediator.scrollToTabById(tabId);
        mMediator.openTabGroupDialog(tabId);
    }

    /** Returns the range (inclusive) of visible view indexes. */
    public @Nullable Pair<Integer, Integer> getVisibleRange() {
        return mGetVisibleIndex == null ? null : mGetVisibleIndex.get();
    }

    /** Returns the root view at a given index. */
    public @Nullable View getViewByIndex(int viewIndex) {
        return mFetchViewByIndex == null ? null : mFetchViewByIndex.apply(viewIndex);
    }

    /** Returns a nested supplier for the scrolling state of the view. */
    public OneshotSupplier<ObservableSupplier<Boolean>> getIsScrollingSupplier() {
        return mIsScrollingSupplier;
    }

    @Override
    public @BackPressResult int handleBackPress() {
        return mMediator.handleBackPress();
    }

    @Override
    public ObservableSupplier<Boolean> getHandleBackPressChangedSupplier() {
        return mMediator.getHandleBackPressChangedSupplier();
    }

    private boolean isTabGridDialogVisible() {
        return mTabGridDialogCoordinator == null ? false : mTabGridDialogCoordinator.isVisible();
    }

    private void onTabSwitcherShown() {
        mTabListCoordinator.attachEmptyView();
    }

    private View getTabGridDialogAnimationSourceView(int tabId) {
        // If we are animating to show or hide the HubLayout, the TabGridDialog should hide or show
        // via fade instead of animating from a tab. Return null so that this happens.
        if (mIsAnimatingSupplier.get()) return null;

        TabListCoordinator coordinator = mTabListCoordinator;
        int index = coordinator.getTabIndexFromTabId(tabId);
        ViewHolder sourceViewHolder =
                coordinator.getContainerView().findViewHolderForAdapterPosition(index);
        // TODO(crbug.com/41479135): This is band-aid fix that will show basic fade-in/fade-out
        // animation when we cannot find the animation source view holder. This is happening due to
        // current group id in TabGridDialog can not be indexed in TabListModel, which should never
        // happen. Remove this when figure out the actual cause.
        return sourceViewHolder == null ? null : sourceViewHolder.itemView;
    }

    private void onVisibilityChanged(boolean visible) {
        if (visible) {
            mMessageManager.bind(
                    mTabListCoordinator,
                    mParentView,
                    /* priceWelcomeMessageReviewActionProvider= */ mMediator,
                    (tabId) -> mMediator.onTabSelecting(tabId, false));
            mMessageManager.addObserver(mMessageUpdateObserver);
            updateBottomPadding();
            mTabListCoordinator.prepareTabSwitcherPaneView();
        } else {
            mMessageManager.removeObserver(mMessageUpdateObserver);
            mMessageManager.unbind(mTabListCoordinator);
            updateBottomPadding();
            mTabListCoordinator.postHiding();
        }
    }

    private GridCardOnClickListenerProvider getGridCardOnClickListenerProvider() {
        return mMediator;
    }

    private TabGroupTitleEditor getTabGroupTitleEditor() {
        return mTabListCoordinator.getTabGroupTitleEditor();
    }

    private PriceWelcomeMessageController getPriceWelcomeMessageController() {
        return mMessageManager;
    }

    private void onLongPressOnTabCard(int tabId) {
        mTabListEditorManager.showTabListEditor();
        RecordUserAction.record("TabMultiSelectV2.OpenLongPressInGrid");
    }

    /** Returns the container view property model for testing. */
    @NonNull
    PropertyModel getContainerViewModelForTesting() {
        return mContainerViewModel;
    }

    /** Returns the dialog controller for testing. */
    @NonNull
    DialogController getTabGridDialogControllerForTesting() {
        return mDialogControllerSupplier.get();
    }

    void showQuickDeleteAnimation(Runnable onAnimationEnd, List<Tab> tabs) {
        mTabListCoordinator.showQuickDeleteAnimation(onAnimationEnd, tabs);
    }

    void showCloseAllTabsAnimation(Runnable closeTabs) {
        mTabListCoordinator.showCloseAllTabsAnimation(closeTabs);
    }

    /** Returns the filter index of a tab from its view index. */
    public int countOfTabCardsOrInvalid(int viewIndex) {
        return mTabListCoordinator.indexOfTabCardsOrInvalid(viewIndex);
    }

    private int getNthTabIndexInModel(int filterIndex) {
        assert mTabListCoordinator != null;
        int indexInModel = mTabListCoordinator.getIndexOfNthTabCard(filterIndex);
        // If the tab list coordinator doesn't contain tab data yet assume filterIndex is a
        // sufficient approximation as the offset would be caused by message cards.
        if (indexInModel == TabList.INVALID_TAB_INDEX) return filterIndex;

        return indexInModel;
    }

    private void updateBottomPadding() {
        if (HubFieldTrial.usesFloatActionButton() && mTabListCoordinator.isLastItemMessage()) {
            mContainerViewModel.set(
                    TabListContainerProperties.BOTTOM_PADDING,
                    mActivity
                            .getResources()
                            .getDimensionPixelSize(R.dimen.floating_action_button_space));
        } else {
            mContainerViewModel.set(TabListContainerProperties.BOTTOM_PADDING, 0);
        }
    }

    /**
     * Open the invitation modal on top of the tab switcher view when an invitation intent is
     * intercepted.
     *
     * @param invitationId The id of the invitation.
     */
    public void openInvitationModal(String invitationId) {
        if (mTabGridDialogCoordinator != null) {
            mTabGridDialogCoordinator.hideDialog(true);
        }

        if (mDataSharingDialogCoordinator == null) {
            mDataSharingDialogCoordinator =
                    new DataSharingInvitationDialogCoordinator(mActivity, mModalDialogManager);
        }
        mDataSharingDialogCoordinator.show();
    }
}