chromium/chrome/android/java/src/org/chromium/chrome/browser/UndoRefocusHelper.java

// Copyright 2022 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;

import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/** Refocus on previously selected tab if the selected tab closure was undone. */
public class UndoRefocusHelper {
    private final Set<Integer> mTabsClosedFromTabStrip;
    private final TabModelSelector mModelSelector;
    private final ObservableSupplier<LayoutManagerImpl> mLayoutManagerObservableSupplier;

    private LayoutManagerImpl mLayoutManager;
    private LayoutStateProvider.LayoutStateObserver mLayoutStateObserver;
    private TabModelSelectorTabModelObserver mTabModelSelectorTabModelObserver;
    private int mSelectedTabIdWhenTabClosed = Tab.INVALID_TAB_ID;
    private boolean mTabSwitcherActive;
    private Callback<LayoutManagerImpl> mLayoutManagerSupplierCallback;
    private boolean mIsTablet;

    /**
     * @param modelSelector TabModelSelector used to subscribe to TabModelSelectorTabModelObserver
     *     to capture when tabs are being closed or the closure is being undone.
     * @param layoutManagerObservableSupplier This supplies the LayoutManager implementation to
     *     observe the layout state when it's available.
     * @param isTablet Whether the current device is a tablet.
     */
    public UndoRefocusHelper(
            TabModelSelector modelSelector,
            ObservableSupplier<LayoutManagerImpl> layoutManagerObservableSupplier,
            boolean isTablet) {
        mLayoutManagerObservableSupplier = layoutManagerObservableSupplier;
        mModelSelector = modelSelector;
        mTabsClosedFromTabStrip = new HashSet<>();
        mTabSwitcherActive = false;
        mIsTablet = isTablet;

        observeTabModel();
        observeLayoutState();
    }

    public void destroy() {
        mTabModelSelectorTabModelObserver.destroy();
        mLayoutManagerObservableSupplier.removeObserver(mLayoutManagerSupplierCallback);
        if (mLayoutManager != null) {
            mLayoutManager.removeObserver(mLayoutStateObserver);
        }
    }

