chromium/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/TabModelOrchestrator.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 androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutHelperManager.TabModelStartupInfo;
import org.chromium.chrome.browser.crash.ChromePureJavaExceptionReporter;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.MismatchedIndicesHandler;
import org.chromium.chrome.browser.tabmodel.TabModel;
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.TabPersistencePolicy;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabPersistentStoreObserver;

import java.util.Locale;

/**
 * Implementers are glue-level objects that manage lifetime of root .tabmodel objects: {@link
 * TabPersistentStore} and {@link TabModelSelectorImpl}.
 */
public abstract class TabModelOrchestrator {
    protected TabPersistentStore mTabPersistentStore;
    @Nullable protected TabModelSelectorBase mTabModelSelector;
    protected TabPersistencePolicy mTabPersistencePolicy;
    private boolean mTabModelsInitialized;
    private Callback<String> mOnStandardActiveIndexRead;
    private boolean mTabPersistentStoreDestroyedEarly;

    // TabModelStartupInfo variables
    private ObservableSupplierImpl<TabModelStartupInfo> mTabModelStartupInfoSupplier;
    private boolean mIgnoreIncognitoFiles;
    private int mStandardCount;
    private int mIncognitoCount;
    private int mStandardActiveIndex = TabModel.INVALID_TAB_INDEX;
    private int mIncognitoActiveIndex = TabModel.INVALID_TAB_INDEX;

    /**
     * @return Whether the tab models have been fully initialized.
     */
    public boolean areTabModelsInitialized() {
        return mTabModelsInitialized;
    }

    /**
     * @return The {@link TabModelSelector} managed by this orchestrator.
     */
    public TabModelSelectorBase getTabModelSelector() {
        return mTabModelSelector;
    }

    /**
     * Sets {@link TabPersistentStore} for testing.
     * @param tabPersistentStore The {@link TabPersistentStore}.
     */
    void setTabPersistentStoreForTesting(TabPersistentStore tabPersistentStore) {
        mTabPersistentStore = tabPersistentStore;
    }

    /**
     * @return The {@link TabPersistentStore} managed by this orchestrator.
     */
    public TabPersistentStore getTabPersistentStore() {
        return mTabPersistentStore;
    }

    /** Destroy the {@link TabPersistentStore} and {@link TabModelSelectorImpl} members. */
    public void destroy() {
        if (!mTabModelsInitialized) {
            return;
        }

        // TODO(crbug.com/40743848): Set the members to null and mTabModelsInitialized to false.
        // Right now, it breaks destruction of VrShell, which relies on using TabModel after
        // its destruction.

        if (mTabPersistentStore != null) {
            mTabPersistentStore.destroy();
        }

        if (mTabModelSelector != null) {
            mTabModelSelector.destroy();
        }
    }

    /**
     * Destroy the {@link TabPersistentStore} earlier than activity destruction. See the
     * implementation of {@link MismatchedIndicesHandler#handleMismatchedIndices(Activity)} for more
     * details.
     */
    public void destroyTabPersistentStore() {
        if (mTabPersistentStore != null) {
            mTabPersistentStore.destroy();
            mTabPersistentStoreDestroyedEarly = true;
        }
    }

    public void onNativeLibraryReady(TabContentManager tabContentManager) {
        mTabModelSelector.onNativeLibraryReady(tabContentManager);
        mTabPersistencePolicy.setTabContentManager(tabContentManager);
        if (!mTabPersistentStoreDestroyedEarly) mTabPersistentStore.onNativeLibraryReady();
    }

    /**
     * Save the current state of the tab model. Usage of this method is discouraged due to it
     * writing to disk.
     */
    public void saveState() {
        mTabModelSelector.commitAllTabClosures();
        if (!mTabPersistentStoreDestroyedEarly) mTabPersistentStore.saveState();
    }

    /**
     * Load the saved tab state. This should be called before any new tabs are created. The saved
     * tabs shall not be restored until {@link #restoreTabs} is called.
     * @param ignoreIncognitoFiles Whether to skip loading incognito tabs.
     * @param onStandardActiveIndexRead The callback to be called when the active non-incognito Tab
     *                                  is found.
     */
    public void loadState(
            boolean ignoreIncognitoFiles, Callback<String> onStandardActiveIndexRead) {
        mIgnoreIncognitoFiles = ignoreIncognitoFiles;
        mOnStandardActiveIndexRead = onStandardActiveIndexRead;
        if (!mTabPersistentStoreDestroyedEarly) mTabPersistentStore.loadState(ignoreIncognitoFiles);
    }

