chromium/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java

// Copyright 2015 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.tabmodel;

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

import org.chromium.base.MathUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.homepage.HomepageManager;
import org.chromium.chrome.browser.profiles.Profile;
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.TabContentManager;
import org.chromium.chrome.browser.tabmodel.NextTabPolicy.NextTabPolicySupplier;
import org.chromium.chrome.browser.tabmodel.PendingTabClosureManager.PendingTabClosureDelegate;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ResourceRequestBody;
import org.chromium.ui.mojom.WindowOpenDisposition;
import org.chromium.url.GURL;
import org.chromium.url.Origin;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This is the implementation of the synchronous {@link TabModel} for the {@link
 * ChromeTabbedActivity}.
 */
public class TabModelImpl extends TabModelJniBridge {
    /**
     * The application ID used for tabs opened from an application that does not specify an app ID
     * in its VIEW intent extras.
     */
    public static final String UNKNOWN_APP_ID = "com.google.android.apps.chrome.unknown_app";

    /**
     * The main list of tabs. Note that when this changes, all pending closures must be committed
     * via {@link #commitAllTabClosures()} as the indices are no longer valid. Also {@link
     * PendingTabClosureManager#resetState()} must be called so that the full model will be up to
     * date.
     */
    private final List<Tab> mTabs = new ArrayList<>();

    // Allows efficient lookup by tab id instead of index.
    private final Map<Integer, Tab> mTabIdToTabs = new HashMap<>();

    private final TabCreator mRegularTabCreator;
    private final TabCreator mIncognitoTabCreator;
    private final TabModelOrderController mOrderController;
    private final TabContentManager mTabContentManager;
    private final TabModelDelegate mModelDelegate;
    private final ObserverList<TabModelObserver> mObservers;
    private final NextTabPolicySupplier mNextTabPolicySupplier;
    private final AsyncTabParamsManager mAsyncTabParamsManager;
    private final ObservableSupplierImpl<Tab> mCurrentTabSupplier = new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Integer> mTabCountSupplier =
            new ObservableSupplierImpl<>();

    /** This specifies the current {@link Tab} in {@link #mTabs}. */
    private int mIndex = INVALID_TAB_INDEX;

    private boolean mActive;

    // Undo State Tracking -------------------------------------------------------------------------

    private PendingTabClosureManager mPendingTabClosureManager;

    /**
     * Implementation of {@link PendingTabClosureDelegate} that has access to the internal state of
     * TabModelImpl.
     */
    private class PendingTabClosureDelegateImpl implements PendingTabClosureDelegate {
        @Override
        public void insertUndoneTabClosureAt(Tab tab, int insertIndex) {
            if (mIndex >= insertIndex) mIndex++;
            assert !tab.isDestroyed() : "Attempting to undo tab that is destroyed.";
            mTabs.add(insertIndex, tab);
            mTabIdToTabs.put(tab.getId(), tab);
            mTabCountSupplier.set(mTabs.size());

            WebContents webContents = tab.getWebContents();
            if (webContents != null) webContents.setAudioMuted(false);

            // Start by setting a valid index to the restored tab if not already valid. This ensures
            // getting the current index is valid for any observers.
            boolean wasInvalidIndex = mIndex == INVALID_TAB_INDEX;
            if (wasInvalidIndex) {
                mIndex = insertIndex;
            }

            // Alert observers the tab closure was undone before calling setIndex if necessary as
            // * Observers may rely on this signal to re-introduce the tab to their visibility if it
            //   is selected before this it may not exist for those observers.
            // * UndoRefocusHelper may update the index out-of-band.
            for (TabModelObserver obs : mObservers) {
                obs.tabClosureUndone(tab);
            }

            // If the mIndex we set earlier is still in use then trigger a proper index update and
            // notify any observers.
            if (wasInvalidIndex && isActiveModel() && mIndex == insertIndex) {
                // Reset the index first so the event is raised properly as a index change and not
                // re-using the current index.
                mIndex = INVALID_TAB_INDEX;
                TabModelUtils.setIndex(TabModelImpl.this, insertIndex, TabSelectionType.FROM_UNDO);
            } else if (wasInvalidIndex && !isActiveModel()) {
                mCurrentTabSupplier.set(TabModelUtils.getCurrentTab(TabModelImpl.this));
            }
        }

