chromium/chrome/browser/tabmodel/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelFilter.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.tabmodel;

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

import org.chromium.base.ObserverList;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;

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

/**
 * This class is responsible for filtering tabs from {@link TabModel}. The filtering logic is
 * delegated to the concrete class that extends this abstract class. If no filter is active, this
 * class has the same {@link TabList} as {@link TabModel} does.
 *
 * If there is at least one filter active, this is a {@link TabList} that contains the most
 * important tabs that the filter defines.
 */
public abstract class TabModelFilter implements TabModelObserver, TabList {
    private static final List<Tab> sEmptyRelatedTabList =
            Collections.unmodifiableList(new ArrayList<Tab>());
    private static final List<Integer> sEmptyRelatedTabIds =
            Collections.unmodifiableList(new ArrayList<Integer>());
    private TabModel mTabModel;
    protected ObserverList<TabModelObserver> mFilteredObservers = new ObserverList<>();
    private boolean mTabRestoreCompleted;
    private boolean mTabStateInitialized;

    public TabModelFilter(TabModel tabModel) {
        mTabModel = tabModel;
        mTabModel.addObserver(this);
    }

    /**
     * Adds a {@link TabModelObserver} to be notified on {@link TabModelFilter} changes.
     * @param observer The {@link TabModelObserver} to add.
     */
    public void addObserver(TabModelObserver observer) {
        mFilteredObservers.addObserver(observer);
    }

    /**
     * Removes a {@link TabModelObserver}.
     * @param observer The {@link TabModelObserver} to remove.
     */
    public void removeObserver(TabModelObserver observer) {
        mFilteredObservers.removeObserver(observer);
    }

    public boolean isCurrentlySelectedFilter() {
        return getTabModel().isActiveModel();
    }

    /**
     * Mark TabState initialized, and TabModelFilter ready to use. This should only be called once,
     * and should only be called by {@link TabModelFilterProvider}.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public void markTabStateInitialized() {
        assert !mTabStateInitialized;
        mTabStateInitialized = true;
    }

    /**
     * To be called when this filter should be destroyed. This filter should no longer be used after
     * this.
     */
    public void destroy() {
        mFilteredObservers.clear();
    }

    /** Returns the {@link TabModel} that the filter is acting on. */
    public TabModel getTabModel() {
        return mTabModel;
    }

    /**
     * @return The total tab count in the underlying {@link TabModel}.
     */
    public int getTotalTabCount() {
        return mTabModel.getCount();
    }

    /**
     * Any of the concrete class can override and define a relationship that links a {@link Tab} to
     * a list of related {@link Tab}s. By default, this returns an unmodifiable list that only
     * contains the {@link Tab} with the given id. Note that the meaning of related can vary
     * depending on the filter being applied.
     *
     * @param tabId Id of the {@link Tab} try to relate with.
     * @return An unmodifiable list of {@link Tab} that relate with the given tab id.
     */
    @NonNull
    public List<Tab> getRelatedTabList(int tabId) {
        Tab tab = getTabModel().getTabById(tabId);
        if (tab == null) return sEmptyRelatedTabList;
        List<Tab> relatedTab = new ArrayList<>();
        relatedTab.add(tab);
        return Collections.unmodifiableList(relatedTab);
    }

    /**
     * Any of the concrete class can override and define a relationship that links a {@link Tab} to
     * a list of related {@link Tab}s. By default, this returns an unmodifiable list that only
     * contains the given id. Note that the meaning of related can vary depending on the filter
     * being applied.
     *
     * @param tabId Id of the {@link Tab} try to relate with.
     * @return An unmodifiable list of id that relate with the given tab id.
     */
    @NonNull
    public List<Integer> getRelatedTabIds(int tabId) {
        Tab tab = getTabModel().getTabById(tabId);
        if (tab == null) return sEmptyRelatedTabIds;
        List<Integer> relatedTabIds = new ArrayList<>();
        relatedTabIds.add(tabId);
        return Collections.unmodifiableList(relatedTabIds);
    }

    // TODO(crbug.com/41496693): This method sort of breaks the encapsulation of TabGroups being a
    // concept of TabGroupModelFilter and TabModelFilter being generic. We could call it something
    // like hasRelationship, but at this point there is only one valid implementation of
    // TabModelFilter and we should fold TabGroupModelFilter into TabModel eventually so breaking
    // encapsulation to be more clear when adding that groups of size one seems like a reasonable
    // tradeoff.
    /**
     * @param tab A {@link Tab} to check group membership of.
     * @return Whether the given {@link Tab} is part of a tab group.
     */
    public boolean isTabInTabGroup(Tab tab) {
        return false;
    }

    /**
     * Returns a valid position to add or move a tab to this model in the context of any related
     * tabs.
     * @param tab The tab to be added/moved.
     * @param proposedPosition The current or proposed position of the tab in the model.
     * @return a valid position close to proposedPosition that respects related tab ordering rules.
     */
    public abstract int getValidPosition(Tab tab, int proposedPosition);

    /**
     * Concrete class requires to define what's the behavior when {@link TabModel} added a {@link
     * Tab}.
     *
     * @param tab {@link Tab} had added to {@link TabModel}.
     * @param fromUndo Whether the tab was added by undo.
     */
    protected abstract void addTab(Tab tab, boolean fromUndo);

