chromium/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/TabbedModeTabModelOrchestrator.java

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

import android.app.Activity;
import android.util.Pair;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.chrome.browser.DeferredStartupHandler;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.multiwindow.MultiInstanceManager;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.MismatchedIndicesHandler;
import org.chromium.chrome.browser.tabmodel.NextTabPolicy.NextTabPolicySupplier;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorBase;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorImpl;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore;
import org.chromium.chrome.browser.tabmodel.TabbedModeTabPersistencePolicy;
import org.chromium.ui.widget.Toast;

/**
 * Glue-level class that manages lifetime of root .tabmodel objects: {@link TabPersistentStore} and
 * {@link TabModelSelectorImpl} for tabbed mode.
 */
public class TabbedModeTabModelOrchestrator extends TabModelOrchestrator {
    private final boolean mTabMergingEnabled;
    private final ActivityLifecycleDispatcher mActivityLifecycleDispatcher;

    // This class is driven by TabbedModeTabModelOrchestrator to prevent duplicate glue code in
    //  ChromeTabbedActivity.
    private ArchivedTabModelOrchestrator mArchivedTabModelOrchestrator;
    private OneshotSupplier<ProfileProvider> mProfileProviderSupplier;
    private TabCreatorManager mTabCreatorManager;

    /**
     * Constructor.
     *
     * @param tabMergingEnabled Whether we are on the platform where tab merging is enabled.
     * @param activityLifecycleDispatcher Used to determine if the current activity context is still
     *     valid when running deferred tasks.
     */
    public TabbedModeTabModelOrchestrator(
            boolean tabMergingEnabled, ActivityLifecycleDispatcher activityLifecycleDispatcher) {
        mTabMergingEnabled = tabMergingEnabled;
        mActivityLifecycleDispatcher = activityLifecycleDispatcher;
    }

    /**
     * Creates the TabModelSelector and the TabPersistentStore.
     *
     * @param activity The activity that hosts this TabModelOrchestrator.
     * @param profileProviderSupplier Supplies the {@link ProfileProvider} for the activity.
     * @param tabCreatorManager Manager for the {@link TabCreator} for the {@link TabModelSelector}.
     * @param nextTabPolicyProvider Policy for what to do when a tab is closed.
     * @param mismatchedIndicesHandler Handles when indices are mismatched.
     * @param selectorIndex Which index to use when requesting a selector.
     * @return Whether the creation was successful. It may fail is we reached the limit of number of
     *     windows.
     */
    public boolean createTabModels(
            Activity activity,
            OneshotSupplier<ProfileProvider> profileProviderSupplier,
            TabCreatorManager tabCreatorManager,
            NextTabPolicySupplier nextTabPolicySupplier,
            MismatchedIndicesHandler mismatchedIndicesHandler,
            int selectorIndex) {
        mProfileProviderSupplier = profileProviderSupplier;
        mTabCreatorManager = tabCreatorManager;
        boolean mergeTabsOnStartup = shouldMergeTabs(activity);
        if (mergeTabsOnStartup) {
            MultiInstanceManager.mergedOnStartup();
        }

        // Instantiate TabModelSelectorImpl
        Pair<Integer, TabModelSelector> selectorAssignment =
                TabWindowManagerSingleton.getInstance()
                        .requestSelector(
                                activity,
                                profileProviderSupplier,
                                tabCreatorManager,
                                nextTabPolicySupplier,
                                mismatchedIndicesHandler,
                                selectorIndex);
        if (selectorAssignment == null) {
            mTabModelSelector = null;
        } else {
            mTabModelSelector = (TabModelSelectorBase) selectorAssignment.second;
        }

        if (mTabModelSelector == null) {
            markTabModelsInitialized();
            Toast.makeText(
                            activity,
                            activity.getString(
                                    org.chromium.chrome.R.string.unsupported_number_of_windows),
                            Toast.LENGTH_LONG)
                    .show();
            return false;
        }

        int assignedIndex = selectorAssignment.first;

        // Instantiate TabPersistentStore
        mTabPersistencePolicy =
                new TabbedModeTabPersistencePolicy(
                        assignedIndex, mergeTabsOnStartup, mTabMergingEnabled);
        mTabPersistentStore =
                new TabPersistentStore(
                        TabPersistentStore.CLIENT_TAG_REGULAR,
                        mTabPersistencePolicy,
                        mTabModelSelector,
                        tabCreatorManager,
                        TabWindowManagerSingleton.getInstance());

        wireSelectorAndStore();
        markTabModelsInitialized();
        return true;
    }