        @Override
        public void finalizeClosure(Tab tab) {
            finalizeTabClosure(tab, true);
        }

        @Override
        public void notifyAllTabsClosureUndone() {
            for (TabModelObserver obs : mObservers) {
                obs.allTabsClosureUndone();
            }
        }

        @Override
        public void notifyOnFinishingMultipleTabClosure(List<Tab> tabs) {
            TabModelImpl.this.notifyOnFinishingMultipleTabClosure(
                    tabs, /* saveToTabRestoreService= */ true);
        }
    }

    public TabModelImpl(
            @NonNull Profile profile,
            @ActivityType int activityType,
            TabCreator regularTabCreator,
            TabCreator incognitoTabCreator,
            TabModelOrderController orderController,
            @NonNull TabContentManager tabContentManager,
            NextTabPolicySupplier nextTabPolicySupplier,
            AsyncTabParamsManager asyncTabParamsManager,
            TabModelDelegate modelDelegate,
            boolean supportUndo,
            boolean isArchivedTabModel) {
        super(profile, activityType, isArchivedTabModel);
        mRegularTabCreator = regularTabCreator;
        mIncognitoTabCreator = incognitoTabCreator;
        mOrderController = orderController;
        mTabContentManager = tabContentManager;
        assert mTabContentManager != null;
        mNextTabPolicySupplier = nextTabPolicySupplier;
        mAsyncTabParamsManager = asyncTabParamsManager;
        mModelDelegate = modelDelegate;
        mTabCountSupplier.set(0);
        if (supportUndo && !isIncognito()) {
            mPendingTabClosureManager =
                    new PendingTabClosureManager(this, new PendingTabClosureDelegateImpl());
        }
        mObservers = new ObserverList<TabModelObserver>();
        // The call to initializeNative() should be as late as possible, as it results in calling
        // observers on the native side, which may in turn call |addObserver()| on this object.
        initializeNative(profile);
    }

    @Override
    public void removeTab(Tab tab) {
        removeTabAndSelectNext(
                tab, null, TabSelectionType.FROM_CLOSE, false, true, TabCloseType.SINGLE);

        for (TabModelObserver obs : mObservers) obs.tabRemoved(tab);
    }

    @Override
    public void destroy() {
        commitAllTabClosures();
        for (Tab tab : mTabs) {
            // When reparenting tabs, we skip destroying tabs that we're intentionally keeping in
            // memory.
            if (mModelDelegate.isReparentingInProgress()
                    && mAsyncTabParamsManager.hasParamsForTabId(tab.getId())) {
                continue;
            }

            if (tab.isInitialized()) tab.destroy();
        }

        if (mPendingTabClosureManager != null) {
            if (mModelDelegate.isReparentingInProgress()) {
                mPendingTabClosureManager.destroyWhileReparentingInProgress();
            } else {
                mPendingTabClosureManager.destroy();
            }
        }
        mTabs.clear();
        mTabIdToTabs.clear();
        mTabCountSupplier.set(0);
        mObservers.clear();
        super.destroy();
    }

    @Override
    public void broadcastSessionRestoreComplete() {
        super.broadcastSessionRestoreComplete();

        // This is to make sure TabModel has a valid index when it has at least one valid Tab after
        // TabState is initialized. Otherwise, TabModel can have an invalid index even though it has
        // valid tabs, if the TabModel becomes active before any Tab is restored to that model.
        if (hasValidTab() && mIndex == INVALID_TAB_INDEX) {
            // Actually select the first tab if it is the active model, otherwise just set mIndex.
            if (isActiveModel()) {
                TabModelUtils.setIndex(this, 0);
            } else {
                mIndex = 0;
                mCurrentTabSupplier.set(TabModelUtils.getCurrentTab(this));
            }
        }

        for (TabModelObserver observer : mObservers) observer.restoreCompleted();
    }

    @Override
    public void addObserver(TabModelObserver observer) {
        mObservers.addObserver(observer);
    }

    @Override
    public void removeObserver(TabModelObserver observer) {
        mObservers.removeObserver(observer);
    }

    @Override
    public @NonNull ObservableSupplier<Integer> getTabCountSupplier() {
        return mTabCountSupplier;
    }