    private void observeTabModel() {
        mTabModelSelectorTabModelObserver =
                new TabModelSelectorTabModelObserver(mModelSelector) {
                    @Override
                    public void willCloseTab(Tab tab, boolean didCloseAlone) {
                        // Tabs not closed alone are handled in #willCloseMultipleTabs and
                        // #willCloseAllTabs
                        if (!didCloseAlone || tab.isIncognito()) return;

                        int tabId = tab.getId();
                        if (!mTabSwitcherActive && mIsTablet) {
                            mTabsClosedFromTabStrip.add(tabId);
                        }

                        maybeSetSelectedTabId(tab);
                    }

                    @Override
                    public void willCloseMultipleTabs(boolean allowUndo, List<Tab> tabs) {
                        if (!allowUndo || tabs.isEmpty()) return;

                        // Record metric only once for the set.
                        // Use the first id to track the set.
                        if (!mTabSwitcherActive && mIsTablet) {
                            mTabsClosedFromTabStrip.add(tabs.get(0).getId());
                        }
                        for (Tab tab : tabs) {
                            if (maybeSetSelectedTabId(tab)) {
                                break;
                            }
                        }
                    }

                    @Override
                    public void willCloseAllTabs(boolean incognito) {
                        if (incognito) return;
                        int selectedTabIdx = mModelSelector.getModel(false).index();
                        // Selected tab will be invalid when there are already no tabs in a window
                        // when this method is invoked.
                        if (selectedTabIdx == TabList.INVALID_TAB_INDEX) return;

                        Tab selectedTab = mModelSelector.getModel(false).getTabAt(selectedTabIdx);
                        maybeSetSelectedTabId(selectedTab);
                        // Record metric only once for the set.
                        // Use the selected id to track the set.
                        if (!mTabSwitcherActive && mIsTablet) {
                            mTabsClosedFromTabStrip.add(selectedTab.getId());
                        }
                    }

                    @Override
                    public void didSelectTab(Tab tab, int type, int lastId) {
                        // Undoing a selected tab closure, after manually switching tabs shouldn't
                        // switch focus to the reopened tab.
                        if (type == TabSelectionType.FROM_USER
                                || type == TabSelectionType.FROM_OMNIBOX
                                || type == TabSelectionType.FROM_NEW) {
                            resetSelectionsForUndo();
                        }
                    }

                    @Override
                    public void tabClosureUndone(Tab tab) {
                        int id = tab.getId();
                        recordClosureCancellation(id);
                        if (mSelectedTabIdWhenTabClosed == id) {
                            selectPreviouslySelectedTab();
                        }
                    }

                    @Override
                    public void allTabsClosureUndone() {
                        if (mSelectedTabIdWhenTabClosed != Tab.INVALID_TAB_ID) {
                            selectPreviouslySelectedTab();
                        }

                        resetSelectionsForUndo();
                        mTabsClosedFromTabStrip.clear();
                    }

                    @Override
                    public void tabClosureCommitted(Tab tab) {
                        if (!tab.isIncognito()) {
                            if (tab.getId() == mSelectedTabIdWhenTabClosed) {
                                resetSelectionsForUndo();
                            }
                            mTabsClosedFromTabStrip.remove(tab.getId());
                        }
                    }

                    @Override
                    public void allTabsClosureCommitted(boolean isIncognito) {
                        if (!isIncognito) {
                            resetSelectionsForUndo();
                            mTabsClosedFromTabStrip.clear();
                        }
                    }

                    private boolean maybeSetSelectedTabId(Tab tab) {
                        TabModel model = mModelSelector.getModel(false);
                        int tabId = tab.getId();
                        int selTabIndex = model.index();
                        if (selTabIndex != TabModel.INVALID_TAB_INDEX
                                && selTabIndex < model.getCount()) {
                            Tab selectedTab = model.getTabAt(selTabIndex);
                            if (selectedTab != null && tabId == selectedTab.getId()) {
                                mSelectedTabIdWhenTabClosed = tabId;
                                return true;
                            }
                        }
                        return false;
                    }

                    private void recordClosureCancellation(int id) {
                        // Only record the action if the tab was previously closed from the tab
                        // strip.
                        if (mTabsClosedFromTabStrip.contains(id)) {
                            RecordUserAction.record("TabletTabStrip.UndoCloseTab");
                            mTabsClosedFromTabStrip.remove(id);
                        }
                    }
                };
    }

    private void observeLayoutState() {
        mLayoutManagerSupplierCallback = this::onLayoutManagerAvailable;
        mLayoutManagerObservableSupplier.addObserver(mLayoutManagerSupplierCallback);
        @Nullable LayoutManagerImpl layoutManager = mLayoutManagerObservableSupplier.get();
        if (layoutManager != null && layoutManager.isLayoutVisible(LayoutType.TAB_SWITCHER)) {
            mTabSwitcherActive = true;
        }
    }

    private void onLayoutManagerAvailable(LayoutManagerImpl layoutManager) {
        mLayoutManager = layoutManager;
        mLayoutStateObserver =
                new LayoutStateProvider.LayoutStateObserver() {
                    @Override
                    public void onFinishedShowing(int layoutType) {
                        if (layoutType != LayoutType.TAB_SWITCHER) {
                            return;
                        }
                        mTabSwitcherActive = true;
                    }

                    @Override
                    public void onFinishedHiding(int layoutType) {
                        if (layoutType != LayoutType.TAB_SWITCHER) {
                            return;
                        }
                        mTabSwitcherActive = false;
                    }
                };

        mLayoutManager.addObserver(mLayoutStateObserver);
    }

    /** If a tab closure is undone, this selects tab if it was previously selected. */
    private void selectPreviouslySelectedTab() {
        if (mSelectedTabIdWhenTabClosed == Tab.INVALID_TAB_ID) return;

        TabModelUtils.selectTabById(
                mModelSelector, mSelectedTabIdWhenTabClosed, TabSelectionType.FROM_UNDO);
        resetSelectionsForUndo();
    }

    /**
     * After a tab closure has been committed or user manually selects a different tab, these values
     * are reset so the next undo closure action does not reselect the reopened tab.
     */
    private void resetSelectionsForUndo() {
        mSelectedTabIdWhenTabClosed = Tab.INVALID_TAB_ID;
    }
}