chromium/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/TabReparentingController.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.tab_activity_glue;

import androidx.annotation.NonNull;

import org.chromium.base.Log;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.AsyncTabParamsManager;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabReparentingParams;
import org.chromium.url.GURL;

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

/**
 * Controls the reparenting of tabs when an app restart is required due to configuration changes.
 * Tabs are preserved when the app is restarted under the following conditions:
 * - The current app theme changes.
 * - The layout switches between tablet/phone.
 * - (Keep this list up to date by adding future conditions here)
 */
public class TabReparentingController {
    /** Provides data to {@link TabReparentingController} facilitate reparenting tabs. */
    public interface Delegate {
        /** Gets a {@link TabModelSelector} which is used to add the tab. */
        TabModelSelector getTabModelSelector();

        /**
         * @return Whether the given Url is an NTP url, exists solely to support unit testing.
         */
        boolean isNtpUrl(GURL url);
    }

    private static final String TAG =
            "org.chromium.chrome.browser.app.tab_activity_glue.TabReparentingController";

    private final Delegate mDelegate;
    private final AsyncTabParamsManager mAsyncTabParamsManager;

    /** Constructs a {@link TabReparentingController} with the given delegate. */
    public TabReparentingController(
            @NonNull Delegate delegate, @NonNull AsyncTabParamsManager asyncTabParamsManager) {
        mDelegate = delegate;
        mAsyncTabParamsManager = asyncTabParamsManager;
    }

    /**
     * Prepares the tabs for reparenting by, 1. Informing the {@link TabModelSelector} that
     * reparenting is in progress. 2. Detaching each tab from the models. 3. For each tab that's
     * detached, it's added to {@link AsyncTabParamsManager}. These tabs are held in memory until an
     * application restart.
     *
     * <p>On app restart, the tabs from AsyncTabParamsManager are reattached/enabled in {@link
     * ChromeTabCreator}.
     */
    public void prepareTabsForReparenting() {
        // TODO(crbug.com/40124038): Make tab models detachable.
        TabModelSelector selector = mDelegate.getTabModelSelector();

        // Close tabs pending closure before saving params.
        selector.getModel(false).commitAllTabClosures();
        selector.getModel(true).commitAllTabClosures();

        // Aggregate all the tabs.
        List<Tab> tabs = new ArrayList<>(selector.getTotalTabCount());
        populateComprehensiveTabsFromModel(selector.getModel(false), tabs);
        populateComprehensiveTabsFromModel(selector.getModel(true), tabs);

        // Save all the tabs in memory to be retrieved after restart.
        mDelegate.getTabModelSelector().enterReparentingMode();
        int tabsAwaitingReparenting = 0;
        int tabsStillLoading = 0;
        for (int i = 0; i < tabs.size(); i++) {
            Tab tab = tabs.get(i);
            if (tab.isLoading()) {
                tab.stopLoading();
                tab.getWebContents().getNavigationController().setNeedsReload();
                tabsStillLoading++;
            }

            // The current tab has already been detached/stored and is waiting for android to
            // recreate the activity.
            if (mAsyncTabParamsManager.hasParamsForTabId(tab.getId())) {
                tabsAwaitingReparenting++;
                continue;
            }
            // Intentionally skip new tab pages and allow them to reload and restore scroll
            // state themselves.
            if (mDelegate.isNtpUrl(tab.getUrl())) continue;

            TabReparentingParams params = new TabReparentingParams(tab, null);
            mAsyncTabParamsManager.add(tab.getId(), params);
            ReparentingTask.from(tab).detach();

            tabsAwaitingReparenting++;
        }

        // TODO(crbug.com/40793204): Remove logging once root cause of bug is identified &
        //  fixed.
        Log.i(
                TAG,
                "#prepareTabsForReparenting, num tabs awaiting reparenting: "
                        + tabsAwaitingReparenting
                        + ", num tabs still loading: "
                        + tabsStillLoading);
    }

    protected static void populateComprehensiveTabsFromModel(TabModel model, List<Tab> outputTabs) {
        TabList tabList = model.getComprehensiveModel();
        for (int i = 0; i < tabList.getCount(); i++) {
            outputTabs.add(tabList.getTabAt(i));
        }
    }
}