    /**
     * Initializes the newly created tab, adds it to controller, and dispatches creation
     * step notifications.
     */
    @Override
    public void addTab(
            Tab tab, int index, @TabLaunchType int type, @TabCreationState int creationState) {
        try {
            TraceEvent.begin("TabModelImpl.addTab");
            // TODO(crbug.com/40923859): Technically this should trigger NPEs downstream. Adding out
            // of an abundance of caution.
            assert tab != null : "Attempting to add a tab that is null to TabModel.";
            assert !mTabIdToTabs.containsKey(tab.getId())
                    : "Attempting to add a tab with a duplicate id=" + tab.getId();

            for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);

            boolean selectTab =
                    mOrderController.willOpenInForeground(type, isIncognitoBranded())
                            || (mTabs.size() == 0
                                    && type == TabLaunchType.FROM_LONGPRESS_BACKGROUND);

            index = mOrderController.determineInsertionIndex(type, index, tab);
            assert index <= mTabs.size();

            if (tab.isIncognito() != isIncognito()) {
                throw new IllegalStateException("Attempting to open tab in wrong model");
            }

            // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
            commitAllTabClosures();

            if (index < 0 || index > mTabs.size()) {
                mTabs.add(tab);
            } else {
                mTabs.add(index, tab);
                if (index <= mIndex) {
                    mIndex++;
                }
            }
            mTabIdToTabs.put(tab.getId(), tab);
            mTabCountSupplier.set(mTabs.size());

            if (!isActiveModel()) {
                // When adding new tabs in the background, make sure we set a valid index when the
                // first one is added. When in the foreground, calls to setIndex will take care of
                // this.
                mIndex = Math.max(mIndex, 0);
                if (!selectTab) {
                    mCurrentTabSupplier.set(TabModelUtils.getCurrentTab(this));
                }
            }

            if (supportsPendingClosures()) {
                mPendingTabClosureManager.resetState();
            }

            int newIndex = indexOf(tab);
            tabAddedToModel(tab);

            for (TabModelObserver obs : mObservers) {
                obs.didAddTab(tab, type, creationState, selectTab);
            }

            // setIndex takes care of making sure the appropriate model is active.
            if (selectTab) setIndex(newIndex, TabSelectionType.FROM_NEW);
        } finally {
            TraceEvent.end("TabModelImpl.addTab");
        }
    }

    @Override
    public void moveTab(int id, int newIndex) {
        newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());

        int curIndex = TabModelUtils.getTabIndexById(this, id);

        if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
            return;
        }

        // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
        commitAllTabClosures();

        Tab tab = mTabs.remove(curIndex);
        if (curIndex < newIndex) --newIndex;

        assert tab != null : "Attempting to move a tab that is null.";
        mTabs.add(newIndex, tab);

        if (curIndex == mIndex) {
            mIndex = newIndex;
        } else if (curIndex < mIndex && newIndex >= mIndex) {
            --mIndex;
        } else if (curIndex > mIndex && newIndex <= mIndex) {
            ++mIndex;
        }

        if (supportsPendingClosures()) {
            mPendingTabClosureManager.resetState();
        }

        for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
    }

    private Tab findTabInAllTabModels(int tabId) {
        Tab tab = mModelDelegate.getModel(isIncognito()).getTabById(tabId);
        if (tab != null) return tab;
        return mModelDelegate.getModel(!isIncognito()).getTabById(tabId);
    }

    private Tab findNearbyNotClosingTab(int closingIndex) {
        if (closingIndex > 0) {
            // Search for the first tab before the closing tab.
            for (int i = closingIndex - 1; i >= 0; i--) {
                Tab tab = getTabAt(i);
                if (!tab.isClosing()) {
                    return tab;
                }
            }
        }
        // If this is the first tab or all tabs before the closing tab are closed then search the
        // other direction.
        for (int i = closingIndex + 1; i < mTabs.size(); i++) {
            Tab tab = getTabAt(i);
            if (!tab.isClosing()) {
                return tab;
            }
        }
        return null;
    }

    @Override
    public Tab getNextTabIfClosed(int id, boolean uponExit) {
        return getNextTabIfClosed(id, uponExit, TabCloseType.SINGLE);
    }

    /**
     * See public getNextTabIfClosed documentation
     *
     * @param tabCloseType the type of tab closure occurring. This is used to avoid searching for a
     *     nearby tab when closing all tabs.
     */
    private Tab getNextTabIfClosed(int id, boolean uponExit, @TabCloseType int tabCloseType) {
        Tab tabToClose = getTabById(id);
        Tab currentTab = TabModelUtils.getCurrentTab(this);
        if (tabToClose == null) return currentTab;

        final boolean useCurrentTab =
                tabToClose != currentTab && currentTab != null && !currentTab.isClosing();

        int closingTabIndex = indexOf(tabToClose);
        Tab nearbyTab = null;
        if (tabCloseType != TabCloseType.ALL && !useCurrentTab) {
            nearbyTab = findNearbyNotClosingTab(closingTabIndex);
        }
        Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
        Tab nextMostRecentTab = null;
        if (uponExit) {
            nextMostRecentTab = TabModelUtils.getMostRecentTab(this, id);
        }

        // Determine which tab to select next according to these rules:
        //   * If closing a background tab, keep the current tab selected.
        //   * Otherwise, if closing the tab upon exit select the next most recent tab.
        //   * Otherwise, if not in overview mode, select the parent tab if it exists.
        //   * Otherwise, select a nearby tab if one exists.
        //   * Otherwise, if closing the last incognito tab, select the current normal tab.
        //   * Otherwise, select nothing.
        Tab nextTab = null;
        if (!isActiveModel()) {
            nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
        } else if (useCurrentTab) {
            nextTab = currentTab;
        } else if (nextMostRecentTab != null && !nextMostRecentTab.isClosing()) {
            nextTab = nextMostRecentTab;
        } else if (parentTab != null
                && !parentTab.isClosing()
                && mNextTabPolicySupplier.get() == NextTabPolicy.HIERARCHICAL) {
            nextTab = parentTab;
        } else if (nearbyTab != null && !nearbyTab.isClosing()) {
            nextTab = nearbyTab;
        } else if (isIncognito()) {
            nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
        }

        return nextTab != null && nextTab.isClosing() ? null : nextTab;
    }

    @Override
    public boolean isClosurePending(int tabId) {
        if (!supportsPendingClosures()) return false;

        return mPendingTabClosureManager.isClosurePending(tabId);
    }

    @Override
    public boolean supportsPendingClosures() {
        assert mPendingTabClosureManager == null || !isIncognito();
        return mPendingTabClosureManager != null;
    }

    @Override
    public TabList getComprehensiveModel() {
        if (!supportsPendingClosures()) return this;
        return mPendingTabClosureManager.getRewoundList();
    }

    @Override
    public void cancelTabClosure(int tabId) {
        if (!supportsPendingClosures()) return;

        mPendingTabClosureManager.cancelTabClosure(tabId);
    }

    @Override
    public void commitTabClosure(int tabId) {
        if (!supportsPendingClosures()) return;

        mPendingTabClosureManager.commitTabClosure(tabId);
    }

    @Override
    public void commitAllTabClosures() {
        if (!supportsPendingClosures()) return;

        mPendingTabClosureManager.commitAllTabClosures();

        for (TabModelObserver obs : mObservers) obs.allTabsClosureCommitted(isIncognito());
    }

    @Override
    public void notifyAllTabsClosureUndone() {
        if (!supportsPendingClosures()) return;

        mPendingTabClosureManager.notifyAllTabsClosureUndone();
    }

    /**
     * @param tabToClose The tab being closed.
     * @param recommendedNextTab The tab to select next unless some condition overrides it.
     * @param uponExit Whether the app is closing as a result of this tab closing.
     * @param allowUndo Whether undo of the closure is allowed.
     * @param notifyPending Whether or not to notify observers about the pending closure. If this is
     *     {@code true}, {@link #supportsPendingClosures()} is {@code true}, and allowUndo is {@code
     *     true}, observers will be notified of the pending closure. Observers will still be
     *     notified of a committed/cancelled closure even if they are not notified of a pending
     *     closure to start with.
     * @param tabCloseType Used to notify observers that this tab is closing by itself for {@link
     *     TabModelObserver#onFinishingMultipleTabClosure} if the closure cannot be undone and for
     *     {@link TabModelObserver#willCloseTab}. This should be {@code TabCloseType.SINGLE} if
     *     closing the tab by itself, {@code TabCloseType.MULTIPLE} if closing multiple tabs or
     *     {@code TabCloseType.ALL} all tabs (which also does additional optimization).
     * @return true if the closure succeeds and false otherwise.
     */
    private boolean closeTab(
            Tab tabToClose,
            Tab recommendedNextTab,
            boolean uponExit,
            boolean allowUndo,
            boolean notifyPending,
            @TabCloseType int tabCloseType) {
        if (tabToClose == null) {
            assert false : "Tab is null!";
            return false;
        }

        if (!containsTab(tabToClose)) {
            assert false : "Tried to close a tab from another model!";
            return false;
        }

        allowUndo &= supportsPendingClosures();

        startTabClosure(tabToClose, recommendedNextTab, uponExit, allowUndo, tabCloseType);
        if (notifyPending && allowUndo) {
            mPendingTabClosureManager.addTabClosureEvent(Collections.singletonList(tabToClose));
            for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
        }
        if (!allowUndo) {
            if (tabCloseType == TabCloseType.SINGLE) {
                notifyOnFinishingMultipleTabClosure(
                        Collections.singletonList(tabToClose), /* saveToTabRestoreService= */ true);
            }
            finalizeTabClosure(tabToClose, false);
        }
        return true;
    }

    private void closeMultipleTabs(
            List<Tab> tabs, boolean allowUndo, boolean saveToTabRestoreService) {
        assert (!allowUndo || saveToTabRestoreService)
                : "saveToTabRestoreService == false is ignored if allowUndo == true.";

        for (Tab tab : tabs) {
            if (!containsTab(tab)) {
                assert false : "Tried to close a tab from another model!";
                continue;
            }
            tab.setClosing(true);
        }
        allowUndo &= supportsPendingClosures();
        if (!allowUndo) {
            notifyOnFinishingMultipleTabClosure(tabs, saveToTabRestoreService);
        }
        for (TabModelObserver obs : mObservers) obs.willCloseMultipleTabs(allowUndo, tabs);
        for (Tab tab : tabs) {
            closeTab(tab, null, false, allowUndo, false, TabCloseType.MULTIPLE);
        }
        if (allowUndo) {
            mPendingTabClosureManager.addTabClosureEvent(tabs);
            for (TabModelObserver obs : mObservers) obs.multipleTabsPendingClosure(tabs, false);
        }
    }

    private void closeAllTabs(boolean uponExit) {
        for (TabModelObserver obs : mObservers) obs.willCloseAllTabs(isIncognito());

        // Force close immediately upon exit or if Chrome needs to close with a zero-state.
        if (uponExit || HomepageManager.getInstance().shouldCloseAppWithZeroTabs()) {
            commitAllTabClosures();

            for (int i = 0; i < getCount(); i++) getTabAt(i).setClosing(true);
            notifyOnFinishingMultipleTabClosure(mTabs, /* saveToTabRestoreService= */ true);
            while (getCount() > 0) {
                Tab tab = getTabAt(0);
                closeTab(tab, null, uponExit, false, false, TabCloseType.ALL);
            }
            return;
        }

        // Close with the opportunity to undo if this TabModel supports pending closures.
        for (int i = 0; i < getCount(); i++) getTabAt(i).setClosing(true);
        List<Tab> closedTabs = new ArrayList<>(mTabs);
        if (!supportsPendingClosures()) {
            notifyOnFinishingMultipleTabClosure(closedTabs, /* saveToTabRestoreService= */ true);
        }
        while (getCount() > 0) {
            Tab tab = getTabAt(0);
            closeTab(tab, null, false, true, false, TabCloseType.ALL);
        }

        if (supportsPendingClosures()) {
            mPendingTabClosureManager.addTabClosureEvent(closedTabs);
            for (TabModelObserver obs : mObservers) {
                obs.multipleTabsPendingClosure(closedTabs, true);
            }
        }
    }

    @Override
    public Tab getTabAt(int index) {
        // This will catch INVALID_TAB_INDEX and return null
        if (index < 0 || index >= mTabs.size()) return null;
        return mTabs.get(index);
    }

    @Override
    public @Nullable Tab getTabById(int tabId) {
        return mTabIdToTabs.get(tabId);
    }

    @Override
    public boolean closeTabs(TabClosureParams tabClosureParams) {
        // TODO(crbug.com/356445932): Respect the provided params more broadly.
        switch (tabClosureParams.tabCloseType) {
            case TabCloseType.SINGLE:
                assert tabClosureParams.tabs.size() == 1;
                boolean notifyPending = tabClosureParams.allowUndo;
                return closeTab(
                        tabClosureParams.tabs.get(0),
                        tabClosureParams.recommendedNextTab,
                        tabClosureParams.uponExit,
                        tabClosureParams.allowUndo,
                        notifyPending,
                        tabClosureParams.tabCloseType);
            case TabCloseType.MULTIPLE:
                closeMultipleTabs(
                        tabClosureParams.tabs,
                        tabClosureParams.allowUndo,
                        tabClosureParams.saveToTabRestoreService);
                return true;
            case TabCloseType.ALL:
                closeAllTabs(tabClosureParams.uponExit);
                return true;
            default:
                assert false : "Not reached.";
                return false;
        }
    }

    // Index of the given tab in the order of the tab stack.
    @Override
    public int indexOf(Tab tab) {
        if (tab == null) return INVALID_TAB_INDEX;
        int retVal = mTabs.indexOf(tab);
        return retVal == -1 ? INVALID_TAB_INDEX : retVal;
    }

    // TODO(aurimas): Move this method to TabModelSelector when notifications move there.
    private int getLastId(@TabSelectionType int type) {
        if (type == TabSelectionType.FROM_CLOSE || type == TabSelectionType.FROM_EXIT) {
            return Tab.INVALID_TAB_ID;
        }

        // Get the current tab in the current tab model.
        Tab currentTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
        return currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
    }

    private boolean hasValidTab() {
        if (mTabs.size() <= 0) return false;
        for (int i = 0; i < mTabs.size(); i++) {
            if (!mTabs.get(i).isClosing()) return true;
        }
        return false;
    }

    private boolean containsTab(Tab tab) {
        return mTabIdToTabs.containsKey(tab.getId());
    }

    @Override
    public ObservableSupplier<Tab> getCurrentTabSupplier() {
        return mCurrentTabSupplier;
    }

    // This function is complex and its behavior depends on persisted state, including mIndex.
    @Override
    public void setIndex(int i, final @TabSelectionType int type) {
        try {
            TraceEvent.begin("TabModelImpl.setIndex");
            int lastId = getLastId(type);

            // This can cause recursive entries into setIndex, which causes duplicate notifications
            // and UMA records.
            if (!isActiveModel()) mModelDelegate.selectModel(isIncognito());

            if (!hasValidTab()) {
                mIndex = INVALID_TAB_INDEX;
            } else {
                mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
            }

            Tab tab = TabModelUtils.getCurrentTab(this);

            mModelDelegate.requestToShowTab(tab, type);

            mCurrentTabSupplier.set(tab);
            if (tab != null) {
                for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);

                boolean wasAlreadySelected = tab.getId() == lastId;
                if (!wasAlreadySelected && type == TabSelectionType.FROM_USER) {
                    // We only want to record when the user actively switches to a different tab.
                    RecordUserAction.record("MobileTabSwitched");
                }
            }
        } finally {
            TraceEvent.end("TabModelImpl.setIndex");
        }
    }

    @Override
    public boolean isActiveModel() {
        return mActive;
    }

    /**
     * Performs the necessary actions to remove this {@link Tab} from this {@link TabModel}. This
     * does not actually destroy the {@link Tab} (see {@link #finalizeTabClosure(Tab)}.
     *
     * @param tab The {@link Tab} to remove from this {@link TabModel}.
     * @param uponExit Whether or not this is closing while the Activity is exiting.
     * @param allowUndo Whether or not this operation can be undone. Note that if this is {@code
     *     true} and {@link #supportsPendingClosures()} is {@code true}, {@link
     *     #commitTabClosure(int)} or {@link #commitAllTabClosures()} needs to be called to actually
     *     delete and clean up {@code tab}.
     * @param tabCloseType Used to notify observers that this tab is closing by itself for {@link
     *     TabModelObserver#onFinishingMultipleTabClosure} if the closure cannot be undone and for
     *     {@link TabModelObserver#willCloseTab}. This should be {@code TabCloseType.SINGLE} if
     *     closing the tab by itself, {@code TabCloseType.MULTIPLE} if closing multiple tabs or
     *     {@code TabCloseType.ALL} all tabs (which also does additional optimization).
     */
    private void startTabClosure(
            Tab tab,
            Tab recommendedNextTab,
            boolean uponExit,
            boolean allowUndo,
            @TabCloseType int tabCloseType) {
        tab.setClosing(true);

        for (TabModelObserver obs : mObservers) {
            obs.willCloseTab(tab, tabCloseType == TabCloseType.SINGLE);
        }

        @TabSelectionType
        int selectionType = uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE;
        boolean pauseMedia = allowUndo;
        boolean updatePendingTabClosureManager = !allowUndo;
        removeTabAndSelectNext(
                tab,
                recommendedNextTab,
                selectionType,
                pauseMedia,
                updatePendingTabClosureManager,
                tabCloseType);
    }

    /** Removes the given tab from the tab model and selects a new tab. */
    private void removeTabAndSelectNext(
            Tab tab,
            Tab recommendedNextTab,
            @TabSelectionType int selectionType,
            boolean pauseMedia,
            boolean updatePendingTabClosureManager,
            @TabCloseType int tabCloseType) {
        assert selectionType == TabSelectionType.FROM_CLOSE
                || selectionType == TabSelectionType.FROM_EXIT;

        final int closingTabId = tab.getId();
        final int closingTabIndex = indexOf(tab);

        Tab currentTabInModel = TabModelUtils.getCurrentTab(this);
        Tab adjacentTabInModel = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
        Tab nextTab =
                recommendedNextTab == null
                        ? getNextTabIfClosed(closingTabId, /* uponExit= */ false, tabCloseType)
                        : recommendedNextTab;

        // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
        if (updatePendingTabClosureManager) commitAllTabClosures();

        // Cancel or mute any media currently playing.
        if (pauseMedia) {
            WebContents webContents = tab.getWebContents();
            if (webContents != null) {
                webContents.suspendAllMediaPlayers();
                webContents.setAudioMuted(true);
            }
        }

        mTabs.remove(tab);
        mTabIdToTabs.remove(tab.getId());
        mTabCountSupplier.set(mTabs.size());

        boolean nextIsIncognito = nextTab == null ? false : nextTab.isIncognito();
        int nextTabId = nextTab == null ? Tab.INVALID_TAB_ID : nextTab.getId();
        int nextTabIndex =
                nextTab == null
                        ? INVALID_TAB_INDEX
                        : TabModelUtils.getTabIndexById(
                                mModelDelegate.getModel(nextIsIncognito), nextTabId);

        if (nextTab != currentTabInModel) {
            if (nextIsIncognito != isIncognito()) {
                mIndex = indexOf(adjacentTabInModel);
            }

            TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
            nextModel.setIndex(nextTabIndex, selectionType);
        } else {
            mIndex = nextTabIndex;
            mCurrentTabSupplier.set(TabModelUtils.getCurrentTab(this));
        }

        if (updatePendingTabClosureManager && supportsPendingClosures()) {
            mPendingTabClosureManager.resetState();
        }
    }

    /**
     * Actually closes and cleans up {@code tab}.
     * @param tab The {@link Tab} to close.
     * @param notifyTabClosureCommitted If true then observers will receive a tabClosureCommitted
     *     notification.
     */
    private void finalizeTabClosure(Tab tab, boolean notifyTabClosureCommitted) {
        mTabContentManager.removeTabThumbnail(tab.getId());

        for (TabModelObserver obs : mObservers) obs.onFinishingTabClosure(tab);
        if (notifyTabClosureCommitted) {
            for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
        }

        // Destroy the native tab after the observer notifications have fired, otherwise they risk a
        // use after free or null dereference.
        tab.destroy();
    }

    @Override
    protected boolean closeTabAt(int index) {
        return closeTabs(TabClosureParams.closeTab(getTabAt(index)).allowUndo(false).build());
    }

    @Override
    protected TabCreator getTabCreator(boolean incognito) {
        return incognito ? mIncognitoTabCreator : mRegularTabCreator;
    }

    /** Used to restore tabs from native. */
    @Override
    protected boolean createTabWithWebContents(
            Tab parent, Profile profile, WebContents webContents, boolean select) {
        return getTabCreator(profile.isOffTheRecord())
                .createTabWithWebContents(
                        parent,
                        webContents,
                        select
                                ? TabLaunchType.FROM_RECENT_TABS_FOREGROUND
                                : TabLaunchType.FROM_RECENT_TABS);
    }

    @Override
    public void openNewTab(
            Tab parent,
            GURL url,
            @Nullable Origin initiatorOrigin,
            String extraHeaders,
            ResourceRequestBody postData,
            int disposition,
            boolean persistParentage,
            boolean isRendererInitiated) {
        if (parent.isClosing()) return;

        boolean incognito = parent.isIncognito();
        @TabLaunchType int tabLaunchType = TabLaunchType.FROM_LONGPRESS_FOREGROUND;

        switch (disposition) {
            case WindowOpenDisposition.NEW_WINDOW: // fall through
            case WindowOpenDisposition.NEW_FOREGROUND_TAB:
                break;
            case WindowOpenDisposition.NEW_POPUP: // fall through
            case WindowOpenDisposition.NEW_BACKGROUND_TAB:
                tabLaunchType = TabLaunchType.FROM_LONGPRESS_BACKGROUND;
                break;
            case WindowOpenDisposition.OFF_THE_RECORD:
                incognito = true;
                break;
            default:
                assert false;
        }

        LoadUrlParams loadUrlParams = new LoadUrlParams(url);
        loadUrlParams.setInitiatorOrigin(initiatorOrigin);
        loadUrlParams.setVerbatimHeaders(extraHeaders);
        loadUrlParams.setPostData(postData);
        loadUrlParams.setIsRendererInitiated(isRendererInitiated);
        getTabCreator(incognito)
                .createNewTab(loadUrlParams, tabLaunchType, persistParentage ? parent : null);
    }

    @Override
    public int getCount() {
        return mTabs.size();
    }

    @Override
    public int index() {
        return mIndex;
    }

    @Override
    protected boolean isSessionRestoreInProgress() {
        return mModelDelegate.isSessionRestoreInProgress();
    }

    @Override
    public void openMostRecentlyClosedEntry() {
        // First try to recover tab from rewound list.
        if (supportsPendingClosures() && mPendingTabClosureManager.openMostRecentlyClosedEntry()) {
            return;
        }

        // If there are no pending closures in the rewound list, then try to restore from the native
        // tab restore service.
        mModelDelegate.openMostRecentlyClosedEntry(this);
        // If there is only one tab, select it.
        if (getCount() == 1) setIndex(0, TabSelectionType.FROM_NEW);
    }

    @Override
    public void setActive(boolean active) {
        mActive = active;
    }

    @Override
    public int getTabCountNavigatedInTimeWindow(long beginTimeMs, long endTimeMs) {
        return getTabsNavigatedInTimeWindow(beginTimeMs, endTimeMs).size();
    }

    @Override
    public void closeTabsNavigatedInTimeWindow(long beginTimeMs, long endTimeMs) {
        List<Tab> tabsToClose = getTabsNavigatedInTimeWindow(beginTimeMs, endTimeMs);
        if (tabsToClose.isEmpty()) return;

        final TabModelFilter filter = TabModelUtils.getTabModelFilterByTab(tabsToClose.get(0));

        assert filter instanceof TabGroupModelFilter;
        final TabGroupModelFilter groupingFilter = (TabGroupModelFilter) filter;

        var params =
                TabClosureParams.closeTabs(tabsToClose)
                        .allowUndo(false)
                        .saveToTabRestoreService(false)
                        .build();
        groupingFilter.closeTabs(params);
    }

    @VisibleForTesting
    List<Tab> getTabsNavigatedInTimeWindow(long beginTimeMs, long endTimeMs) {
        List<Tab> tabList = new ArrayList<>();
        for (Tab tab : mTabs) {
            if (tab.isCustomTab()) continue;

            final long recentNavigationTime = tab.getLastNavigationCommittedTimestampMillis();
            if (recentNavigationTime >= beginTimeMs && recentNavigationTime < endTimeMs) {
                tabList.add(tab);
            }
        }

        return tabList;
    }

    private void notifyOnFinishingMultipleTabClosure(
            List<Tab> tabs, boolean saveToTabRestoreService) {
        for (TabModelObserver obs : mObservers) {
            obs.onFinishingMultipleTabClosure(tabs, saveToTabRestoreService);
        }
    }
}