    private boolean shouldMergeTabs(Activity activity) {
        if (isMultiInstanceApi31Enabled()) {
            // For multi-instance on Android S, this is a restart after the upgrade or fresh
            // installation. Allow merging tabs from CTA/CTA2 used by the previous version
            // if present.
            return MultiWindowUtils.getInstanceCount() == 0;
        }

        // Merge tabs if this TabModelSelector is for a ChromeTabbedActivity created in
        // fullscreen mode and there are no TabModelSelector's currently alive. This indicates
        // that it is a cold start or process restart in fullscreen mode.
        boolean mergeTabs = mTabMergingEnabled && !activity.isInMultiWindowMode();
        if (MultiInstanceManager.shouldMergeOnStartup(activity)) {
            mergeTabs =
                    mergeTabs
                            && (!MultiWindowUtils.getInstance().isInMultiDisplayMode(activity)
                                    || TabWindowManagerSingleton.getInstance()
                                                    .getNumberOfAssignedTabModelSelectors()
                                            == 0);
        } else {
            mergeTabs =
                    mergeTabs
                            && TabWindowManagerSingleton.getInstance()
                                            .getNumberOfAssignedTabModelSelectors()
                                    == 0;
        }
        return mergeTabs;
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    protected boolean isMultiInstanceApi31Enabled() {
        return MultiWindowUtils.isMultiInstanceApi31Enabled();
    }

    @Override
    public void cleanupInstance(int instanceId) {
        mTabPersistentStore.cleanupStateFile(instanceId);
    }

    @Override
    public void onNativeLibraryReady(TabContentManager tabContentManager) {
        super.onNativeLibraryReady(tabContentManager);

        if (ChromeFeatureList.sAndroidTabDeclutterRescueKillSwitch.isEnabled()) {
            DeferredStartupHandler.getInstance()
                    .addDeferredTask(
                            () -> createAndInitArchivedTabModelOrchestrator(tabContentManager));
            DeferredStartupHandler.getInstance().queueDeferredTasksOnIdleHandler();
        }
    }

    @Override
    public void saveState() {
        super.saveState();
        if (mArchivedTabModelOrchestrator != null
                && mArchivedTabModelOrchestrator.areTabModelsInitialized()) {
            mArchivedTabModelOrchestrator.saveState();
        }
    }

    private void createAndInitArchivedTabModelOrchestrator(TabContentManager tabContentManager) {
        if (mActivityLifecycleDispatcher.isActivityFinishingOrDestroyed()) return;
        ThreadUtils.assertOnUiThread();
        // The profile will be available because native is initialized.
        assert mProfileProviderSupplier.hasValue();
        assert tabContentManager != null;

        Profile profile = mProfileProviderSupplier.get().getOriginalProfile();
        assert profile != null;

        TabCreator regularTabCreator = mTabCreatorManager.getTabCreator(/* incognito= */ false);
        mArchivedTabModelOrchestrator = ArchivedTabModelOrchestrator.getForProfile(profile);
        mArchivedTabModelOrchestrator.maybeCreateAndInitTabModels(
                tabContentManager, regularTabCreator);
        mArchivedTabModelOrchestrator.initializeHistoricalTabModelObserver(
                () -> getTabModelSelector().getModel(/* incognito= */ false));

        // If the feature flag is enabled, then start the declutter process. Otherwise, rescue
        // tabs that may have been archived previously.
        if (ChromeFeatureList.sAndroidTabDeclutter.isEnabled()) {
            mArchivedTabModelOrchestrator.maybeBeginDeclutter();
        } else {
            mArchivedTabModelOrchestrator.maybeRescueArchivedTabs();
        }
    }

    public TabPersistentStore getTabPersistentStoreForTesting() {
        return mTabPersistentStore;
    }
}