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

// Copyright 2019 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.content.Context;
import android.content.res.ColorStateList;
import android.view.View;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.base.ValueChangedCallback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_ui.RecyclerViewPosition;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorCoordinator.LifecycleObserver;
import org.chromium.chrome.browser.tasks.tab_management.TabProperties.TabActionState;
import org.chromium.chrome.browser.tasks.tab_management.TabUiMetricsHelper.TabListEditorExitMetricGroups;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler.BackPressResult;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
import org.chromium.ui.modelutil.ListModelChangeProcessor;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyListModel;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.util.TokenHolder;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * This class is the mediator that contains all business logic for TabListEditor component. It is
 * also responsible for resetting the selectable tab grid based on visibility property.
 */
class TabListEditorMediator
        implements TabListEditorCoordinator.TabListEditorController,
                TabListEditorAction.ActionDelegate {
    private final Context mContext;
    private final @NonNull ObservableSupplier<TabModelFilter> mCurrentTabModelFilterSupplier;
    private final @NonNull ValueChangedCallback<TabModelFilter> mOnTabModelFilterChanged =
            new ValueChangedCallback<>(this::onTabModelFilterChanged);
    private final PropertyModel mModel;
    private final SelectionDelegate<Integer> mSelectionDelegate;
    private final boolean mActionOnRelatedTabs;
    private final TabModelObserver mTabModelObserver;
    private final ObservableSupplierImpl<Boolean> mBackPressChangedSupplier =
            new ObservableSupplierImpl<>();
    private final List<Tab> mVisibleTabs = new ArrayList<>();
    private final TabListEditorLayout mTabListEditorLayout;

    private @Nullable TabListCoordinator mTabListCoordinator;
    private @Nullable TabListEditorCoordinator.ResetHandler mResetHandler;
    private PropertyListModel<PropertyModel, PropertyKey> mActionListModel;
    private ListModelChangeProcessor mActionChangeProcessor;
    private TabListEditorMenu mTabListEditorMenu;
    private SnackbarManager mSnackbarManager;
    private BottomSheetController mBottomSheetController;
    private TabListEditorToolbar mTabListEditorToolbar;
    private TabListEditorCoordinator.NavigationProvider mNavigationProvider;
    private @TabActionState int mTabActionState;
    private LifecycleObserver mLifecycleObserver;
    private int mSnackbarOverrideToken;

    private final View.OnClickListener mNavigationClickListener =
            new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mNavigationProvider.goBack();
                }
            };

    TabListEditorMediator(
            Context context,
            @NonNull ObservableSupplier<TabModelFilter> currentTabModelFilterSupplier,
            PropertyModel model,
            SelectionDelegate<Integer> selectionDelegate,
            boolean actionOnRelatedTabs,
            SnackbarManager snackbarManager,
            BottomSheetController bottomSheetController,
            TabListEditorLayout tabListEditorLayout,
            @TabActionState int initialTabActionState) {
        mContext = context;
        mCurrentTabModelFilterSupplier = currentTabModelFilterSupplier;
        mModel = model;
        mSelectionDelegate = selectionDelegate;
        mActionOnRelatedTabs = actionOnRelatedTabs;
        mSnackbarManager = snackbarManager;
        mBottomSheetController = bottomSheetController;
        mTabListEditorLayout = tabListEditorLayout;
        mTabActionState = initialTabActionState;

        mTabModelObserver =
                new TabModelObserver() {
                    @Override
                    public void didAddTab(
                            Tab tab,
                            int type,
                            @TabCreationState int creationState,
                            boolean markedForSelection) {
                        TabModelFilter filter = mCurrentTabModelFilterSupplier.get();
                        if (filter == null || !filter.isTabModelRestored()) return;
                        // When tab is added due to
                        // 1) multi-window close
                        // 2) moving between multiple windows
                        // 3) NTP at startup
                        // force hiding the selection editor.
                        if (type == TabLaunchType.FROM_RESTORE
                                || type == TabLaunchType.FROM_REPARENTING
                                || type == TabLaunchType.FROM_STARTUP) {
                            mNavigationProvider.goBack();
                        }
                    }

                    @Override
                    public void willCloseTab(Tab tab, boolean didCloseAlone) {
                        if (mTabActionState != TabProperties.TabActionState.CLOSABLE) {
                            mNavigationProvider.goBack();
                        }
                    }

                    // TODO(crbug.com/40945153): Revisit after adding the inactive tab model for
                    // using a custom click handler when selecting tabs.
                    @Override
                    public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) {
                        if (mTabActionState == TabProperties.TabActionState.CLOSABLE
                                && type == TabSelectionType.FROM_USER) {
                            mNavigationProvider.goBack();
                        }
                    }
                };

        mOnTabModelFilterChanged.onResult(
                mCurrentTabModelFilterSupplier.addObserver(mOnTabModelFilterChanged));

        mBackPressChangedSupplier.set(isEditorVisible());
        mModel.addObserver(
                (source, key) -> {
                    if (key == TabListEditorProperties.IS_VISIBLE) {
                        mBackPressChangedSupplier.set(isEditorVisible());
                    }
                });
    }

    private boolean isEditorVisible() {
        if (mTabListCoordinator == null) return false;
        return mModel.get(TabListEditorProperties.IS_VISIBLE);
    }

    private void updateColors(boolean isIncognito) {
        @ColorInt int primaryColor = ChromeColors.getPrimaryBackgroundColor(mContext, isIncognito);
        @ColorInt
        int toolbarBackgroundColor =
                TabUiThemeProvider.getTabSelectionToolbarBackground(mContext, isIncognito);
        ColorStateList toolbarTintColorList =
                TabUiThemeProvider.getTabSelectionToolbarIconTintList(mContext, isIncognito);

        mModel.set(TabListEditorProperties.PRIMARY_COLOR, primaryColor);
        mModel.set(TabListEditorProperties.TOOLBAR_BACKGROUND_COLOR, toolbarBackgroundColor);
        mModel.set(TabListEditorProperties.TOOLBAR_TEXT_TINT, toolbarTintColorList);
        mModel.set(TabListEditorProperties.TOOLBAR_BUTTON_TINT, toolbarTintColorList);

        if (mActionListModel == null) return;

        for (PropertyModel model : mActionListModel) {
            model.set(TabListEditorActionProperties.TEXT_TINT, toolbarTintColorList);
            model.set(TabListEditorActionProperties.ICON_TINT, toolbarTintColorList);
        }
    }

    public void initializeWithTabListCoordinator(
            TabListCoordinator tabListCoordinator,
            TabListEditorCoordinator.ResetHandler resetHandler) {
        mTabListCoordinator = tabListCoordinator;
        mTabListEditorToolbar = mTabListEditorLayout.getToolbar();
        mResetHandler = resetHandler;

        mModel.set(TabListEditorProperties.TOOLBAR_NAVIGATION_LISTENER, mNavigationClickListener);
        if (mActionOnRelatedTabs) {
            mModel.set(
                    TabListEditorProperties.RELATED_TAB_COUNT_PROVIDER,
                    (tabIdList) -> {
                        return TabListEditorAction.getTabCountIncludingRelatedTabs(
                                (TabGroupModelFilter) mCurrentTabModelFilterSupplier.get(),
                                tabIdList);
                    });
        }
        updateColors(mCurrentTabModelFilterSupplier.get().isIncognito());
    }

    /** {@link TabListEditorCoordinator.TabListEditorController} implementation. */
    @Override
    public void show(List<Tab> tabs, @Nullable RecyclerViewPosition recyclerViewPosition) {
        assert mNavigationProvider != null : "NavigationProvider must be set before calling #show";
        // Reparent the snackbarManager to use the selection editor layout to avoid layering issues.
        mSnackbarOverrideToken =
                mSnackbarManager.pushParentViewToOverrideStack(mTabListEditorLayout);
        // Records to a histogram the time since an instance of TabListEditor was last opened
        // within an activity lifespan.
        TabUiMetricsHelper.recordEditorTimeSinceLastShownHistogram();
        // We don't call TabListCoordinator#prepareTabSwitcherView, since not all the logic (e.g.
        // requiring one tab to be selected) is applicable here.
        mTabListCoordinator.prepareTabGridView();
        mVisibleTabs.clear();
        mVisibleTabs.addAll(tabs);
        mSelectionDelegate.setSelectionModeEnabledForZeroItems(true);

        mResetHandler.resetWithListOfTabs(tabs, recyclerViewPosition, /* quickMode= */ false);

        mModel.set(TabListEditorProperties.IS_VISIBLE, true);
        mModel.set(
                TabListEditorProperties.TOOLBAR_TITLE,
                mContext.getString(R.string.tab_selection_editor_toolbar_select_tabs));
        updateColors(mCurrentTabModelFilterSupplier.get().isIncognito());
    }

    @Override
    public void configureToolbarWithMenuItems(List<TabListEditorAction> actions) {
        // Deferred initialization.
        if (mActionListModel == null) {
            mActionListModel = new PropertyListModel<>();
            mTabListEditorMenu =
                    new TabListEditorMenu(
                            mContext, mTabListEditorToolbar.getActionViewLayout());
            mSelectionDelegate.addObserver(mTabListEditorMenu);
            mActionChangeProcessor =
                    new ListModelChangeProcessor(
                            mActionListModel,
                            mTabListEditorMenu,
                            new TabListEditorMenuAdapter());
            mActionListModel.addObserver(mActionChangeProcessor);
        }

        mActionListModel.clear();
        for (TabListEditorAction action : actions) {
            action.configure(
                    mCurrentTabModelFilterSupplier, mSelectionDelegate, this, mActionOnRelatedTabs);
            mActionListModel.add(action.getPropertyModel());
        }

        updateColors(mCurrentTabModelFilterSupplier.get().isIncognito());
    }

    @Override
    public boolean handleBackPressed() {
        if (!isEditorVisible()) return false;
        mNavigationProvider.goBack();
        return true;
    }

    @Override
    public @BackPressResult int handleBackPress() {
        int result = isEditorVisible() ? BackPressResult.SUCCESS : BackPressResult.FAILURE;
        mNavigationProvider.goBack();
        return result;
    }

    @Override
    public void syncRecyclerViewPosition() {
        mResetHandler.syncRecyclerViewPosition();
    }

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

    @Override
    public void hide() {
        hideInternal(/* hiddenByAction= */ false);
    }

    @Override
    public void hideByAction() {
        hideInternal(/* hiddenByAction= */ true);
    }

    private void hideInternal(boolean hiddenByAction) {
        if (!isEditorVisible()) return;
        if (mLifecycleObserver != null) mLifecycleObserver.willHide();
        mSnackbarManager.popParentViewFromOverrideStack(mSnackbarOverrideToken);
        mSnackbarOverrideToken = TokenHolder.INVALID_TOKEN;
        TabUiMetricsHelper.recordSelectionEditorExitMetrics(
                TabListEditorExitMetricGroups.CLOSED, mContext);

        // When hiding by action it is expected that syncRecyclerViewPosition() is called before the
        // action occurs. This is because an action may remove tabs so sync position must happen
        // first so the recyclerViewStat is valid due to the same number of items.
        if (!hiddenByAction) {
            syncRecyclerViewPosition();
        }
        mTabListCoordinator.cleanupTabGridView();
        mVisibleTabs.clear();
        mResetHandler.resetWithListOfTabs(
                null, /* recyclerViewPosition= */ null, /* quickMode= */ false);
        mModel.set(TabListEditorProperties.IS_VISIBLE, false);
        mResetHandler.postHiding();
        if (mLifecycleObserver != null) mLifecycleObserver.didHide();
    }

    @Override
    public boolean isVisible() {
        return isEditorVisible();
    }

    @Override
    public void setToolbarTitle(String title) {
        mModel.set(TabListEditorProperties.TOOLBAR_TITLE, title);
    }

    @Override
    public void setNavigationProvider(
            @NonNull TabListEditorCoordinator.NavigationProvider navigationProvider) {
        assert navigationProvider != null;
        mNavigationProvider = navigationProvider;
    }

    @Override
    public void setTabActionState(@TabActionState int tabActionState) {
        mTabActionState = tabActionState;
        mTabListCoordinator.setTabActionState(tabActionState);
    }

    @Override
    public void setLifecycleObserver(LifecycleObserver lifecycleObserver) {
        mLifecycleObserver = lifecycleObserver;
    }

    @Override
    public void selectAll() {
        Set<Integer> selectedTabIds = mSelectionDelegate.getSelectedItems();
        for (Tab tab : mVisibleTabs) {
            selectedTabIds.add(tab.getId());
        }
        mSelectionDelegate.setSelectedItems(selectedTabIds);
        mResetHandler.resetWithListOfTabs(
                mVisibleTabs, /* recyclerViewPosition= */ null, /* quickMode= */ true);
    }

    @Override
    public void deselectAll() {
        Set<Integer> selectedTabIds = mSelectionDelegate.getSelectedItems();
        selectedTabIds.clear();
        mSelectionDelegate.setSelectedItems(selectedTabIds);
        mResetHandler.resetWithListOfTabs(
                mVisibleTabs, /* recyclerViewPosition= */ null, /* quickMode= */ true);
    }

    @Override
    public boolean areAllTabsSelected() {
        Set<Integer> selectedTabIds = mSelectionDelegate.getSelectedItems();
        return selectedTabIds.size() == mVisibleTabs.size();
    }

    @Override
    public SnackbarManager getSnackbarManager() {
        return mSnackbarManager;
    }

    @Override
    public BottomSheetController getBottomSheetController() {
        return mBottomSheetController;
    }

    /** Destroy any members that needs clean up. */
    public void destroy() {
        removeTabModelFilterObserver(mCurrentTabModelFilterSupplier.get());
        mCurrentTabModelFilterSupplier.removeObserver(mOnTabModelFilterChanged);
    }

    private void onTabModelFilterChanged(
            @Nullable TabModelFilter newFilter, @Nullable TabModelFilter oldFilter) {
        removeTabModelFilterObserver(oldFilter);

        if (newFilter != null) {
            // Incognito in both light/dark theme is the same as non-incognito mode in dark theme.
            // Non-incognito mode and incognito in both light/dark themes in dark theme all look
            // dark.
            updateColors(newFilter.isIncognito());
            newFilter.addObserver(mTabModelObserver);
        }
    }

    private void removeTabModelFilterObserver(@Nullable TabModelFilter filter) {
        if (filter != null) {
            filter.removeObserver(mTabModelObserver);
        }
    }
}