    /**
     * Concrete class requires to define what's the behavior when {@link TabModel} closed a {@link
     * Tab}.
     *
     * @param tab {@link Tab} had closed from {@link TabModel}.
     */
    protected abstract void closeTab(Tab tab);

    /**
     * Concrete class requires to define what's the behavior when {@link TabModel} selected a
     * {@link Tab}.
     * @param tab {@link Tab} had selected.
     */
    protected abstract void selectTab(Tab tab);

    /** Concrete class requires to define the ordering of each Tab within the filter. */
    protected abstract void reorder();

    /** Concrete class requires to define what to clean up. */
    protected abstract void resetFilterStateInternal();

    /**
     * @return Whether the tab model is fully restored.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    public boolean isTabModelRestored() {
        // TODO(crbug.com/40130477): Remove |mTabRestoreCompleted|. |mTabRestoreCompleted| is always
        // false for incognito, while |mTabStateInitialized| is not. |mTabStateInitialized| is
        // marked after the TabModelSelector is initialized, therefore it is the true state of the
        // TabModel.
        return mTabRestoreCompleted || mTabStateInitialized;
    }

    /**
     * Concrete class requires to define what's the behavior when {@link TabModel} removed a
     * {@link Tab}.
     * @param tab {@link Tab} had removed.
     */
    protected abstract void removeTab(Tab tab);

    /**
     * Calls {@code resetFilterStateInternal} method to clean up filter internal data, and resets
     * the internal data based on the current {@link TabModel}.
     */
    protected void resetFilterState() {
        resetFilterStateInternal();

        TabModel tabModel = getTabModel();
        for (int i = 0; i < tabModel.getCount(); i++) {
            Tab tab = tabModel.getTabAt(i);
            addTab(tab, /* fromUndo= */ false);
        }
    }

    // TODO(crbug.com/41450619): This is a band-aid fix for not crashing when undo the last closed
    // tab, should remove later.
    /**
     * @return Whether filter should notify observers about the SetIndex call.
     */
    protected boolean shouldNotifyObserversOnSetIndex() {
        return true;
    }

    // TabModelObserver implementation.
    @Override
    public void didSelectTab(Tab tab, int type, int lastId) {
        RecordHistogram.recordBooleanHistogram(
                "TabGroups.SelectedTabInTabGroup", isTabInTabGroup(tab));
        selectTab(tab);
        if (!shouldNotifyObserversOnSetIndex()) return;
        for (TabModelObserver observer : mFilteredObservers) {
            observer.didSelectTab(tab, type, lastId);
        }
    }

    @Override
    public void willCloseTab(Tab tab, boolean didCloseAlone) {
        closeTab(tab);
        for (TabModelObserver observer : mFilteredObservers) {
            observer.willCloseTab(tab, didCloseAlone);
        }
    }

    @Override
    public void onFinishingTabClosure(Tab tab) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.onFinishingTabClosure(tab);
        }
    }

    @Override
    public void onFinishingMultipleTabClosure(List<Tab> tabs, boolean canRestore) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.onFinishingMultipleTabClosure(tabs, canRestore);
        }
    }

    @Override
    public void willAddTab(Tab tab, int type) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.willAddTab(tab, type);
        }
    }

    @Override
    public void didAddTab(
            Tab tab,
            @TabLaunchType int type,
            @TabCreationState int creationState,
            boolean markedForSelection) {
        addTab(tab, /* fromUndo= */ false);
        for (TabModelObserver observer : mFilteredObservers) {
            observer.didAddTab(tab, type, creationState, markedForSelection);
        }
    }

    @Override
    public void didMoveTab(Tab tab, int newIndex, int curIndex) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.didMoveTab(tab, newIndex, curIndex);
        }
    }

    @Override
    public void tabPendingClosure(Tab tab) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.tabPendingClosure(tab);
        }
    }

    @Override
    public void multipleTabsPendingClosure(List<Tab> tabs, boolean isAllTabs) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.multipleTabsPendingClosure(tabs, isAllTabs);
        }
    }

    @Override
    public void tabClosureUndone(Tab tab) {
        addTab(tab, /* fromUndo= */ true);
        reorder();
        for (TabModelObserver observer : mFilteredObservers) {
            observer.tabClosureUndone(tab);
        }
    }

    @Override
    public void tabClosureCommitted(Tab tab) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.tabClosureCommitted(tab);
        }
    }

    @Override
    public void willCloseAllTabs(boolean incognito) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.willCloseAllTabs(incognito);
        }
    }

    @Override
    public void allTabsClosureUndone() {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.allTabsClosureUndone();
        }
    }

    @Override
    public void allTabsClosureCommitted(boolean isIncognito) {
        for (TabModelObserver observer : mFilteredObservers) {
            observer.allTabsClosureCommitted(isIncognito);
        }
    }

    @Override
    public void tabRemoved(Tab tab) {
        removeTab(tab);
        for (TabModelObserver observer : mFilteredObservers) {
            observer.tabRemoved(tab);
        }
    }

    @Override
    public void restoreCompleted() {
        mTabRestoreCompleted = true;

        if (getCount() != 0) reorder();

        for (TabModelObserver observer : mFilteredObservers) {
            observer.restoreCompleted();
        }
    }
}