chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiOneshotSupplier.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.Nullable;

import org.chromium.base.CallbackController;
import org.chromium.base.Token;
import org.chromium.base.ValueChangedCallback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.data_sharing.DataSharingTabManager;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.IncognitoStateProvider;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.ui.modaldialog.ModalDialogManager;

/**
 * A custom {@link OneshotSupplier} for a {@link TabGroupUi}. The supplied value will remain null
 * until the current activity tab is in a tab group.
 */
public class TabGroupUiOneshotSupplier extends OneshotSupplierImpl<TabGroupUi> {

    /** Controller containing the logic that manages when the supplier is set with a value. */
    private static class TabGroupUiCreationController {
        private final TabObserver mTabObserver =
                new EmptyTabObserver() {
                    @Override
                    public void onTabGroupIdChanged(Tab tab, @Nullable Token tabGroupId) {
                        postMaybeCreateTabGroupUi(tab);
                    }
                };
        private final ValueChangedCallback<Tab> mActivityTabObserver =
                new ValueChangedCallback<>(this::onActivityTabChanged);
        private final ActivityTabProvider mActivityTabProvider;
        private final TabModelSelector mTabModelSelector;
        private @Nullable Runnable mSetter;
        private @Nullable CallbackController mCallbackController = new CallbackController();

        TabGroupUiCreationController(
                ActivityTabProvider activityTabProvider,
                TabModelSelector tabModelSelector,
                Runnable setter) {
            mSetter = setter;
            mActivityTabProvider = activityTabProvider;
            mTabModelSelector = tabModelSelector;
            activityTabProvider.addObserver(mActivityTabObserver);
        }

        void destroy() {
            if (mCallbackController != null) {
                mCallbackController.destroy();
                mCallbackController = null;
            }
            mActivityTabProvider.removeObserver(mActivityTabObserver);
            // Trigger a null new tab selection to effectively unregister the observer from the old
            // tab.
            mActivityTabObserver.onResult(null);
        }

        private void postMaybeCreateTabGroupUi(Tab tab) {
            if (mCallbackController == null) return;

            PostTask.postTask(
                    TaskTraits.UI_DEFAULT,
                    mCallbackController.makeCancelable(() -> maybeCreateTabGroupUi(tab)));
        }

        private void maybeCreateTabGroupUi(Tab tab) {
            if (mSetter == null) return;

            if (tab == null || tab.isClosing() || tab.isDestroyed()) return;

            boolean isInTabGroup =
                    mTabModelSelector
                            .getTabModelFilterProvider()
                            .getTabModelFilter(tab.isIncognito())
                            .isTabInTabGroup(tab);
            if (!isInTabGroup) return;

            mSetter.run();
            mSetter = null;
        }

        private void onActivityTabChanged(@Nullable Tab newTab, @Nullable Tab oldTab) {
            if (oldTab != null) {
                oldTab.removeObserver(mTabObserver);
            }

            if (newTab != null) {
                postMaybeCreateTabGroupUi(newTab);

                newTab.addObserver(mTabObserver);
            }
        }
    }

    private @Nullable TabGroupUiCreationController mTabGroupUiCreationController;

    /**
     * Constructs a specialized {@link OneshotSupplier} for {@link TabGroupUi}.
     *
     * @param activityTabProvider Gives access to the current tab.
     * @param tabModelSelector Gives access to the current set of {@TabModel}.
     * @param activity The {@link Activity} that creates this surface.
     * @param parentView The parent view of this UI.
     * @param browserControlsStateProvider The {@link BrowserControlsStateProvider} of the top
     *     controls.
     * @param incognitoStateProvider Observable provider of incognito state.
     * @param scrimCoordinator The {@link ScrimCoordinator} to control scrim view.
     * @param omniboxFocusStateSupplier Supplier to access the focus state of the omnibox.
     * @param bottomSheetController The {@link BottomSheetController} for the current activity.
     * @param dataSharingTabManager The {@link} DataSharingTabManager managing communication between
     *     UI and DataSharing services.
     * @param tabContentManager Gives access to the tab content.
     * @param tabCreatorManager Manages creation of tabs.
     * @param layoutStateProviderSupplier Supplies the {@link LayoutStateProvider}.
     * @param modalDialogManager Used to show confirmation dialogs.
     */
    public TabGroupUiOneshotSupplier(
            ActivityTabProvider activityTabProvider,
            TabModelSelector tabModelSelector,
            Activity activity,
            ViewGroup parentView,
            BrowserControlsStateProvider browserControlsStateProvider,
            IncognitoStateProvider incognitoStateProvider,
            ScrimCoordinator scrimCoordinator,
            ObservableSupplier<Boolean> omniboxFocusStateSupplier,
            BottomSheetController bottomSheetController,
            DataSharingTabManager dataSharingTabManager,
            TabContentManager tabContentManager,
            TabCreatorManager tabCreatorManager,
            OneshotSupplier<LayoutStateProvider> layoutStateProviderSupplier,
            ModalDialogManager modalDialogManager) {
        Runnable setter =
                () -> {
                    var tabGroupUi =
                            TabManagementDelegateProvider.getDelegate()
                                    .createTabGroupUi(
                                            activity,
                                            parentView,
                                            browserControlsStateProvider,
                                            incognitoStateProvider,
                                            scrimCoordinator,
                                            omniboxFocusStateSupplier,
                                            bottomSheetController,
                                            dataSharingTabManager,
                                            tabModelSelector,
                                            tabContentManager,
                                            tabCreatorManager,
                                            layoutStateProviderSupplier,
                                            modalDialogManager);
                    set(tabGroupUi);
                    maybeDestroyTabGroupUiCreationController();
                };
        mTabGroupUiCreationController =
                new TabGroupUiCreationController(activityTabProvider, tabModelSelector, setter);
    }

    /** Removes all callbacks and observers. */
    public void destroy() {
        maybeDestroyTabGroupUiCreationController();
    }

    private void maybeDestroyTabGroupUiCreationController() {
        if (mTabGroupUiCreationController != null) {
            mTabGroupUiCreationController.destroy();
            mTabGroupUiCreationController = null;
        }
    }
}