    /**
     * Restore the saved tabs which were loaded by {@link #loadState}.
     *
     * @param setActiveTab If true, synchronously load saved active tab and set it as the current
     *                     active tab.
     */
    public void restoreTabs(boolean setActiveTab) {
        if (mTabModelStartupInfoSupplier != null) {
            boolean createdStandardTabOnStartup =
                    getTabModelSelector().getModel(false).getCount() > 0;
            boolean createdIncognitoTabOnStartup =
                    getTabModelSelector().getModel(true).getCount() > 0;

            // Incognito tabs are read first, so we have to adjust to find the real active index in
            // the standard model.
            int standardActiveIndex =
                    mStandardActiveIndex != TabModel.INVALID_TAB_INDEX
                            ? mStandardActiveIndex - mIncognitoCount
                            : TabModel.INVALID_TAB_INDEX;

            // Log additional info to help determine why we're seeing OOB-exceptions on startup.
            // TODO(crbug.com/352550150): Clean up once investigation is complete.
            boolean isStandardIndexOutOfBounds =
                    standardActiveIndex < TabModel.INVALID_TAB_INDEX
                            || standardActiveIndex >= mStandardCount;
            boolean isIncognitoIndexOutOfBounds =
                    mIncognitoActiveIndex < TabModel.INVALID_TAB_INDEX
                            || mIncognitoActiveIndex >= mIncognitoCount;
            if (isStandardIndexOutOfBounds || isIncognitoIndexOutOfBounds) {
                String message =
                        String.format(
                                Locale.ENGLISH,
                                "This is not a crash. See https://crbug.com/352550150.\n"
                                        + "Standard index: %d, Original: %d, Incognito index: %d\n"
                                        + "Standard count: %d, Incognito count: %d\n"
                                        + "Created standard tab: %b, Created Incognito tab: %b\n"
                                        + "Ignoring Incognito tabs: %b",
                                standardActiveIndex,
                                mStandardActiveIndex,
                                mIncognitoActiveIndex,
                                mStandardCount,
                                mIncognitoCount,
                                createdStandardTabOnStartup,
                                createdIncognitoTabOnStartup,
                                mIgnoreIncognitoFiles);
                ChromePureJavaExceptionReporter.reportJavaException(new Throwable(message));
            }

            // If we're going to cull the Incognito tabs, reset the startup state.
            if (mIgnoreIncognitoFiles) {
                mIncognitoCount = 0;
                mIncognitoActiveIndex = TabModel.INVALID_TAB_INDEX;
            }

            // Account for tabs created on startup (e.g. through intents).
            if (createdStandardTabOnStartup) mStandardCount++;
            if (createdIncognitoTabOnStartup) mIncognitoCount++;

            mTabModelStartupInfoSupplier.set(
                    new TabModelStartupInfo(
                            mStandardCount,
                            mIncognitoCount,
                            standardActiveIndex,
                            mIncognitoActiveIndex,
                            createdStandardTabOnStartup,
                            createdIncognitoTabOnStartup));
        }
        if (!mTabPersistentStoreDestroyedEarly) mTabPersistentStore.restoreTabs(setActiveTab);
    }

    public void mergeState() {
        if (!mTabPersistentStoreDestroyedEarly) mTabPersistentStore.mergeState();
    }

    public void clearState() {
        if (!mTabPersistentStoreDestroyedEarly) mTabPersistentStore.clearState();
    }

    /**
     * Clean up persistent state for a given instance.
     * @param instanceId Instance ID.
     */
    public void cleanupInstance(int instanceId) {}

    /**
     * If there is an asynchronous session restore in-progress, try to synchronously restore
     * the state of a tab with the given url as a frozen tab. This method has no effect if
     * there isn't a tab being restored with this url, or the tab has already been restored.
     */
    public void tryToRestoreTabStateForUrl(String url) {
        if (mTabModelSelector.isSessionRestoreInProgress() && !mTabPersistentStoreDestroyedEarly) {
            mTabPersistentStore.restoreTabStateForUrl(url);
        }
    }

    /**
     * If there is an asynchronous session restore in-progress, try to synchronously restore
     * the state of a tab with the given id as a frozen tab. This method has no effect if
     * there isn't a tab being restored with this id, or the tab has already been restored.
     */
    public void tryToRestoreTabStateForId(int id) {
        if (mTabModelSelector.isSessionRestoreInProgress() && !mTabPersistentStoreDestroyedEarly) {
            mTabPersistentStore.restoreTabStateForId(id);
        }
    }

    /**
     * @return Number of restored tabs on cold startup.
     */
    public int getRestoredTabCount() {
        if (mTabPersistentStore == null || mTabPersistentStoreDestroyedEarly) return 0;
        return mTabPersistentStore.getRestoredTabCount();
    }

    /**
     * Sets the supplier for {@link TabModelStartupInfo} on startup.
     *
     * @param observableSupplier The {@link TabModelStartupInfo} supplier.
     */
    public void setStartupInfoObservableSupplier(
            ObservableSupplierImpl<TabModelStartupInfo> observableSupplier) {
        mTabModelStartupInfoSupplier = observableSupplier;
    }

    public boolean getTabPersistentStoreDestroyedEarlyForTesting() {
        return mTabPersistentStoreDestroyedEarly;
    }

    protected void wireSelectorAndStore() {
        if (mTabPersistentStoreDestroyedEarly) return;
        // Notify TabModelSelector when TabPersistentStore initializes tab state
        final TabPersistentStoreObserver persistentStoreObserver =
                new TabPersistentStoreObserver() {
                    @Override
                    public void onStateLoaded() {
                        mTabModelSelector.markTabStateInitialized();
                    }

                    @Override
                    public void onDetailsRead(
                            int index,
                            int id,
                            String url,
                            boolean isStandardActiveIndex,
                            boolean isIncognitoActiveIndex,
                            Boolean isIncognito,
                            boolean fromMerge) {
                        if (isIncognito == null || !isIncognito.booleanValue()) {
                            mStandardCount++;
                        } else {
                            mIncognitoCount++;
                        }

                        // We prioritize focusing the active tab from the "primary" (non-merging)
                        // instance.
                        if (!fromMerge) {
                            if (isStandardActiveIndex) {
                                mStandardActiveIndex = index;
                            } else if (isIncognitoActiveIndex) {
                                mIncognitoActiveIndex = index;
                            }
                        }

                        if (mOnStandardActiveIndexRead != null && isStandardActiveIndex) {
                            mOnStandardActiveIndexRead.onResult(url);
                        }
                    }

                    @Override
                    public void onInitialized(int tabCountAtStartup) {
                        // Resets the callback once the read of the Tab state file is completed.
                        mOnStandardActiveIndexRead = null;
                    }
                };
        mTabPersistentStore.addObserver(persistentStoreObserver);
    }

    protected void markTabModelsInitialized() {
        mTabModelsInitialized = true;
    }
}