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

// Copyright 2014 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 android.content.SharedPreferences;
import android.os.StrictMode;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Pair;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.AtomicFile;

import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.StreamUtil;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.BackgroundOnlyAsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabIdManager;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabState;
import org.chromium.chrome.browser.tab.TabStateAttributes;
import org.chromium.chrome.browser.tab.TabStateExtractor;
import org.chromium.chrome.browser.tab.state.PersistedTabData;
import org.chromium.chrome.browser.tabmodel.TabPersistenceFileInfo.TabStateFileInfo;
import org.chromium.chrome.browser.tabpersistence.TabStateDirectory;
import org.chromium.chrome.browser.tabpersistence.TabStateFileManager;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.content_public.browser.LoadUrlParams;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;

/** This class handles saving and loading tab state from the persistent storage. */
public class TabPersistentStore {
    public static final String CLIENT_TAG_REGULAR = "Regular";
    public static final String CLIENT_TAG_CUSTOM = "Custom";
    public static final String CLIENT_TAG_ARCHIVED = "Archived";

    private static final String TAG = "tabmodel";
    private static final String TAG_MIGRATION = "fb_migration";

    private static final long INVALID_TIME = -1;

    /**
     * The current version of the saved state file. Version 4: In addition to the tab's ID, save the
     * tab's last URL. Version 5: In addition to the total tab count, save the incognito tab count.
     */
    private static final int SAVED_STATE_VERSION = 5;

    /**
     * The prefix of the name of the file where the metadata is saved. Values returned by {@link
     * #getMetadataFileName(String)} must begin with this prefix.
     */
    @VisibleForTesting static final String SAVED_METADATA_FILE_PREFIX = "tab_state";

    /** Prevents two TabPersistentStores from saving the same file simultaneously. */
    private static final Object SAVE_LIST_LOCK = new Object();

    private static boolean sDeferredStartupComplete;

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    protected static int sMaxMigrationsPerSave = 5;

    private TabModelObserver mTabModelObserver;
    private TabModelSelectorTabRegistrationObserver mTabRegistrationObserver;

    private int mDuplicateTabIdsSeen;

    @IntDef({ActiveTabState.OTHER, ActiveTabState.NTP, ActiveTabState.EMPTY})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ActiveTabState {
        /** No active tab. */
        int EMPTY = 0;

        /** Active tab is NTP. */
        int NTP = 1;

        /** Active tab is anything other than NTP. */
        int OTHER = 2;
    }

    /** Values are recorded in metrics and should not be changed. */
    @IntDef({
        TabRestoreMethod.TAB_STATE,
        TabRestoreMethod.CRITICAL_PERSISTED_TAB_DATA,
        TabRestoreMethod.CREATE_NEW_TAB,
        TabRestoreMethod.FAILED_TO_RESTORE,
        TabRestoreMethod.SKIPPED_NTP,
        TabRestoreMethod.SKIPPED_EMPTY_URL,
        TabRestoreMethod.NUM_ENTRIES
    })
    @Retention(RetentionPolicy.SOURCE)
    @VisibleForTesting
    protected @interface TabRestoreMethod {
        /** Tab restored using TabState. */
        int TAB_STATE = 0;

        /** Tab restored using CriticalPersistedTabData. */
        int CRITICAL_PERSISTED_TAB_DATA = 1;

        /** Tab restored by creating a new Tab from Tab metadata file. */
        int CREATE_NEW_TAB = 2;

        /** Failed to restore Tab using any of the above methods. */
        int FAILED_TO_RESTORE = 3;

        /** In some situations the NTP is skipped when we re-create the Tab as a fallback. */
        int SKIPPED_NTP = 4;

        /** The URL was empty so restoration was skipped. */
        int SKIPPED_EMPTY_URL = 5;

        int NUM_ENTRIES = 6;
    }

    public void onNativeLibraryReady() {
        TabStateAttributes.Observer attributesObserver =
                new TabStateAttributes.Observer() {
                    @Override
                    public void onTabStateDirtinessChanged(
                            Tab tab, @TabStateAttributes.DirtinessState int dirtiness) {
                        if (dirtiness == TabStateAttributes.DirtinessState.DIRTY
                                && !tab.isDestroyed()) {
                            addTabToSaveQueue(tab);
                        }
                    }
                };
        mTabRegistrationObserver = new TabModelSelectorTabRegistrationObserver(mTabModelSelector);
        mTabRegistrationObserver.addObserverAndNotifyExistingTabRegistration(
                new TabModelSelectorTabRegistrationObserver.Observer() {
                    @Override
                    public void onTabRegistered(Tab tab) {
                        TabStateAttributes attributes = TabStateAttributes.from(tab);
                        if (attributes.addObserver(attributesObserver)
                                == TabStateAttributes.DirtinessState.DIRTY) {
                            addTabToSaveQueue(tab);
                        }
                    }

                    @Override
                    public void onTabUnregistered(Tab tab) {
                        if (!tab.isDestroyed()) {
                            TabStateAttributes.from(tab).removeObserver(attributesObserver);
                        }
                        if (tab.isClosing()) {
                            PersistedTabData.onTabClose(tab);
                            removeTabFromQueues(tab);
                        }
                    }
                });

        mTabModelObserver =
                new TabModelObserver() {
                    @Override
                    public void willCloseAllTabs(boolean incognito) {
                        cancelLoadingTabs(incognito);
                    }

                    @Override
                    public void tabClosureUndone(Tab tab) {
                        saveTabListAsynchronously();
                    }

                    @Override
                    public void onFinishingMultipleTabClosure(List<Tab> tabs, boolean canRestore) {
                        if (!mTabModelSelector.isIncognitoSelected()) {
                            saveTabListAsynchronously();
                        }
                    }
                };
        mTabModelSelector.getModel(false).addObserver(mTabModelObserver);
        mTabModelSelector.getModel(true).addObserver(mTabModelObserver);
    }

    /** Callback interface to use while reading the persisted TabModelSelector info from disk. */
    public static interface OnTabStateReadCallback {
        /**
         * To be called as the details about a persisted Tab are read from the TabModelSelector's
         * persisted data.
         * @param index                  The index out of all tabs for the current tab read.
         * @param id                     The id for the current tab read.
         * @param url                    The url for the current tab read.
         * @param isIncognito            Whether the Tab is definitely Incognito, or null if it
         *                               couldn't be determined because we didn't know how many
         *                               Incognito tabs were saved out.
         * @param isStandardActiveIndex  Whether the current tab read is the normal active tab.
         * @param isIncognitoActiveIndex Whether the current tab read is the incognito active tab.
         */
        void onDetailsRead(
                int index,
                int id,
                String url,
                Boolean isIncognito,
                boolean isStandardActiveIndex,
                boolean isIncognitoActiveIndex);
    }

    /** Alerted at various stages of operation. */
    public abstract static class TabPersistentStoreObserver {
        /**
         * To be called when the file containing the initial information about the TabModels has
         * been loaded.
         * @param tabCountAtStartup How many tabs there are in the TabModels.
         */
        public void onInitialized(int tabCountAtStartup) {}

        /** Called when details about a Tab are read from the metadata file. */
        public void onDetailsRead(
                int index,
                int id,
                String url,
                boolean isStandardActiveIndex,
                boolean isIncognitoActiveIndex,
                Boolean isIncognito,
                boolean fromMerge) {}

        /** To be called when the TabStates have all been loaded. */
        public void onStateLoaded() {}

        /** To be called when the TabState from another instance has been merged. */
        public void onStateMerged() {}

        /**
         * Called when the metadata file has been saved out asynchronously.
         * This currently does not get called when the metadata file is saved out on the UI thread.
         * @param modelSelectorMetadata The saved metadata of current tab model selector.
         */
        public void onMetadataSavedAsynchronously(TabModelSelectorMetadata modelSelectorMetadata) {}
    }

    /** Stores information about a TabModel. */
    public static class TabModelMetadata {
        public int index;
        public final List<Integer> ids;
        public final List<String> urls;

