chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneCoordinatorFactory.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 android.app.Activity;
import android.view.ViewGroup;

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

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.data_sharing.DataSharingTabManager;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.NativeInitObserver;
import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator.SystemUiScrimDelegate;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.util.TokenHolder;

/** Holds dependencies for constructing a {@link TabSwitcherPane}. */
public class TabSwitcherPaneCoordinatorFactory {
    private final TokenHolder mMessageManagerTokenHolder =
            new TokenHolder(this::onMessageManagerTokenStateChanged);

    private final Activity mActivity;
    private final ActivityLifecycleDispatcher mLifecycleDispatcher;
    private final OneshotSupplier<ProfileProvider> mProfileProviderSupplier;
    private final TabModelSelector mTabModelSelector;
    private final TabContentManager mTabContentManager;
    private final TabCreatorManager mTabCreatorManager;
    private final BrowserControlsStateProvider mBrowserControlsStateProvider;
    private final MultiWindowModeStateDispatcher mMultiWindowModeStateDispatcher;
    private final ScrimCoordinator mScrimCoordinator;
    private final SnackbarManager mSnackbarManager;
    private final ModalDialogManager mModalDialogManager;
    private final @TabListMode int mMode;
    private final @NonNull BottomSheetController mBottomSheetController;
    private final DataSharingTabManager mDataSharingTabManager;
    private final @NonNull BackPressManager mBackPressManager;

    private @Nullable TabSwitcherMessageManager mMessageManager;

    /**
     * @param activity The {@link Activity} that hosts the pane.
     * @param lifecycleDispatcher The lifecycle dispatcher for the activity.
     * @param profileProviderSupplier The supplier for profiles.
     * @param tabModelSelector For access to {@link TabModel}.
     * @param tabContentManager For management of thumbnails.
     * @param tabCreatorManager For creating new tabs.
     * @param browserControlsStateProvider For determining thumbnail size.
     * @param multiWindowModeStateDispatcher For managing behavior in multi-window.
     * @param rootUiScrimCoordinator The root UI coordinator's scrim coordinator. On LFF this is
     *     unused as the root UI's scrim coordinator is used for the show/hide animation.
     * @param snackbarManager The activity level snackbar manager.
     * @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 backPressManager Manages the different back press handlers throughout the app.
     */
    TabSwitcherPaneCoordinatorFactory(
            @NonNull Activity activity,
            @NonNull ActivityLifecycleDispatcher lifecycleDispatcher,
            @NonNull OneshotSupplier<ProfileProvider> profileProviderSupplier,
            @NonNull TabModelSelector tabModelSelector,
            @NonNull TabContentManager tabContentManager,
            @NonNull TabCreatorManager tabCreatorManager,
            @NonNull BrowserControlsStateProvider browserControlsStateProvider,
            @NonNull MultiWindowModeStateDispatcher multiWindowModeStateDispatcher,
            @NonNull ScrimCoordinator rootUiScrimCoordinator,
            @NonNull SnackbarManager snackbarManager,
            @NonNull ModalDialogManager modalDialogManager,
            @NonNull BottomSheetController bottomSheetController,
            @NonNull DataSharingTabManager dataSharingTabManager,
            @NonNull BackPressManager backPressManager) {
        mActivity = activity;
        mLifecycleDispatcher = lifecycleDispatcher;
        mProfileProviderSupplier = profileProviderSupplier;
        mTabModelSelector = tabModelSelector;
        mTabContentManager = tabContentManager;
        mTabCreatorManager = tabCreatorManager;
        mBrowserControlsStateProvider = browserControlsStateProvider;
        mMultiWindowModeStateDispatcher = multiWindowModeStateDispatcher;
        mScrimCoordinator =
                DeviceFormFactor.isNonMultiDisplayContextOnTablet(activity)
                        ? createScrimCoordinatorForTablet(activity)
                        : rootUiScrimCoordinator;
        mSnackbarManager = snackbarManager;
        mModalDialogManager = modalDialogManager;
        mBottomSheetController = bottomSheetController;
        mDataSharingTabManager = dataSharingTabManager;
        mMode =
                TabUiFeatureUtilities.shouldUseListMode()
                        ? TabListCoordinator.TabListMode.LIST
                        : TabListCoordinator.TabListMode.GRID;
        mBackPressManager = backPressManager;
    }

    /**
     * Creates a {@link TabSwitcherPaneCoordinator} using a combination of the held dependencies and
     * those supplied through this method.
     *
     * @param parentView The view to use as a parent.
     * @param resetHandler The reset handler to drive updates.
     * @param isVisibleSupplier Supplies visibility information to the tab switcher.
     * @param isAnimatingSupplier Supplies animation information to the tab switcher.
     * @param onTabClickCallback Callback to be invoked with the tab ID of the selected tab.
     * @param setHairlineVisibilityCallback Callback to be invoked to show or hide the hairline.
     * @param isIncognito Whether this is for the incognito tab switcher.
     * @param onTabGroupCreation Should be run when the UI is used to create a tab group.
     * @return a {@link TabSwitcherPaneCoordinator} to use.
     */
    TabSwitcherPaneCoordinator create(
            @NonNull ViewGroup parentView,
            @NonNull TabSwitcherResetHandler resetHandler,
            @NonNull ObservableSupplier<Boolean> isVisibleSupplier,
            @NonNull ObservableSupplier<Boolean> isAnimatingSupplier,
            @NonNull Callback<Integer> onTabClickCallback,
            @NonNull Callback<Boolean> setHairlineVisibilityCallback,
            boolean isIncognito,
            @Nullable Runnable onTabGroupCreation) {
        int token = mMessageManagerTokenHolder.acquireToken();
        assert mMessageManager != null;
        return new TabSwitcherPaneCoordinator(
                mActivity,
                mProfileProviderSupplier,
                createTabModelFilterSupplier(isIncognito),
                mTabContentManager,
                mTabCreatorManager,
                mBrowserControlsStateProvider,
                mScrimCoordinator,
                mModalDialogManager,
                mBottomSheetController,
                mDataSharingTabManager,
                mMessageManager,
                parentView,
                resetHandler,
                isVisibleSupplier,
                isAnimatingSupplier,
                onTabClickCallback,
                setHairlineVisibilityCallback,
                mMode,
                /* supportsEmptyState= */ !isIncognito,
                onTabGroupCreation,
                () -> mMessageManagerTokenHolder.releaseToken(token));
    }

    /** Returns the {@link TabListMode} of the produced {@link TabListCoordinator}s. */
    @TabListMode
    int getTabListMode() {
        // This value will be determined at initialization time based on whether the device is
        // low-end and is not subject to change. Certain behaviors are limited to LIST vs GRID
        // mode and this information may be required even if a coordinator does not exist.
        return mMode;
    }

    /** Returns a scrim coordinator to use for tab grid dialog on LFF devices. */
    @VisibleForTesting
    static ScrimCoordinator createScrimCoordinatorForTablet(Activity activity) {
        ViewGroup coordinator = activity.findViewById(R.id.coordinator);
        // TODO(crbug.com/40067282): Because the show/hide animation already uses the
        // RootUiCoordinator's
        // ScrimCoordinator, a separate instance is needed. However, the way this is implemented the
        // status bar color is not updated. This should be fixed.
        SystemUiScrimDelegate delegate =
                new SystemUiScrimDelegate() {
                    @Override
                    public void setStatusBarScrimFraction(float scrimFraction) {}

                    @Override
                    public void setNavigationBarScrimFraction(float scrimFraction) {}
                };
        return new ScrimCoordinator(
                activity,
                delegate,
                coordinator,
                activity.getColor(R.color.omnibox_focused_fading_background_color));
    }

    @VisibleForTesting
    ObservableSupplier<TabModelFilter> createTabModelFilterSupplier(boolean isIncognito) {
        ObservableSupplierImpl<TabModelFilter> tabModelFilterSupplier =
                new ObservableSupplierImpl<>();
        // This implementation doesn't wait for isTabStateInitialized because we want to be able to
        // show the TabSwitcherPane before tab state initialization finishes. Tab state
        // initialization is an async process; when tab state restoration completes
        // TabModelObserver#restoreCompleted() is called which is listened for in
        // TabSwitcherPaneMediator to properly refresh the list in the event the contents changed.
        TabModelSelector selector = mTabModelSelector;
        if (!selector.getModels().isEmpty()) {
            tabModelFilterSupplier.set(
                    selector.getTabModelFilterProvider().getTabModelFilter(isIncognito));
        } else {
            selector.addObserver(
                    new TabModelSelectorObserver() {
                        @Override
                        public void onChange() {
                            assert !selector.getModels().isEmpty();
                            TabModelFilter filter =
                                    selector.getTabModelFilterProvider()
                                            .getTabModelFilter(isIncognito);
                            assert filter != null;
                            selector.removeObserver(this);
                            tabModelFilterSupplier.set(filter);
                        }
                    });
        }
        return tabModelFilterSupplier;
    }

    private void onMessageManagerTokenStateChanged() {
        if (mMessageManagerTokenHolder.hasTokens()) {
            assert mMessageManager == null : "MessageManager should not exist yet.";
            mMessageManager =
                    new TabSwitcherMessageManager(
                            mActivity,
                            mLifecycleDispatcher,
                            mTabModelSelector
                                    .getTabModelFilterProvider()
                                    .getCurrentTabModelFilterSupplier(),
                            mMultiWindowModeStateDispatcher,
                            mSnackbarManager,
                            mModalDialogManager,
                            mBrowserControlsStateProvider,
                            mTabContentManager,
                            mMode,
                            mActivity.findViewById(R.id.coordinator),
                            mTabCreatorManager.getTabCreator(/* incognito= */ false),
                            mBackPressManager);
            if (mLifecycleDispatcher.isNativeInitializationFinished()) {
                mMessageManager.initWithNative(
                        mProfileProviderSupplier.get().getOriginalProfile(), getTabListMode());
            } else {
                mLifecycleDispatcher.register(
                        new NativeInitObserver() {
                            @Override
                            public void onFinishNativeInitialization() {
                                mMessageManager.initWithNative(
                                        mProfileProviderSupplier.get().getOriginalProfile(),
                                        getTabListMode());
                                mLifecycleDispatcher.unregister(this);
                            }
                        });
            }
        } else {
            mMessageManager.destroy();
            mMessageManager = null;
        }
    }

    TabSwitcherMessageManager getMessageManagerForTesting() {
        return mMessageManager;
    }
}