        @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
        public TabModelMetadata(int selectedIndex) {
            index = selectedIndex;
            ids = new ArrayList<>();
            urls = new ArrayList<>();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof TabModelMetadata that)) return false;
            return index == that.index
                    && Objects.equals(ids, that.ids)
                    && Objects.equals(urls, that.urls);
        }

        @Override
        public int hashCode() {
            return Objects.hash(index, ids, urls);
        }
    }

    private final Set<Integer> mSeenTabIds = new HashSet<>();
    private final String mClientTag;
    private final TabPersistencePolicy mPersistencePolicy;
    private final TabModelSelector mTabModelSelector;
    private final TabCreatorManager mTabCreatorManager;
    private final TabWindowManager mTabWindowManager;
    private final ObserverList<TabPersistentStoreObserver> mObservers;

    private final Deque<Tab> mTabsToSave;
    private final ArrayDeque<Tab> mTabsToMigrate;
    private final Deque<TabRestoreDetails> mTabsToRestore;
    private final Set<Integer> mTabIdsToRestore;

    private TabLoader mTabLoader;
    private SaveTabTask mSaveTabTask;
    private MigrateTabTask mMigrateTabTask;

    private SaveListTask mSaveListTask;

    private boolean mDestroyed;
    private boolean mCancelNormalTabLoads;
    private boolean mCancelIncognitoTabLoads;

    // Keys are the original tab indexes, values are the tab ids.
    private SparseIntArray mNormalTabsRestored;
    private SparseIntArray mIncognitoTabsRestored;

    private SequencedTaskRunner mSequencedTaskRunner;
    private AsyncTask<DataInputStream> mPrefetchTabListTask;
    private List<Pair<AsyncTask<DataInputStream>, String>> mPrefetchTabListToMergeTasks;
    // A set of filenames which are tracked to merge.
    private Set<String> mMergedFileNames;
    private TabModelSelectorMetadata mLastSavedMetadata;

    // Tracks whether this TabPersistentStore's tabs are being loaded.
    private boolean mLoadInProgress;

    private long mTabRestoreStartTime = INVALID_TIME;

    AsyncTask<TabState> mPrefetchTabStateActiveTabTask;

    /**
     * Creates an instance of a TabPersistentStore.
     *
     * @param clientTag The client tag used to record metrics.
     * @param modelSelector The {@link TabModelSelector} to restore to and save from.
     * @param tabCreatorManager The {@link TabCreatorManager} to use.
     */
    public TabPersistentStore(
            String clientTag,
            TabPersistencePolicy policy,
            TabModelSelector modelSelector,
            TabCreatorManager tabCreatorManager,
            TabWindowManager tabWindowManager) {
        mClientTag = clientTag;
        mPersistencePolicy = policy;
        mTabModelSelector = modelSelector;
        mTabCreatorManager = tabCreatorManager;
        mTabWindowManager = tabWindowManager;
        mTabsToSave = new ArrayDeque<>();
        mTabsToMigrate = new ArrayDeque<>();
        mTabsToRestore = new ArrayDeque<>();
        mTabIdsToRestore = new HashSet<>();
        mObservers = new ObserverList<>();
        @TaskTraits int taskTraits = TaskTraits.USER_BLOCKING_MAY_BLOCK;
        mSequencedTaskRunner = PostTask.createSequencedTaskRunner(taskTraits);
        mPrefetchTabListToMergeTasks = new ArrayList<>();
        mMergedFileNames = new HashSet<>();

        assert isMetadataFile(policy.getMetadataFileName()) : "Metadata file name is not valid";
        boolean needsInitialization =
                mPersistencePolicy.performInitialization(mSequencedTaskRunner);

        mPersistencePolicy.setTaskRunner(mSequencedTaskRunner);

        if (mPersistencePolicy.isMergeInProgress()) return;

        // TODO(smaier): We likely can move everything onto the SequencedTaskRunner when the
        //  SERIAL_EXECUTOR path is gone. crbug.com/957735
        TaskRunner taskRunner =
                needsInitialization ? mSequencedTaskRunner : PostTask.createTaskRunner(taskTraits);

        mPrefetchTabListTask =
                startFetchTabListTask(taskRunner, mPersistencePolicy.getMetadataFileName());
        startPrefetchActiveTabTask(taskRunner);

        if (mPersistencePolicy.shouldMergeOnStartup()) {
            String mergedFileName = mPersistencePolicy.getMetadataFileNameToBeMerged();
            assert mergedFileName != null;
            AsyncTask<DataInputStream> task = startFetchTabListTask(taskRunner, mergedFileName);
            mPrefetchTabListToMergeTasks.add(Pair.create(task, mergedFileName));
        }
    }

    /** Waits for the task that migrates all state files to their new location to finish. */
    @VisibleForTesting
    public void waitForMigrationToFinish() {
        mPersistencePolicy.waitForInitializationToFinish();
    }

    public void saveState() {
        // Temporarily allowing disk access. TODO: Fix. See http://b/5518024
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            // Clear out any in-flight migration.
            if (mMigrateTabTask != null) {
                if (mMigrateTabTask.cancel(false) && !mMigrateTabTask.mMigrationComplete) {
                    // The task was successfully cancelled. Re-add Tab to migration queue.
                    Tab cancelledTab = mMigrateTabTask.mTab;
                    mTabsToMigrate.addFirst(cancelledTab);
                }
                mMigrateTabTask = null;
            }
            // Don't want any new save below to trigger new migrations which are unnecessary. Only
            // want to update any migrations for which Tabs have already migrated (so the
            // migrated TabState file is not out of date, which would lead to an old snapshot
            // of the Tab being restored upon restart). If the Tab hasn't migrated yet,
            // the legacy TabState file will be used upon a restart.
            ArrayDeque<Tab> tabsToMigrateCopy = mTabsToMigrate.clone();
            mTabsToMigrate.clear();

            // The list of tabs should be saved first in case our activity is terminated early.
            // Explicitly toss out any existing SaveListTask because they only save the TabModel as
            // it looked when the SaveListTask was first created.
            if (mSaveListTask != null) mSaveListTask.cancel(true);
            try {
                saveListToFile(saveTabMetadata());
            } catch (IOException e) {
                Log.w(TAG, "Error while saving tabs state; will attempt to continue...", e);
            }

            // Add current tabs to save because they did not get a save signal yet.
            Tab currentStandardTab = TabModelUtils.getCurrentTab(mTabModelSelector.getModel(false));
            addTabToSaveQueueIfApplicable(currentStandardTab);

            Tab currentIncognitoTab = TabModelUtils.getCurrentTab(mTabModelSelector.getModel(true));
            addTabToSaveQueueIfApplicable(currentIncognitoTab);

            // Wait for the current tab to save.
            if (mSaveTabTask != null) {
                // Cancel calls get() to wait for this to finish internally if it has to.
                // The issue is it may assume it cancelled the task, but the task still actually
                // wrote the state to disk.  That's why we have to check mStateSaved here.
                if (mSaveTabTask.cancel(false) && !mSaveTabTask.mStateSaved) {
                    // The task was successfully cancelled.  We should try to save this state again.
                    Tab cancelledTab = mSaveTabTask.mTab;
                    addTabToSaveQueueIfApplicable(cancelledTab);
                }

                mSaveTabTask = null;
            }

            // Synchronously save any remaining unsaved tabs (hopefully very few).
            for (Tab tab : mTabsToSave) {
                int id = tab.getId();
                boolean incognito = tab.isIncognito();
                try {
                    TabState state = TabStateExtractor.from(tab);
                    if (state != null) {
                        TabStateFileManager.saveState(getStateDirectory(), state, id, incognito);
                        if (isFlatBufferSchemaEnabled()
                                && TabStateFileManager.isMigrated(
                                        getStateDirectory(), id, incognito)) {
                            // Ensure parity between the FlatBuffer TabState file and legacy.
                            // Otherwise if the user restarts and is in the experiment, they may
                            // have the Tab restored using an out of date FlatBuffer file.
                            TabStateFileManager.migrateTabState(
                                    getStateDirectory(), state, id, incognito);
                            // No longer need to migrate the Tab as it was just migrated.
                            tabsToMigrateCopy.remove(tab);
                        }
                    }
                } catch (OutOfMemoryError e) {
                    Log.e(TAG, "Out of memory error while attempting to save tab state.  Erasing.");
                    deleteTabState(id, incognito);
                    if (isFlatBufferSchemaEnabled()) {
                        TabStateFileManager.deleteMigratedFile(getStateDirectory(), id, incognito);
                    }
                }
            }
            // Now all pending saves (and migrations, if applicable) are complete we are ok to
            // resume any migrations which would be triggered by another Tab save.
            for (Tab tab : tabsToMigrateCopy) {
                mTabsToMigrate.add(tab);
            }
            updateMigratedFiles();
            mTabsToSave.clear();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }
    }

    /**
     * Migrate any Tabs to the TabState FlatBuffer file which have a FlatBuffer file already
     * written. Otherwise if the user restarts in the experiment they may have their Tab restored
     * using an out of date FlatBuffer file.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    protected void updateMigratedFiles() {
        List<Tab> updatedMigrations = new LinkedList<>();
        for (Tab tab : mTabsToMigrate) {
            int id = tab.getId();
            boolean incognito = tab.isIncognito();
            if (TabStateFileManager.isMigrated(getStateDirectory(), id, incognito)) {
                try {
                    TabState state = TabStateExtractor.from(tab);
                    if (state != null) {
                        TabStateFileManager.migrateTabState(
                                getStateDirectory(), state, id, incognito);
                        updatedMigrations.add(tab);
                    }
                } catch (OutOfMemoryError e) {
                    Log.e(
                            TAG,
                            "Out of memory error while attempting to update Migrated TabState file."
                                    + "  Erasing.");
                    TabStateFileManager.deleteMigratedFile(getStateDirectory(), id, incognito);
                }
            }
        }
        // No longer need to migrate Tabs which were just migrated.
        for (Tab migratedTab : updatedMigrations) {
            mTabsToMigrate.remove(migratedTab);
        }
    }

    @VisibleForTesting
    void initializeRestoreVars(boolean ignoreIncognitoFiles) {
        mCancelNormalTabLoads = false;
        mCancelIncognitoTabLoads = ignoreIncognitoFiles;
        mNormalTabsRestored = new SparseIntArray();
        mIncognitoTabsRestored = new SparseIntArray();
    }

    /**
     * Restore saved state. Must be called before any tabs are added to the list.
     *
     * This will read the metadata file for the current TabPersistentStore and the metadata file
     * from another TabPersistentStore if applicable. When restoreTabs() is called, tabs from both
     * will be restored into this instance.
     *
     * @param ignoreIncognitoFiles Whether to skip loading incognito tabs.
     */
    public void loadState(boolean ignoreIncognitoFiles) {
        // If a cleanup task is in progress, cancel it before loading state.
        mPersistencePolicy.cancelCleanupInProgress();

        waitForMigrationToFinish();

        initializeRestoreVars(ignoreIncognitoFiles);

        try {
            mTabRestoreStartTime = SystemClock.elapsedRealtime();
            assert mTabModelSelector.getModel(true).getCount() == 0;
            assert mTabModelSelector.getModel(false).getCount() == 0;
            checkAndUpdateMaxTabId();
            DataInputStream stream;
            if (mPrefetchTabListTask != null) {
                mTabRestoreStartTime = SystemClock.elapsedRealtime();
                stream = mPrefetchTabListTask.get();

                // Restore the tabs for this TabPersistentStore instance if the tab metadata file
                // exists.
                if (stream != null) {
                    mLoadInProgress = true;
                    readSavedMetadataFile(
                            stream,
                            createOnTabStateReadCallback(
                                    mTabModelSelector.isIncognitoSelected(), false),
                            null);
                } else {
                    mTabRestoreStartTime = INVALID_TIME;
                }
            }

            // Restore the tabs for the other TabPersistentStore instance if its tab metadata file
            // exists.
            if (mPrefetchTabListToMergeTasks.size() > 0) {
                for (Pair<AsyncTask<DataInputStream>, String> mergeTask :
                        mPrefetchTabListToMergeTasks) {
                    AsyncTask<DataInputStream> task = mergeTask.first;
                    stream = task.get();
                    if (stream == null) continue;
                    mMergedFileNames.add(mergeTask.second);
                    mPersistencePolicy.setMergeInProgress(true);
                    readSavedMetadataFile(
                            stream,
                            createOnTabStateReadCallback(
                                    mTabModelSelector.isIncognitoSelected(),
                                    mTabsToRestore.size() != 0),
                            null);
                }
                if (!mMergedFileNames.isEmpty()) {
                    RecordUserAction.record("Android.MergeState.ColdStart");
                }
                mPrefetchTabListToMergeTasks.clear();
            }
        } catch (Exception e) {
            // Catch generic exception to prevent a corrupted state from crashing app on startup.
            Log.i(TAG, "loadState exception: " + e.toString(), e);
            mTabRestoreStartTime = INVALID_TIME;
        }

        mPersistencePolicy.notifyStateLoaded(mTabsToRestore.size());
        for (TabPersistentStoreObserver observer : mObservers) {
            observer.onInitialized(mTabsToRestore.size());
        }
    }

    /**
     * Merge the tabs of the other Chrome instance into this instance by reading its tab metadata
     * file and tab state files.
     *
     * This method should be called after a change in activity state indicates that a merge is
     * necessary. #loadState() will take care of merging states on application cold start if needed.
     *
     * If there is currently a merge or load in progress then this method will return early.
     */
    public void mergeState() {
        if (mLoadInProgress
                || mPersistencePolicy.isMergeInProgress()
                || !mTabsToRestore.isEmpty()) {
            Log.d(TAG, "Tab load still in progress when merge was attempted.");
            return;
        }

        // Initialize variables.
        initializeRestoreVars(false);

        try {
            // Read the tab state metadata file.
            String mergeFileName = mPersistencePolicy.getMetadataFileNameToBeMerged();
            assert mergeFileName != null
                    : "mergeState called when no metadata file to be merged exists.";
            DataInputStream stream =
                    startFetchTabListTask(mSequencedTaskRunner, mergeFileName).get();
            if (stream != null) {
                mMergedFileNames.add(mergeFileName);
                mPersistencePolicy.setMergeInProgress(true);
                readSavedMetadataFile(
                        stream,
                        createOnTabStateReadCallback(mTabModelSelector.isIncognitoSelected(), true),
                        null);
            }
        } catch (Exception e) {
            // Catch generic exception to prevent a corrupted state from crashing app.
            Log.d(TAG, "mergeState exception: " + e.toString(), e);
        }

        // Restore the tabs from the second activity asynchronously.
        loadNextTab();
    }

    /**
     * Restore tab state.  Tab state is loaded asynchronously, other than the active tab which
     * can be forced to load synchronously.
     *
     * @param setActiveTab If true the last active tab given in the saved state is loaded
     *                     synchronously and set as the current active tab. If false all tabs are
     *                     loaded asynchronously.
     */
    public void restoreTabs(boolean setActiveTab) {
        if (setActiveTab) {
            // Restore and select the active tab, which is first in the restore list.
            // If the active tab can't be restored, restore and select another tab. Otherwise, the
            // tab model won't have a valid index and the UI will break. http://crbug.com/261378
            while (!mTabsToRestore.isEmpty()
                    && mNormalTabsRestored.size() == 0
                    && mIncognitoTabsRestored.size() == 0) {
                try (TraceEvent e = TraceEvent.scoped("LoadFirstTabState")) {
                    TabRestoreDetails tabToRestore = mTabsToRestore.removeFirst();
                    restoreTab(tabToRestore, true);
                }
            }
        }
        loadNextTab();
    }

    /**
     * If a tab is being restored with the given url, then restore the tab in a frozen state
     * synchronously.
     */
    public void restoreTabStateForUrl(String url) {
        restoreTabStateInternal(url, Tab.INVALID_TAB_ID);
    }

    /**
     * If a tab is being restored with the given id, then restore the tab in a frozen state
     * synchronously.
     */
    public void restoreTabStateForId(int id) {
        restoreTabStateInternal(null, id);
    }

    private void restoreTabStateInternal(String url, int id) {
        TabRestoreDetails tabToRestore = null;
        if (mTabLoader != null) {
            if ((url == null && mTabLoader.mTabToRestore.id == id)
                    || (url != null && TextUtils.equals(mTabLoader.mTabToRestore.url, url))) {
                // Steal the task of restoring the tab from the active load tab task.
                mTabLoader.cancel(false);
                tabToRestore = mTabLoader.mTabToRestore;
                loadNextTab(); // Queue up async task to load next tab after we're done here.
            }
        }

        if (tabToRestore == null) {
            if (url == null) {
                tabToRestore = getTabToRestoreById(id);
            } else {
                tabToRestore = getTabToRestoreByUrl(url);
            }
        }

        if (tabToRestore != null) {
            mTabsToRestore.remove(tabToRestore);
            restoreTab(tabToRestore, false);
        }
    }

    private void restoreTab(TabRestoreDetails tabToRestore, boolean setAsActive) {
        // As we do this in startup, and restoring the active tab's state is critical, we permit
        // this read in the event that the prefetch task is not available. Either:
        // 1. The user just upgraded, has not yet set the new active tab id pref yet. Or
        // 2. restoreTab is used to preempt async queue and restore immediately on the UI thread.
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
        // As we add more field to TabState, we are crossing the 10 operation counts threshold to
        // enforce the detection of unbuffered input/output operations, which results in
        // https://crbug.com/1276907. After evaluating the performance impact, here we disabled the
        // detection of unbuffered input/output operations.
        // This will no longer be necessary when the TabState schema is replaced with
        // a FlatBuffer approach - go/tabstate-flatbuffer-decision.
        try {
            int restoredTabId =
                    ChromeSharedPreferences.getInstance()
                            .readInt(
                                    ChromePreferenceKeys.TABMODEL_ACTIVE_TAB_ID,
                                    Tab.INVALID_TAB_ID);
            @Nullable TabState state = maybeRestoreTabState(restoredTabId, tabToRestore);
            restoreTab(tabToRestore, state, setAsActive);
        } catch (Exception e) {
            // Catch generic exception to prevent a corrupted state from crashing the app
            // at startup.
            Log.i(TAG, "loadTabs exception: " + e.toString(), e);
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }
    }

    private @Nullable TabState maybeRestoreTabState(
            int restoredTabId, TabRestoreDetails tabToRestore)
            throws InterruptedException, ExecutionException {
        // If the Tab being restored is the Active Tab and the corresponding TabState prefetch
        // was initiated, use the prefetch result.
        if (restoredTabId == tabToRestore.id && mPrefetchTabStateActiveTabTask != null) {
            return mPrefetchTabStateActiveTabTask.get();
        }
        // Necessary to do on the UI thread as a last resort.
        return TabStateFileManager.restoreTabState(getStateDirectory(), tabToRestore.id);
    }

    /**
     * Handles restoring an individual tab.
     *
     * @param tabToRestore Meta data about the tab to be restored.
     * @param tabState     The previously serialized state of the tab to be restored.
     * @param setAsActive  Whether the tab should be set as the active tab as part of the
     *                     restoration process.
     */
    @VisibleForTesting
    protected void restoreTab(
            TabRestoreDetails tabToRestore, TabState tabState, boolean setAsActive) {
        // If we don't have enough information about the Tab, bail out.
        boolean isIncognito = isIncognitoTabBeingRestored(tabToRestore, tabState);

        if (tabState == null) {
            if (tabToRestore.isIncognito == null) {
                Log.w(TAG, "Failed to restore tab: not enough info about its type was available.");
                return;
            } else if (isIncognito) {
                boolean isNtp = UrlUtilities.isNtpUrl(tabToRestore.url);
                boolean isNtpFromMerge = isNtp && tabToRestore.fromMerge;

                if (!isNtpFromMerge && (!isNtp || !setAsActive || mCancelIncognitoTabLoads)) {
                    Log.i(
                            TAG,
                            "Failed to restore Incognito tab: its TabState could not be restored.");
                    return;
                }
            }
        }
        TabModel model = mTabModelSelector.getModel(isIncognito);

        if (model.isIncognito() != isIncognito) {
            throw new IllegalStateException(
                    "Incognito state mismatch. Restored tab state: "
                            + isIncognito
                            + ". Model: "
                            + model.isIncognito());
        }
        SparseIntArray restoredTabs = isIncognito ? mIncognitoTabsRestored : mNormalTabsRestored;
        int restoredIndex = 0;
        if (tabToRestore.fromMerge) {
            // Put any tabs being merged into this list at the end.
            // TODO(ltian): need to figure out a way to add merged tabs before Browser Actions tabs
            // when tab restore and Browser Actions tab merging happen at the same time.
            restoredIndex = model.getCount();
        } else if (restoredTabs.size() > 0
                && tabToRestore.originalIndex > restoredTabs.keyAt(restoredTabs.size() - 1)) {
            // If the tab's index is too large, restore it at the end of the list.
            restoredIndex = Math.min(model.getCount(), restoredTabs.size());
        } else {
            // Otherwise try to find the tab we should restore before, if any.
            for (int i = 0; i < restoredTabs.size(); i++) {
                if (restoredTabs.keyAt(i) > tabToRestore.originalIndex) {
                    restoredIndex = TabModelUtils.getTabIndexById(model, restoredTabs.valueAt(i));
                    break;
                }
            }
        }
        int tabId = tabToRestore.id;
        if (ChromeFeatureList.sAndroidTabDeclutterDedupeTabIdsKillSwitch.isEnabled()
                && mSeenTabIds.contains(tabId)) {
            mDuplicateTabIdsSeen++;
            return;
        }

        if (tabState != null) {
            @TabRestoreMethod int tabRestoreMethod = TabRestoreMethod.TAB_STATE;
            RecordHistogram.recordEnumeratedHistogram(
                    "Tabs.TabRestoreMethod", tabRestoreMethod, TabRestoreMethod.NUM_ENTRIES);
            Tab tab =
                    mTabCreatorManager
                            .getTabCreator(isIncognito)
                            .createFrozenTab(tabState, tabToRestore.id, restoredIndex);
            if (tabState.shouldMigrate) {
                mTabsToMigrate.add(tab);
            }

            if (tab != null) {
                RecordHistogram.recordBooleanHistogram(
                        "Tabs.TabRestoreUrlMatch", tabToRestore.url.equals(tab.getUrl().getSpec()));
            }

            mSeenTabIds.add(tabId);
        } else {
            Log.w(TAG, "Failed to restore TabState; creating Tab with last known URL.");
            Tab fallbackTab =
                    mTabCreatorManager
                            .getTabCreator(isIncognito)
                            .createNewTab(
                                    new LoadUrlParams(tabToRestore.url),
                                    TabLaunchType.FROM_RESTORE,
                                    null,
                                    restoredIndex);

            if (fallbackTab == null) {
                RecordHistogram.recordEnumeratedHistogram(
                        "Tabs.TabRestoreMethod",
                        TabRestoreMethod.FAILED_TO_RESTORE,
                        TabRestoreMethod.NUM_ENTRIES);
                return;
            }

            RecordHistogram.recordEnumeratedHistogram(
                    "Tabs.TabRestoreMethod",
                    TabRestoreMethod.CREATE_NEW_TAB,
                    TabRestoreMethod.NUM_ENTRIES);

            // restoredIndex might not be the one used in createNewTab so update accordingly.
            tabId = fallbackTab.getId();
            restoredIndex = model.indexOf(fallbackTab);
        }

        // If the tab is being restored from a merge and its index is 0, then the model being
        // merged into doesn't contain any tabs. Select the first tab to avoid having no tab
        // selected. TODO(twellington): The first tab will always be selected. Instead, the tab that
        // was selected in the other model before the merge should be selected after the merge.
        if (setAsActive || (tabToRestore.fromMerge && restoredIndex == 0)) {
            boolean wasIncognitoTabModelSelected = mTabModelSelector.isIncognitoSelected();
            int selectedModelTabCount = mTabModelSelector.getCurrentModel().getCount();

            TabModelUtils.setIndex(model, TabModelUtils.getTabIndexById(model, tabId));
            boolean isIncognitoTabModelSelected = mTabModelSelector.isIncognitoSelected();

            // Setting the index will cause the tab's model to be selected. Set it back to the model
            // that was selected before setting the index if the index is being set during a merge
            // unless the previously selected model is empty (e.g. showing the empty background
            // view on tablets).
            if (tabToRestore.fromMerge
                    && wasIncognitoTabModelSelected != isIncognitoTabModelSelected
                    && selectedModelTabCount != 0) {
                mTabModelSelector.selectModel(wasIncognitoTabModelSelected);
            }
        }
        restoredTabs.put(tabToRestore.originalIndex, tabId);
    }

    /**
     * @return Number of restored tabs on cold startup.
     */
    public int getRestoredTabCount() {
        return mTabsToRestore.size();
    }

    /**
     * Deletes all files in the tab state directory.  This will delete all files and not just those
     * owned by this TabPersistentStore.
     */
    public void clearState() {
        mPersistencePolicy.cancelCleanupInProgress();

        mSequencedTaskRunner.postTask(
                new Runnable() {
                    @Override
                    public void run() {
                        File[] baseStateFiles =
                                TabStateDirectory.getOrCreateBaseStateDirectory().listFiles();
                        if (baseStateFiles == null) return;
                        for (File baseStateFile : baseStateFiles) {
                            // In legacy scenarios (prior to migration, state files could reside in
                            // the
                            // root state directory.  So, handle deleting direct child files as well
                            // as those that reside in sub directories.
                            if (!baseStateFile.isDirectory()) {
                                if (!baseStateFile.delete()) {
                                    Log.e(TAG, "Failed to delete file: " + baseStateFile);
                                }
                            } else {
                                File[] files = baseStateFile.listFiles();
                                if (files == null) continue;
                                for (File file : files) {
                                    if (!file.delete()) {
                                        Log.e(TAG, "Failed to delete file: " + file);
                                    }
                                }
                            }
                        }
                    }
                });

        onStateLoaded();
    }

    /**
     * Cancels loading of {@link Tab}s from disk from saved state. This is useful if the user
     * does an action which impacts all {@link Tab}s, not just the ones currently loaded into
     * the model. For example, if the user tries to close all {@link Tab}s, we need don't want
     * to restore old {@link Tab}s anymore.
     *
     * @param incognito Whether or not to ignore incognito {@link Tab}s or normal
     *                  {@link Tab}s as they are being restored.
     */
    public void cancelLoadingTabs(boolean incognito) {
        if (incognito) {
            mCancelIncognitoTabLoads = true;
        } else {
            mCancelNormalTabLoads = true;
        }
    }

    public void addTabToSaveQueue(Tab tab) {
        addTabToSaveQueueIfApplicable(tab);
        saveNextTab();
    }

    /**
     * @return Whether the specified tab is in any pending save operations.
     */
    @VisibleForTesting
    boolean isTabPendingSave(Tab tab) {
        return (mSaveTabTask != null && mSaveTabTask.mTab.equals(tab)) || mTabsToSave.contains(tab);
    }

    private void addTabToSaveQueueIfApplicable(Tab tab) {
        if (tab == null || tab.isDestroyed()) return;
        TabStateAttributes tabStateAttributes = TabStateAttributes.from(tab);
        @TabStateAttributes.DirtinessState
        int dirtinessState = tabStateAttributes.getDirtinessState();
        if (mTabsToSave.contains(tab)
                || dirtinessState == TabStateAttributes.DirtinessState.CLEAN) {
            return;
        }

        if (mSaveTabTask != null && mSaveTabTask.mId == tab.getId()) {
            RecordHistogram.recordCount100Histogram(
                    "Tabs.PotentialDoubleDirty.SaveQueueSize", mTabsToSave.size());
        }

        mTabsToSave.addLast(tab);
    }

    public void removeTabFromQueues(Tab tab) {
        mTabsToSave.remove(tab);
        mTabsToRestore.remove(getTabToRestoreById(tab.getId()));
        mTabsToMigrate.remove(tab);

        if (mTabLoader != null && mTabLoader.mTabToRestore.id == tab.getId()) {
            mTabLoader.cancel(false);
            mTabLoader = null;
            loadNextTab();
        }

        if (mSaveTabTask != null && mSaveTabTask.mId == tab.getId()) {
            mSaveTabTask.cancel(false);
            mSaveTabTask = null;
            saveNextTab();
        }

        if (mMigrateTabTask != null && mMigrateTabTask.mId == tab.getId()) {
            mMigrateTabTask.cancel(false);
            int nextNumMigration = mMigrateTabTask.mNumMigration + 1;
            mMigrateTabTask = null;
            migrateNextTabIfApplicable(nextNumMigration);
        }

        // If the tab can't be found in any selector, then cleanup it's data.
        if (mTabWindowManager.canTabStateBeDeleted(tab.getId())) {
            cleanupPersistentData(tab.getId(), tab.isIncognito());
        }
    }

    private TabRestoreDetails getTabToRestoreByUrl(String url) {
        for (TabRestoreDetails tabBeingRestored : mTabsToRestore) {
            if (TextUtils.equals(tabBeingRestored.url, url)) {
                return tabBeingRestored;
            }
        }
        return null;
    }

    private TabRestoreDetails getTabToRestoreById(int id) {
        for (TabRestoreDetails tabBeingRestored : mTabsToRestore) {
            if (tabBeingRestored.id == id) {
                return tabBeingRestored;
            }
        }
        return null;
    }

    public void destroy() {
        mDestroyed = true;
        if (mTabModelObserver != null) {
            mTabModelSelector.getModel(false).removeObserver(mTabModelObserver);
            mTabModelSelector.getModel(true).removeObserver(mTabModelObserver);
            mTabModelObserver = null;
        }
        if (mTabRegistrationObserver != null) {
            mTabRegistrationObserver.destroy();
        }
        mPersistencePolicy.destroy();
        if (mTabLoader != null) mTabLoader.cancel(true);
        mTabsToSave.clear();
        mTabsToRestore.clear();
        if (mSaveTabTask != null) mSaveTabTask.cancel(false);
        if (mSaveListTask != null) mSaveListTask.cancel(true);
    }

    private void cleanupPersistentData(int id, boolean incognito) {
        TabStateFileManager.deleteAsync(getStateDirectory(), id, incognito);
        // No need to forward that event to the tab content manager as this is already
        // done as part of the standard tab removal process.
    }

    private TabModelSelectorMetadata saveTabMetadata() throws IOException {
        List<TabRestoreDetails> tabsToRestore = new ArrayList<>();

        // The metadata file may be being written out before all of the Tabs have been restored.
        // Save that information out, as well.
        if (mTabLoader != null) tabsToRestore.add(mTabLoader.mTabToRestore);
        for (TabRestoreDetails details : mTabsToRestore) {
            tabsToRestore.add(details);
        }

        return saveTabModelSelectorMetadata(mTabModelSelector, tabsToRestore);
    }

    /**
     * Records state of {@code selector} into a separate DataStructure to be used for save/restore.
     *
     * @param selector The {@link TabModelSelector} to process.
     * @param tabsBeingRestored Tabs that are in the process of being restored.
     * @return {@link TabModelSelectorMetadata} containing the meta data of {@code selector}.
     */
    @VisibleForTesting
    public static TabModelSelectorMetadata saveTabModelSelectorMetadata(
            TabModelSelector selector, List<TabRestoreDetails> tabsBeingRestored)
            throws IOException {
        ThreadUtils.assertOnUiThread();

        // TODO(crbug.com/40549331): Convert TabModelMetadata to use GURL.
        TabModelMetadata incognitoInfo = metadataFromModel(selector, true);

        TabModel normalModel = selector.getModel(false);
        TabModelMetadata normalInfo = metadataFromModel(selector, false);

        // Cache the active tab id to be pre-loaded next launch.
        int activeTabId = Tab.INVALID_TAB_ID;
        int activeIndex = normalModel.index();
        @ActiveTabState int activeTabState = ActiveTabState.EMPTY;
        if (activeIndex != TabList.INVALID_TAB_INDEX) {
            Tab activeTab = normalModel.getTabAt(activeIndex);
            activeTabId = activeTab.getId();
            activeTabState =
                    UrlUtilities.isNtpUrl(activeTab.getUrl())
                            ? ActiveTabState.NTP
                            : ActiveTabState.OTHER;
        }

        // Add information about the tabs that haven't finished being loaded.
        // We shouldn't have to worry about Tab duplication because the tab details are processed
        // only on the UI Thread.
        if (tabsBeingRestored != null) {
            for (TabRestoreDetails details : tabsBeingRestored) {
                // isIncognito was added in M61 (see https://crbug.com/485217), so it is extremely
                // unlikely that isIncognito will be null. But if it is, assume that the tab is
                // incognito so that #restoreTab() will require a tab state file on disk to
                // restore. If a tab state file exists and the tab is not actually incognito, it
                // will be restored in the normal tab model. If a tab state file does not exist,
                // the tab will not be restored.
                if (details.isIncognito == null || details.isIncognito) {
                    incognitoInfo.ids.add(details.id);
                    incognitoInfo.urls.add(details.url);
                } else {
                    normalInfo.ids.add(details.id);
                    normalInfo.urls.add(details.url);
                }
            }
        }

        Log.d(
                TAG,
                "Recording tab lists; counts: "
                        + normalInfo.ids.size()
                        + ", "
                        + incognitoInfo.ids.size());

        saveTabModelPrefs(normalInfo, incognitoInfo, activeTabId, activeTabState);
        return new TabModelSelectorMetadata(normalInfo, incognitoInfo);
    }

    @VisibleForTesting
    public static void saveTabModelPrefs(
            TabModelMetadata normalInfo,
            TabModelMetadata incognitoInfo,
            int activeTabId,
            int activeTabState) {
        // Always override the existing value in case there is no active tab.
        SharedPreferences.Editor editor = ChromeSharedPreferences.getInstance().getEditor();
        editor.putInt(ChromePreferenceKeys.TABMODEL_ACTIVE_TAB_ID, activeTabId);
        editor.putInt(ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE, activeTabState);
        editor.apply();
    }

    /**
     * Creates a TabModelMetadata for the given TabModel mode.
     *
     * @param selector The object of {@link TabModelSelector}
     * @param isIncognito Whether the TabModel is incognito.
     */
    private static TabModelMetadata metadataFromModel(
            TabModelSelector selector, boolean isIncognito) {
        TabModel tabModel = selector.getModel(isIncognito);
        TabModelMetadata modelInfo = new TabModelMetadata(tabModel.index());

        int activeIndex = tabModel.index();
        for (int i = 0; i < tabModel.getCount(); i++) {
            Tab tab = tabModel.getTabAt(i);
            // This tab has likely just been deleted, and it's possible we're being notified before
            // hand because undo is not allowed. This shouldn't be persisted.
            if (tab.isClosing()) {
                // Select the previous tab if there is one. 0 should be fine even if there are no
                // tabs left.
                if (i == activeIndex) {
                    modelInfo.index = Math.max(0, modelInfo.ids.size() - 1);
                }
                continue;
            }

            if (i == activeIndex) {
                // If any non-active NTPs have been skipped, the serialized tab model index
                // needs to be adjusted.
                modelInfo.index = modelInfo.ids.size();
            } else if (shouldSkipTab(tab)) {
                continue;
            }
            modelInfo.ids.add(tab.getId());
            modelInfo.urls.add(tab.getUrl().getSpec());
        }
        return modelInfo;
    }

    private static boolean shouldSkipTab(@NonNull Tab tab) {
        boolean isNtp = tab.isNativePage() && UrlUtilities.isNtpUrl(tab.getUrl());
        if (!isNtp) return false;

        // Only skip NTP tabs that are not in a tab group.
        return tab.getTabGroupId() == null;
    }

    private void saveListToFile(TabModelSelectorMetadata listData) {
        if (Objects.equals(mLastSavedMetadata, listData)) return;
        // Save the index file containing the list of tabs to restore.
        File metadataFile = new File(getStateDirectory(), mPersistencePolicy.getMetadataFileName());
        saveListToFile(metadataFile, listData);
        mLastSavedMetadata = listData;
    }

    /**
     * Atomically writes the given tab model selector data to disk.
     *
     * @param metadataFile File to save TabModel data into.
     * @param metadata TabModel data in copied types.
     */
    public static void saveListToFile(File metadataFile, TabModelSelectorMetadata metadata) {
        synchronized (SAVE_LIST_LOCK) {
            AtomicFile file = new AtomicFile(metadataFile);
            FileOutputStream output = null;
            try {
                output = file.startWrite();

                int standardCount = metadata.normalModelMetadata.ids.size();
                int incognitoCount = metadata.incognitoModelMetadata.ids.size();

                // Determine how many Tabs there are.
                int numTabsTotal = incognitoCount + standardCount;
                Log.d(TAG, "Persisting tab lists; " + standardCount + ", " + incognitoCount);

                // Save the index file containing the list of tabs to restore. Wrap a
                // BufferedOutputStream to batch/buffer actual writes. Most urls are far smaller
                // than the 8K buffer.
                DataOutputStream stream = new DataOutputStream(new BufferedOutputStream(output));
                stream.writeInt(SAVED_STATE_VERSION);
                stream.writeInt(numTabsTotal);
                stream.writeInt(incognitoCount);
                stream.writeInt(metadata.incognitoModelMetadata.index);
                stream.writeInt(metadata.normalModelMetadata.index + incognitoCount);

                // Save incognito state first, so when we load, if the incognito files are
                // unreadable
                // we can fall back easily onto the standard selected tab.
                for (int i = 0; i < incognitoCount; i++) {
                    stream.writeInt(metadata.incognitoModelMetadata.ids.get(i));
                    stream.writeUTF(metadata.incognitoModelMetadata.urls.get(i));
                }
                for (int i = 0; i < standardCount; i++) {
                    stream.writeInt(metadata.normalModelMetadata.ids.get(i));
                    stream.writeUTF(metadata.normalModelMetadata.urls.get(i));
                }

                stream.flush();
                file.finishWrite(output);

            } catch (IOException e) {
                if (output != null) file.failWrite(output);
            }
        }
    }

    /**
     * @param isIncognitoSelected Whether the tab model is incognito.
     * @return A callback for reading data from tab models.
     */
    private OnTabStateReadCallback createOnTabStateReadCallback(
            final boolean isIncognitoSelected, final boolean fromMerge) {
        return new OnTabStateReadCallback() {
            @Override
            public void onDetailsRead(
                    int index,
                    int id,
                    String url,
                    Boolean isIncognito,
                    boolean isStandardActiveIndex,
                    boolean isIncognitoActiveIndex) {
                if (mLoadInProgress) {
                    // If a load and merge are both in progress, that means two metadata files
                    // are being read. If a merge was previously started and interrupted due to the
                    // app dying, the two metadata files may contain duplicate IDs. Skip tabs with
                    // duplicate IDs.
                    if (mPersistencePolicy.isMergeInProgress() && mTabIdsToRestore.contains(id)) {
                        return;
                    }

                    mTabIdsToRestore.add(id);
                }

                // Note that incognito tab may not load properly so we may need to use
                // the current tab from the standard model.
                // This logic only works because we store the incognito indices first.
                TabRestoreDetails details =
                        new TabRestoreDetails(id, index, isIncognito, url, fromMerge);

                if (!fromMerge
                        && ((isIncognitoActiveIndex && isIncognitoSelected)
                                || (isStandardActiveIndex && !isIncognitoSelected))) {
                    // Active tab gets loaded first
                    mTabsToRestore.addFirst(details);
                } else {
                    mTabsToRestore.addLast(details);
                }

                for (TabPersistentStoreObserver observer : mObservers) {
                    observer.onDetailsRead(
                            index,
                            id,
                            url,
                            isStandardActiveIndex,
                            isIncognitoActiveIndex,
                            isIncognito,
                            fromMerge);
                }
            }
        };
    }

    /**
     * If a global max tab ID has not been computed and stored before, then check all the state
     * folders and calculate a new global max tab ID to be used. Must be called before any new tabs
     * are created.
     *
     * @throws IOException
     */
    private void checkAndUpdateMaxTabId() throws IOException {
        if (ChromeSharedPreferences.getInstance()
                .readBoolean(ChromePreferenceKeys.TABMODEL_HAS_COMPUTED_MAX_ID, false)) {
            return;
        }

        int maxId = 0;
        // Calculation of the max tab ID is done only once per user and is stored in
        // SharedPreferences afterwards.  This is done on the UI thread because it is on the
        // critical patch to initializing the TabIdManager with the correct max tab ID.
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
        try {
            File[] subDirectories = TabStateDirectory.getOrCreateBaseStateDirectory().listFiles();
            if (subDirectories != null) {
                for (File subDirectory : subDirectories) {
                    if (!subDirectory.isDirectory()) {
                        assert false
                                : "Only directories should exist below the base state directory";
                        continue;
                    }
                    File[] files = subDirectory.listFiles();
                    if (files == null) continue;

                    for (File file : files) {
                        Pair<Integer, Boolean> tabStateInfo =
                                TabStateFileManager.parseInfoFromFilename(file.getName());
                        if (tabStateInfo != null) {
                            maxId = Math.max(maxId, tabStateInfo.first);
                        } else if (isMetadataFile(file.getName())) {
                            DataInputStream stream = null;
                            try {
                                stream =
                                        new DataInputStream(
                                                new BufferedInputStream(new FileInputStream(file)));
                                maxId = Math.max(maxId, readSavedMetadataFile(stream, null, null));
                            } finally {
                                StreamUtil.closeQuietly(stream);
                            }
                        }
                    }
                }
            }
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }
        TabIdManager.getInstance().incrementIdCounterTo(maxId);
        ChromeSharedPreferences.getInstance()
                .writeBoolean(ChromePreferenceKeys.TABMODEL_HAS_COMPUTED_MAX_ID, true);
    }

    /**
     * Extracts the tab information from a given tab state metadata stream.
     *
     * @param stream The stream pointing to the tab state metadata file to be parsed.
     * @param callback A callback to be streamed updates about the tab state information being read.
     * @param tabIds A mapping of tab ID to whether the tab is an off the record tab.
     * @return The next available tab ID based on the maximum ID referenced in this state file.
     */
    public static int readSavedMetadataFile(
            DataInputStream stream,
            @Nullable OnTabStateReadCallback callback,
            @Nullable SparseBooleanArray tabIds)
            throws IOException {
        if (stream == null) return 0;
        int nextId = 0;
        boolean skipUrlRead = false;
        boolean skipIncognitoCount = false;
        final int version = stream.readInt();
        if (version != SAVED_STATE_VERSION) {
            // We don't support restoring Tab data from before M18.
            if (version < 3) return 0;
            // Older versions are missing newer data.
            if (version < 5) skipIncognitoCount = true;
            if (version < 4) skipUrlRead = true;
        }

        final int count = stream.readInt();
        final int incognitoCount = skipIncognitoCount ? -1 : stream.readInt();
        final int incognitoActiveIndex = stream.readInt();
        int standardActiveIndex = stream.readInt();
        if (standardActiveIndex < incognitoCount) {
            // See https://crbug.com/354041918. This is equal to the original standard active index
            // + incognitoCount. If there are not standard tabs, that would be -1 + incognitoCount,
            // which unexpectedly maps to the last incognito tab. Adjust here.
            standardActiveIndex = TabModel.INVALID_TAB_INDEX;
        }
        if (count < 0 || incognitoActiveIndex >= count || standardActiveIndex >= count) {
            throw new IOException();
        }

        for (int i = 0; i < count; i++) {
            int id = stream.readInt();
            String tabUrl = skipUrlRead ? "" : stream.readUTF();
            if (id >= nextId) nextId = id + 1;
            if (tabIds != null) tabIds.append(id, true);

            Boolean isIncognito = (incognitoCount < 0) ? null : i < incognitoCount;

            if (callback != null) {
                callback.onDetailsRead(
                        i,
                        id,
                        tabUrl,
                        isIncognito,
                        i == standardActiveIndex,
                        i == incognitoActiveIndex);
            }
        }
        return nextId;
    }

    /**
     * Triggers the next save tab task.  Clients do not need to call this as it will be triggered
     * automatically by calling {@link #addTabToSaveQueue(Tab)}.
     */
    @VisibleForTesting
    void saveNextTab() {
        if (mSaveTabTask != null) return;
        if (!mTabsToSave.isEmpty()) {
            Tab tab = mTabsToSave.removeFirst();
            mSaveTabTask = new SaveTabTask(tab);
            mSaveTabTask.executeOnTaskRunner(mSequencedTaskRunner);
            // Ensure Tab is moved to the front of the migration queue to ensure the two versions
            // of the TabState file are kept in sync.
            mTabsToMigrate.remove(tab);
            mTabsToMigrate.addFirst(tab);
            migrateNextTabIfApplicable(1);
        } else {
            saveTabListAsynchronously();
        }
    }

    private void migrateNextTabIfApplicable(int numMigration) {
        // Only migrate TabState to FlatBuffer format if:
        // - FlatBuffer schema flag is enabled
        // - We haven't hit the limit of sMaxMigrationsPerSave migrations per save yet
        // - Deferred startup is complete (to reduce the risk of Jank).
        if (!isFlatBufferSchemaEnabled()
                || mTabsToMigrate.isEmpty()
                || numMigration > sMaxMigrationsPerSave
                || !sDeferredStartupComplete) {
            return;
        }
        Tab tab = mTabsToMigrate.removeFirst();
        mMigrateTabTask = new MigrateTabTask(tab, numMigration);
        mMigrateTabTask.executeOnTaskRunner(mSequencedTaskRunner);
    }

    /** Kick off an AsyncTask to save the current list of Tabs. */
    public void saveTabListAsynchronously() {
        if (mSaveListTask != null) mSaveListTask.cancel(true);
        mSaveListTask = new SaveListTask();
        mSaveListTask.executeOnTaskRunner(mSequencedTaskRunner);
    }

    private class SaveTabTask extends AsyncTask<Void> {
        Tab mTab;
        int mId;
        TabState mState;
        boolean mEncrypted;
        boolean mStateSaved;

        SaveTabTask(Tab tab) {
            mTab = tab;
            mId = tab.getId();
            mEncrypted = tab.isIncognito();
        }

        @Override
        protected void onPreExecute() {
            if (mDestroyed || isCancelled()) return;
            TabStateAttributes.from(mTab).clearTabStateDirtiness();
            mState = TabStateExtractor.from(mTab);
        }

        @Override
        protected Void doInBackground() {
            mStateSaved = saveTabState(mId, mEncrypted, mState);
            return null;
        }

        @Override
        protected void onPostExecute(Void v) {
            if (mDestroyed || isCancelled()) return;
            mSaveTabTask = null;
            saveNextTab();
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    /** Migrate Tab to new FlatBuffer format. */
    class MigrateTabTask extends AsyncTask<Void> {
        Tab mTab;
        int mId;
        TabState mState;
        boolean mEncrypted;
        int mNumMigration;
        boolean mMigrationComplete;

        MigrateTabTask(Tab tab, int numMigration) {
            mTab = tab;
            mId = tab.getId();
            mEncrypted = tab.isIncognito();
            mNumMigration = numMigration;
        }

        @Override
        protected void onPreExecute() {
            if (mDestroyed || mTab.isDestroyed() || isCancelled()) return;
            try {
                mState = TabStateExtractor.from(mTab);
            } catch (Exception e) {
                Log.d(TAG_MIGRATION, "Error MigrateTabTask#onPreExecute", e);
                throw e;
            }
        }

        @Override
        protected Void doInBackground() {
            try {
                mMigrationComplete =
                        TabStateFileManager.migrateTabState(
                                getStateDirectory(), mState, mId, mEncrypted);
            } catch (Exception e) {
                Log.d(TAG_MIGRATION, "Error MigrateTabTask#doInBackground", e);
                throw e;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void v) {
            if (mDestroyed || isCancelled()) return;
            mMigrateTabTask = null;
            migrateNextTabIfApplicable(mNumMigration + 1);
        }
    }

    /** Stores meta data about the TabModelSelector which can be serialized to disk. */
    public static class TabModelSelectorMetadata {
        public final TabModelMetadata normalModelMetadata;
        public final TabModelMetadata incognitoModelMetadata;

        public TabModelSelectorMetadata(
                TabModelMetadata normalModelMetadata, TabModelMetadata incognitoModelMetadata) {
            this.normalModelMetadata = normalModelMetadata;
            this.incognitoModelMetadata = incognitoModelMetadata;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof TabModelSelectorMetadata that)) return false;
            return Objects.equals(normalModelMetadata, that.normalModelMetadata)
                    && Objects.equals(incognitoModelMetadata, that.incognitoModelMetadata);
        }

        @Override
        public int hashCode() {
            return Objects.hash(normalModelMetadata, incognitoModelMetadata);
        }
    }

    private class SaveListTask extends AsyncTask<Void> {
        TabModelSelectorMetadata mMetadata;

        @Override
        protected void onPreExecute() {
            if (mDestroyed || isCancelled()) return;
            try {
                mMetadata = saveTabMetadata();
            } catch (IOException e) {
                mMetadata = null;
            }
        }

        @Override
        protected Void doInBackground() {
            if (mMetadata == null || isCancelled()) return null;
            saveListToFile(mMetadata);
            return null;
        }

        @Override
        protected void onPostExecute(Void v) {
            if (mDestroyed || isCancelled()) {
                mMetadata = null;
                return;
            }

            if (mSaveListTask == this) {
                mSaveListTask = null;
                for (TabPersistentStoreObserver observer : mObservers) {
                    observer.onMetadataSavedAsynchronously(mMetadata);
                }
                mMetadata = null;
            }
        }
    }

    private File getStateDirectory() {
        return mPersistencePolicy.getOrCreateStateDirectory();
    }

    /**
     * Returns a file pointing at the TabState corresponding to the given Tab.
     * @param tabId ID of the TabState to locate.
     * @param encrypted Whether or not the tab is encrypted.
     * @return File pointing at the TabState for the Tab.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public File getTabStateFile(int tabId, boolean encrypted) {
        return TabStateFileManager.getTabStateFile(
                getStateDirectory(), tabId, encrypted, /* isFlatBuffer= */ false);
    }

    /**
     * Saves the TabState with the given ID.
     * @param tabId ID of the Tab.
     * @param encrypted Whether or not the TabState is encrypted.
     * @param state TabState for the Tab.
     */
    private boolean saveTabState(int tabId, boolean encrypted, TabState state) {
        if (state == null) return false;

        try {
            TabStateFileManager.saveState(getStateDirectory(), state, tabId, encrypted);
            return true;
        } catch (OutOfMemoryError e) {
            android.util.Log.e(
                    TAG, "Out of memory error while attempting to save tab state.  Erasing.");
            deleteTabState(tabId, encrypted);
        }
        return false;
    }

    /**
     * Deletes the TabState corresponding to the given Tab.
     *
     * @param id ID of the TabState to delete.
     * @param encrypted Whether or not the tab is encrypted.
     */
    private void deleteTabState(int id, boolean encrypted) {
        TabStateFileManager.deleteTabState(getStateDirectory(), id, encrypted);
    }

    private void onStateLoaded() {
        for (TabPersistentStoreObserver observer : mObservers) {
            // mergeState() starts an AsyncTask to call this and this calls
            // onTabStateInitialized which should be called from the UI thread.
            PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> observer.onStateLoaded());
        }
    }

    private void loadNextTab() {
        if (mDestroyed) return;

        if (mTabsToRestore.isEmpty()) {
            mNormalTabsRestored = null;
            mIncognitoTabsRestored = null;
            mLoadInProgress = false;

            // If tabs are done being merged into this instance, save the tab metadata file for this
            // TabPersistentStore and delete the metadata file for the other instance, then notify
            // observers.
            if (mPersistencePolicy.isMergeInProgress()) {
                PostTask.postTask(
                        TaskTraits.UI_DEFAULT,
                        new Runnable() {
                            @Override
                            public void run() {
                                // This eventually calls saveTabModelSelectorMetadata() which much
                                // be called from the UI thread. #mergeState() starts an async task
                                // in the background that goes through this code path.
                                saveTabListAsynchronously();
                            }
                        });
                for (String mergedFileName : new HashSet<>(mMergedFileNames)) {
                    deleteFileAsync(mergedFileName, true);
                }
                for (TabPersistentStoreObserver observer : mObservers) observer.onStateMerged();
            }

            recordLegacyTabCountMetrics();
            recordTabCountMetrics();
            recordRestoreDuration();
            cleanUpPersistentData();
            onStateLoaded();
            mTabLoader = null;
            Log.d(
                    TAG,
                    "Loaded tab lists; counts: "
                            + mTabModelSelector.getModel(false).getCount()
                            + ","
                            + mTabModelSelector.getModel(true).getCount());

            // If there were any duplicate tab ids seen, then force a write to overwrite tab ids.
            if (ChromeFeatureList.sAndroidTabDeclutterDedupeTabIdsKillSwitch.isEnabled()
                    && mDuplicateTabIdsSeen > 0) {
                recordDuplicateTabIdMetrics();
                saveState();
            }
        } else {
            TabRestoreDetails tabToRestore = mTabsToRestore.removeFirst();
            mTabLoader = new TabLoader(tabToRestore);
            mTabLoader.load();
        }
    }

    private void recordDuplicateTabIdMetrics() {
        RecordHistogram.recordCount1000Histogram(
                "Tabs.Startup.TabCount2." + mClientTag + ".DuplicateTabIds", mDuplicateTabIdsSeen);
    }

    protected void recordLegacyTabCountMetrics() {
        RecordHistogram.recordCount1MHistogram(
                "Tabs.Startup.TabCount.Regular", mTabModelSelector.getModel(false).getCount());
        RecordHistogram.recordCount1MHistogram(
                "Tabs.Startup.TabCount.Incognito", mTabModelSelector.getModel(true).getCount());
    }

    private void recordTabCountMetrics() {
        RecordHistogram.recordCount1MHistogram(
                "Tabs.Startup.TabCount2." + mClientTag + ".Regular",
                mTabModelSelector.getModel(false).getCount());
        RecordHistogram.recordCount1MHistogram(
                "Tabs.Startup.TabCount2." + mClientTag + ".Incognito",
                mTabModelSelector.getModel(true).getCount());
    }

    private void recordRestoreDuration() {
        if (mTabRestoreStartTime == INVALID_TIME) return;

        long duration = SystemClock.elapsedRealtime() - mTabRestoreStartTime;
        RecordHistogram.recordMediumTimesHistogram(
                "Tabs.Startup.RestoreDuration." + mClientTag, duration);
        int tabCount = mTabModelSelector.getTotalTabCount();
        if (tabCount != 0) {
            RecordHistogram.recordTimesHistogram(
                    "Tabs.Startup.RestoreDurationPerTab." + mClientTag,
                    Math.round((float) duration / tabCount));
        }
        mTabRestoreStartTime = INVALID_TIME;
    }

    /**
     * Manages loading of {@link TabState}. Also used to track if a load is in progress and the tab
     * details of that load. TODO(b/298058408) deprecate TabLoader
     */
    private class TabLoader {
        public final TabRestoreDetails mTabToRestore;
        private LoadTabTask mLoadTabTask;

        /**
         * @param tabToRestore details of {@link Tab} which will be read from storage
         */
        TabLoader(TabRestoreDetails tabToRestore) {
            mTabToRestore = tabToRestore;
        }

        /** Load {@link TabState} */
        public void load() {
            mLoadTabTask = new LoadTabTask(mTabToRestore);
            mLoadTabTask.executeOnTaskRunner(mSequencedTaskRunner);
        }

        public void cancel(boolean mayInterruptIfRunning) {
            if (mLoadTabTask != null) {
                mLoadTabTask.cancel(mayInterruptIfRunning);
            }
        }
    }

    /** Asynchronously triggers a cleanup of any unused persistent data. */
    private void cleanUpPersistentData() {
        mPersistencePolicy.cleanupUnusedFiles(
                new Callback<TabPersistenceFileInfo>() {
                    @Override
                    public void onResult(TabPersistenceFileInfo result) {
                        if (result == null) return;
                        for (String metadataFile : result.getMetadataFiles()) {
                            deleteFileAsync(metadataFile, true);
                        }
                        for (TabStateFileInfo tabStateFileInfo : result.getTabStateFileInfos()) {
                            TabStateFileManager.deleteAsync(
                                    getStateDirectory(),
                                    tabStateFileInfo.tabId,
                                    tabStateFileInfo.isEncrypted);
                        }
                    }
                });
        performPersistedTabDataMaintenance(null);
        TabStateFileManager.cleanupUnusedFiles(getStateDirectory());
    }

    @VisibleForTesting
    protected void performPersistedTabDataMaintenance(Runnable onCompleteForTesting) {
        // PersistedTabData currently doesn't support Custom Tabs, so maintenance
        // only needs to be performed for regular Tabs.
        if (mPersistencePolicy instanceof TabbedModeTabPersistencePolicy) {
            mPersistencePolicy.getAllTabIds(
                    (res) -> {
                        List<Integer> allTabIds = new ArrayList<>();
                        for (int i = 0; i < res.size(); i++) {
                            allTabIds.add(res.keyAt(i));
                        }
                        PersistedTabData.performStorageMaintenance(allTabIds);
                        if (onCompleteForTesting != null) {
                            onCompleteForTesting.run();
                        }
                    });
        }
    }

    /**
     * Clean up persistent state for a given instance.
     * @param instanceId Instance ID.
     */
    public void cleanupStateFile(int instanceId) {
        mPersistencePolicy.cleanupInstanceState(
                instanceId,
                new Callback<TabPersistenceFileInfo>() {
                    @Override
                    public void onResult(TabPersistenceFileInfo result) {
                        // Delete the instance state file (tab_stateX) as well.
                        deleteFileAsync(
                                TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(
                                        instanceId),
                                true);

                        // |result| can be null if the task gets cancelled.
                        if (result == null) return;
                        for (String metadataFile : result.getMetadataFiles()) {
                            deleteFileAsync(metadataFile, true);
                        }
                        for (TabStateFileInfo tabStateFileInfo : result.getTabStateFileInfos()) {
                            TabStateFileManager.deleteAsync(
                                    mPersistencePolicy.getOrCreateStateDirectory(),
                                    tabStateFileInfo.tabId,
                                    tabStateFileInfo.isEncrypted);
                        }
                    }
                });
    }

    /**
     * File mutations (e.g. saving & deleting) are explicitly serialized to ensure that they occur
     * in the correct order.
     *
     * @param file Name of file under the state directory to be deleted.
     * @param useSerialExecution true if serial executor will be used
     */
    private void deleteFileAsync(final String file, boolean useSerialExecution) {
        if (useSerialExecution) {
            new BackgroundOnlyAsyncTask<Void>() {
                @Override
                protected Void doInBackground() {
                    deleteStateFile(file);
                    return null;
                }
            }.executeOnTaskRunner(mSequencedTaskRunner);
        } else {
            PostTask.runOrPostTask(
                    TaskTraits.BEST_EFFORT_MAY_BLOCK,
                    () -> {
                        deleteStateFile(file);
                    });
        }
    }

    private void deleteStateFile(String file) {
        ThreadUtils.assertOnBackgroundThread();
        File stateFile = new File(getStateDirectory(), file);
        if (stateFile.exists()) {
            if (!stateFile.delete()) Log.e(TAG, "Failed to delete file: " + stateFile);

            // The merge isn't completely finished until the other TabPersistentStores'
            // metadata files are deleted.
            boolean wasMergeFile = mMergedFileNames.remove(file);
            if (wasMergeFile && mMergedFileNames.isEmpty()) {
                mPersistencePolicy.setMergeInProgress(false);
            }
        }
    }

    private class LoadTabTask extends AsyncTask<TabState> {
        private final TabRestoreDetails mTabToRestore;
        private TabState mTabState;

        public LoadTabTask(TabRestoreDetails tabToRestore) {
            mTabToRestore = tabToRestore;
            TraceEvent.startAsync("LoadTabTask", mTabToRestore.id);
            TraceEvent.startAsync("LoadTabState", mTabToRestore.id);
        }

        @Override
        protected TabState doInBackground() {
            if (mDestroyed || isCancelled()) return null;
            try {
                return TabStateFileManager.restoreTabState(getStateDirectory(), mTabToRestore.id);
            } catch (Exception e) {
                Log.w(TAG, "Unable to read state: " + e);
                return null;
            }
        }

        @Override
        protected void onPostExecute(TabState tabState) {
            TraceEvent.finishAsync("LoadTabState", mTabToRestore.id);
            mTabState = tabState;

            TraceEvent.finishAsync("LoadTabTask", mTabToRestore.id);
            if (mDestroyed || isCancelled()) {
                return;
            }

            completeLoad(mTabToRestore, mTabState);
        }
    }

    private void completeLoad(TabRestoreDetails tabToRestore, TabState tabState) {
        boolean isIncognito = isIncognitoTabBeingRestored(tabToRestore, tabState);
        boolean isLoadCancelled =
                (isIncognito && mCancelIncognitoTabLoads)
                        || (!isIncognito && mCancelNormalTabLoads);
        if (!isLoadCancelled) {
            restoreTab(tabToRestore, tabState, false);
        }

        loadNextTab();
    }

    /** Provides additional meta data to restore an individual tab. */
    @VisibleForTesting
    protected static final class TabRestoreDetails {
        public final int id;
        public final int originalIndex;
        public final String url;
        public final Boolean isIncognito;
        public final Boolean fromMerge;

        public TabRestoreDetails(
                int id, int originalIndex, Boolean isIncognito, String url, Boolean fromMerge) {
            this.id = id;
            this.originalIndex = originalIndex;
            this.url = url;
            this.isIncognito = isIncognito;
            this.fromMerge = fromMerge;
        }
    }

    /**
     * Determines if a Tab being restored is definitely an Incognito Tab.
     *
     * This function can fail to determine if a Tab is incognito if not enough data about the Tab
     * was successfully saved out.
     *
     * @return True if the tab is definitely Incognito, false if it's not or if it's undecideable.
     */
    private boolean isIncognitoTabBeingRestored(TabRestoreDetails tabDetails, TabState tabState) {
        if (tabState != null) {
            // The Tab's previous state was completely restored.
            return tabState.isIncognito();
        } else if (tabDetails.isIncognito != null) {
            // The TabState couldn't be restored, but we have some information about the tab.
            return tabDetails.isIncognito;
        } else {
            // The tab's type is undecidable.
            return false;
        }
    }

    private AsyncTask<DataInputStream> startFetchTabListTask(
            TaskRunner taskRunner, final String stateFileName) {
        return new BackgroundOnlyAsyncTask<DataInputStream>() {
            @Override
            protected DataInputStream doInBackground() {
                Log.d(TAG, "Starting to fetch tab list for " + stateFileName);
                File stateFile = new File(getStateDirectory(), stateFileName);
                if (!stateFile.exists()) {
                    Log.d(TAG, "State file does not exist.");
                    return null;
                }
                FileInputStream stream = null;
                byte[] data;
                try {
                    stream = new FileInputStream(stateFile);
                    data = new byte[(int) stateFile.length()];
                    stream.read(data);
                } catch (IOException exception) {
                    Log.e(TAG, "Could not read state file.", exception);
                    return null;
                } finally {
                    StreamUtil.closeQuietly(stream);
                }
                Log.d(TAG, "Finished fetching tab list.");
                return new DataInputStream(new ByteArrayInputStream(data));
            }
        }.executeOnTaskRunner(taskRunner);
    }

    private void startPrefetchActiveTabTask(TaskRunner taskRunner) {
        final int activeTabId =
                ChromeSharedPreferences.getInstance()
                        .readInt(ChromePreferenceKeys.TABMODEL_ACTIVE_TAB_ID, Tab.INVALID_TAB_ID);
        if (activeTabId == Tab.INVALID_TAB_ID) return;
        prefetchActiveTabTask(activeTabId, taskRunner);
    }

    private void prefetchActiveTabTask(int activeTabId, TaskRunner taskRunner) {
        mPrefetchTabStateActiveTabTask =
                new BackgroundOnlyAsyncTask<TabState>() {
                    @Override
                    protected TabState doInBackground() {
                        return TabStateFileManager.restoreTabState(
                                getStateDirectory(), activeTabId);
                    }
                }.executeOnTaskRunner(taskRunner);
    }

    public void addObserver(TabPersistentStoreObserver observer) {
        mObservers.addObserver(observer);
    }

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

    /**
     * @param uniqueTag The tag that uniquely identifies this state file. Typically this is an index
     *     or ID.
     * @return The name of the state file.
     */
    public static String getMetadataFileName(String uniqueTag) {
        return SAVED_METADATA_FILE_PREFIX + uniqueTag;
    }

    /**
     * Parses the metadata file name and returns the unique tag encoded into it.
     *
     * @param metadataFileName The state file name to be parsed.
     * @return The unique tag used when generating the file name.
     */
    public static String getMetadataFileUniqueTag(String metadataFileName) {
        assert isMetadataFile(metadataFileName);
        return metadataFileName.substring(SAVED_METADATA_FILE_PREFIX.length());
    }

    /**
     * Returns whether the specified filename matches the expected pattern of the tab metadata
     * files.
     */
    public static boolean isMetadataFile(String fileName) {
        // The .new/.bak suffixes may be added internally by AtomicFile before the file finishes
        // writing. Ignore files in this transitory state.
        return fileName.startsWith(SAVED_METADATA_FILE_PREFIX)
                && !fileName.endsWith(".new")
                && !fileName.endsWith(".bak");
    }

    /**
     * @return The shared pref APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE. This is used when we need to
     *         know the last known tab state before the active tab from the tab state is read.
     */
    public static @ActiveTabState int readLastKnownActiveTabStatePref() {
        return ChromeSharedPreferences.getInstance()
                .readInt(
                        ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
                        ActiveTabState.EMPTY);
    }

    public static void onDeferredStartup() {
        sDeferredStartupComplete = true;
    }

    public static void resetDeferredStartupCompleteForTesting() {
        sDeferredStartupComplete = false;
    }

    public MigrateTabTask getMigrateTabTaskForTesting() {
        return mMigrateTabTask;
    }

    protected Deque<Tab> getTabsToSaveForTesting() {
        return mTabsToSave;
    }

    protected boolean isSavingAndMigratingIdleForTesting() {
        // Idle if
        // 1) no save is in progress and the save queue is empty.
        // 2) No migration in progress (it's ok for the migration queue to be non-empty as only
        // sMaxMigrationsPerSave are executed at a time).
        return mSaveTabTask == null && mTabsToSave.isEmpty() && mMigrateTabTask == null;
    }

    public void setMigrateTabTaskForTesting(MigrateTabTask migrateTabTask) {
        mMigrateTabTask = migrateTabTask;
    }

    protected Deque<Tab> getTabsToMigrateForTesting() {
        return mTabsToMigrate;
    }

    private static boolean isFlatBufferSchemaEnabled() {
        return ChromeFeatureList.sTabStateFlatBuffer.isEnabled();
    }

    SequencedTaskRunner getTaskRunnerForTests() {
        return mSequencedTaskRunner;
    }

    void addTabToRestoreForTesting(TabRestoreDetails tabDetails) {
        mTabsToRestore.add(tabDetails);
    }

    public TabPersistencePolicy getTabPersistencePolicyForTesting() {
        return mPersistencePolicy;
    }

    public List<Pair<AsyncTask<DataInputStream>, String>> getTabListToMergeTasksForTesting() {
        return mPrefetchTabListToMergeTasks;
    }

    public AsyncTask<TabState> getPrefetchTabStateActiveTabTaskForTesting() {
        return mPrefetchTabStateActiveTabTask;
    }
}