chromium/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabModelMergingTest.java

// Copyright 2016 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 static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build.VERSION_CODES;

import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.ThreadUtils;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.ChromeTabbedActivity2;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.layouts.LayoutTestUtils;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.multiwindow.MultiInstanceManager;
import org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabPersistentStoreTest.MockTabPersistentStoreObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.test.util.UiDisableIf;
import org.chromium.ui.test.util.UiRestriction;

import java.util.Collections;
import java.util.concurrent.TimeoutException;

/** Tests merging tab models for Android N+ multi-instance. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DisableIf.Build(sdk_is_greater_than = VERSION_CODES.S_V2) // https://crbug.com/1297370
public class TabModelMergingTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private static final String TEST_URL_0 = UrlUtils.encodeHtmlDataUri("<html>test_url_0.</html>");
    private static final String TEST_URL_1 = UrlUtils.encodeHtmlDataUri("<html>test_url_1.</html>");
    private static final String TEST_URL_2 = UrlUtils.encodeHtmlDataUri("<html>test_url_2.</html>");
    private static final String TEST_URL_3 = UrlUtils.encodeHtmlDataUri("<html>test_url_3.</html>");
    private static final String TEST_URL_4 = UrlUtils.encodeHtmlDataUri("<html>test_url_4.</html>");
    private static final String TEST_URL_5 = UrlUtils.encodeHtmlDataUri("<html>test_url_5.</html>");
    private static final String TEST_URL_6 = UrlUtils.encodeHtmlDataUri("<html>test_url_6.</html>");
    private static final String TEST_URL_7 = UrlUtils.encodeHtmlDataUri("<html>test_url_7.</html>");

    private ChromeTabbedActivity mActivity1;
    private ChromeTabbedActivity mActivity2;
    private int mActivity1State;
    private int mActivity2State;
    private String[] mMergeIntoActivity1ExpectedTabs;
    private String[] mMergeIntoActivity2ExpectedTabs;

    CallbackHelper mNewCTA2CallbackHelper = new CallbackHelper();
    private ChromeTabbedActivity2 mNewCTA2;

    @Before
    public void setUp() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        // Make sure file migrations don't run as they are unnecessary since app data was cleared.
        SharedPreferencesManager prefs = ChromeSharedPreferences.getInstance();
        prefs.writeBoolean(ChromePreferenceKeys.TABMODEL_HAS_RUN_FILE_MIGRATION, true);
        prefs.writeBoolean(
                ChromePreferenceKeys.TABMODEL_HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION, true);

        // Some of the logic for when to trigger a merge depends on whether the activity is in
        // multi-window mode. Set isInMultiWindowMode to true to avoid merging unexpectedly.
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);

        // Initialize activities.
        mActivity1 = mActivityTestRule.getActivity();
        // Start multi-instance mode so that ChromeTabbedActivity's check for whether the activity
        // is started up correctly doesn't fail.
        MultiInstanceManager.onMultiInstanceModeStarted();
        mActivity2 =
                MultiWindowTestHelper.createSecondChromeTabbedActivity(
                        mActivity1, new LoadUrlParams(TEST_URL_7));
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivity2.areTabModelsInitialized(), Matchers.is(true));
                    Criteria.checkThat(
                            mActivity2.getTabModelSelector().isTabStateInitialized(),
                            Matchers.is(true));
                });

        // Create a few tabs in each activity.
        createTabsOnUiThread();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Initialize activity states and register for state change events.
                    mActivity1State = ApplicationStatus.getStateForActivity(mActivity1);
                    mActivity2State = ApplicationStatus.getStateForActivity(mActivity2);
                    ApplicationStatus.registerStateListenerForAllActivities(
                            new ActivityStateListener() {
                                @Override
                                public void onActivityStateChange(Activity activity, int newState) {
                                    if (activity.equals(mActivity1)) {
                                        mActivity1State = newState;
                                    } else if (activity.equals(mActivity2)) {
                                        mActivity2State = newState;
                                    } else if (activity instanceof ChromeTabbedActivity2
                                            && newState == ActivityState.CREATED) {
                                        mNewCTA2 = (ChromeTabbedActivity2) activity;
                                        mNewCTA2CallbackHelper.notifyCalled();
                                    }
                                }
                            });
                });
    }

    /**
     * Creates new tabs in both ChromeTabbedActivity and ChromeTabbedActivity2 and asserts that each
     * has the expected number of tabs.
     */
    private void createTabsOnUiThread() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Create normal tabs.
                    mActivity1
                            .getTabCreator(false)
                            .createNewTab(
                                    new LoadUrlParams(TEST_URL_0),
                                    TabLaunchType.FROM_CHROME_UI,
                                    null);
                    mActivity1
                            .getTabCreator(false)
                            .createNewTab(
                                    new LoadUrlParams(TEST_URL_1),
                                    TabLaunchType.FROM_CHROME_UI,
                                    null);
                    mActivity1
                            .getTabCreator(false)
                            .createNewTab(
                                    new LoadUrlParams(TEST_URL_2),
                                    TabLaunchType.FROM_CHROME_UI,
                                    null);
                    mActivity2
                            .getTabCreator(false)
                            .createNewTab(
                                    new LoadUrlParams(TEST_URL_3),
                                    TabLaunchType.FROM_CHROME_UI,
                                    null);
                    mActivity2
                            .getTabCreator(false)
                            .createNewTab(
                                    new LoadUrlParams(TEST_URL_4),
                                    TabLaunchType.FROM_CHROME_UI,
                                    null);

                    mActivity1.saveState();
                    mActivity2.saveState();
                });

        // ChromeTabbedActivity should have four normal tabs, the one it started with and the three
        // just created.
        Assert.assertEquals(
                "Wrong number of tabs in ChromeTabbedActivity",
                4,
                mActivity1.getTabModelSelector().getModel(false).getCount());

        // ChromeTabbedActivity2 should have three normal tabs, the one it started with and the two
        // just created.
        Assert.assertEquals(
                "Wrong number of tabs in ChromeTabbedActivity2",
                3,
                mActivity2.getTabModelSelector().getModel(false).getCount());

        // Construct expected tabs.
        mMergeIntoActivity1ExpectedTabs = new String[7];
        mMergeIntoActivity2ExpectedTabs = new String[7];
        for (int i = 0; i < 4; i++) {
            mMergeIntoActivity1ExpectedTabs[i] =
                    ChromeTabUtils.getUrlStringOnUiThread(
                            mActivity1.getTabModelSelector().getModel(false).getTabAt(i));
            mMergeIntoActivity2ExpectedTabs[i + 3] =
                    ChromeTabUtils.getUrlStringOnUiThread(
                            mActivity1.getTabModelSelector().getModel(false).getTabAt(i));
        }
        for (int i = 0; i < 3; i++) {
            mMergeIntoActivity2ExpectedTabs[i] =
                    ChromeTabUtils.getUrlStringOnUiThread(
                            mActivity2.getTabModelSelector().getModel(false).getTabAt(i));
            mMergeIntoActivity1ExpectedTabs[i + 4] =
                    ChromeTabUtils.getUrlStringOnUiThread(
                            mActivity2.getTabModelSelector().getModel(false).getTabAt(i));
        }
    }

    private void mergeTabsAndAssert(
            final ChromeTabbedActivity activity, final String[] expectedTabUrls) {
        String selectedTabUrl =
                ChromeTabUtils.getUrlStringOnUiThread(
                        activity.getTabModelSelector().getCurrentTab());
        mergeTabsAndAssert(activity, expectedTabUrls, expectedTabUrls.length, selectedTabUrl);
    }

    /**
     * Merges tabs into the provided activity and asserts that the tab model looks as expected.
     *
     * @param activity The activity to merge into.
     * @param expectedTabUrls The expected ordering of normal tab URLs after the merge.
     * @param expectedNumberOfTabs The expected number of tabs after the merge.
     * @param expectedSelectedTabUrl The expected URL of the selected tab after the merge.
     */
    private void mergeTabsAndAssert(
            final ChromeTabbedActivity activity,
            final String[] expectedTabUrls,
            final int expectedNumberOfTabs,
            String expectedSelectedTabUrl) {
        // Merge tabs into the activity.
        ThreadUtils.runOnUiThreadBlocking(
                () -> activity.getMultiInstanceMangerForTesting().maybeMergeTabs());

        // Wait for all tabs to be merged into the activity.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Total tab count incorrect.",
                            activity.getTabModelSelector().getTotalTabCount(),
                            Matchers.is(expectedNumberOfTabs));
                });

        assertTabModelMatchesExpectations(activity, expectedSelectedTabUrl, expectedTabUrls);
    }

    /**
     * @param context The context to use when creating the intent.
     * @return An intent that can be used to start a new ChromeTabbedActivity.
     */
    private Intent createChromeTabbedActivityIntent(Context context) {
        Intent intent = new Intent();
        intent.setClass(context, ChromeTabbedActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return intent;
    }

    /**
     * Starts a new ChromeTabbedActivity and asserts that the tab model looks as expected.
     *
     * @param intent The intent to launch the new activity.
     * @param expectedSelectedTabUrl The URL of the tab that's expected to be selected.
     * @param expectedTabUrls The expected ordering of tab URLs after the merge.
     * @return The newly created ChromeTabbedActivity.
     */
    private ChromeTabbedActivity startNewChromeTabbedActivityAndAssert(
            Intent intent, String expectedSelectedTabUrl, String[] expectedTabUrls) {
        final ChromeTabbedActivity newActivity =
                (ChromeTabbedActivity)
                        InstrumentationRegistry.getInstrumentation().startActivitySync(intent);

        // Wait for the tab state to be initialized.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(newActivity.areTabModelsInitialized(), Matchers.is(true));
                    Criteria.checkThat(
                            newActivity.getTabModelSelector().isTabStateInitialized(),
                            Matchers.is(true));
                });

        assertTabModelMatchesExpectations(newActivity, expectedSelectedTabUrl, expectedTabUrls);

        return newActivity;
    }

    private void assertTabModelMatchesExpectations(
            final ChromeTabbedActivity activity,
            String expectedSelectedTabUrl,
            final String[] expectedTabUrls) {
        // Assert there are the correct number of tabs.
        Assert.assertEquals(
                "Wrong number of normal tabs",
                expectedTabUrls.length,
                activity.getTabModelSelector().getModel(false).getCount());

        // Assert that the correct tab is selected.
        Assert.assertEquals(
                "Wrong tab selected",
                expectedSelectedTabUrl,
                ChromeTabUtils.getUrlStringOnUiThread(
                        activity.getTabModelSelector().getCurrentTab()));

        // Assert that tabs are in the correct order.
        for (int i = 0; i < expectedTabUrls.length; i++) {
            Assert.assertEquals(
                    "Wrong tab at position " + i,
                    expectedTabUrls[i],
                    ChromeTabUtils.getUrlStringOnUiThread(
                            activity.getTabModelSelector().getModel(false).getTabAt(i)));
        }
    }

    /**
     * Wait until the activity is on an expected or not expected state.
     *
     * @param state The expected state or not one.
     * @param activity The activity whose state will be observed.
     * @param expected If true, wait until activity is on the {@code state}; otherwise, wait util
     *     activity is on any state other than {@code state}.
     * @throws TimeoutException
     */
    private void waitForActivityStateChange(
            @ActivityState int state, Activity activity, boolean expected) throws TimeoutException {
        // do nothing if already in expected state.
        CallbackHelper helper = new CallbackHelper();
        ApplicationStatus.ActivityStateListener listener =
                (act, newState) -> {
                    if (expected == (state == newState)) helper.notifyCalled();
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    int currentState = ApplicationStatus.getStateForActivity(activity);
                    if (expected == (state == currentState)) {
                        helper.notifyCalled();
                        return;
                    }
                    ApplicationStatus.registerStateListenerForActivity(listener, activity);
                });
        helper.waitForOnly();
        // listener was registered on UiThread. So it should be unregistered on UiThread.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ApplicationStatus.unregisterActivityStateListener(listener);
                });
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @DisabledTest(message = "https://crbug.com/1275082")
    public void testMergeIntoChromeTabbedActivity1() {
        mergeTabsAndAssert(mActivity1, mMergeIntoActivity1ExpectedTabs);
        mActivity1.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @DisabledTest(message = "https://crbug.com/1275082")
    public void testMergeIntoChromeTabbedActivity2() {
        mergeTabsAndAssert(mActivity2, mMergeIntoActivity2ExpectedTabs);
        mActivity2.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @DisabledTest(message = "https://crbug.com/1275082")
    public void testMergeOnColdStart() {
        String expectedSelectedUrl =
                ChromeTabUtils.getUrlStringOnUiThread(
                        mActivity1.getTabModelSelector().getCurrentTab());

        // Create an intent to launch a new ChromeTabbedActivity.
        Intent intent = createChromeTabbedActivityIntent(mActivity1);

        // Save state.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity1.saveState();
                    mActivity2.saveState();
                });

        // Destroy both activities.
        mActivity2.finishAndRemoveTask();
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "CTA2 should be destroyed",
                            mActivity2State,
                            Matchers.is(ActivityState.DESTROYED));
                });

        mActivity1.finishAndRemoveTask();
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "CTA should be destroyed",
                            mActivity1State,
                            Matchers.is(ActivityState.DESTROYED));
                });

        // Start the new ChromeTabbedActivity.
        final ChromeTabbedActivity newActivity =
                startNewChromeTabbedActivityAndAssert(
                        intent, expectedSelectedUrl, mMergeIntoActivity1ExpectedTabs);

        // Clean up.
        newActivity.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @DisabledTest(message = "https://crbug.com/1275082")
    public void testMergeOnColdStartFromChromeTabbedActivity2() throws Exception {
        String expectedSelectedUrl =
                ChromeTabUtils.getUrlStringOnUiThread(
                        mActivity2.getTabModelSelector().getCurrentTab());

        MockTabPersistentStoreObserver mockObserver = new MockTabPersistentStoreObserver();
        TabModelSelectorImpl tabModelSelector =
                (TabModelSelectorImpl) mActivity2.getTabModelSelector();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity2
                            .getTabModelOrchestratorSupplier()
                            .get()
                            .getTabPersistentStore()
                            .addObserver(mockObserver);
                });

        // Merge tabs into ChromeTabbedActivity2. Wait for the merge to finish, ensuring the
        // tab metadata file for ChromeTabbedActivity gets deleted before attempting to merge
        // on cold start.
        mergeTabsAndAssert(mActivity2, mMergeIntoActivity2ExpectedTabs);
        mockObserver.stateMergedCallback.waitForCallback(0, 1);

        // Create an intent to launch a new ChromeTabbedActivity.
        Intent intent = createChromeTabbedActivityIntent(mActivity2);

        // Destroy ChromeTabbedActivity2. ChromeTabbedActivity should have been destroyed during the
        // merge.
        mActivity2.finishAndRemoveTask();
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "CTA should be destroyed",
                            mActivity1State,
                            Matchers.is(ActivityState.DESTROYED));
                    Criteria.checkThat(
                            "CTA2 should be destroyed",
                            mActivity2State,
                            Matchers.is(ActivityState.DESTROYED));
                });

        // Start the new ChromeTabbedActivity.
        final ChromeTabbedActivity newActivity =
                startNewChromeTabbedActivityAndAssert(
                        intent, expectedSelectedUrl, mMergeIntoActivity2ExpectedTabs);

        // Clean up.
        newActivity.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @DisabledTest(message = "https://crbug.com/1417018")
    public void testMergeOnColdStartIntoChromeTabbedActivity2() throws TimeoutException {
        String CTA2ClassName = mActivity2.getClass().getName();
        String CTA2PackageName = mActivity2.getPackageName();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity1.saveState();
                    mActivity2.saveState();
                });

        // Destroy both activities without removing tasks.
        mActivity1.finish();
        mActivity2.finish();

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "CTA should be destroyed",
                            mActivity1State,
                            Matchers.is(ActivityState.DESTROYED));
                    Criteria.checkThat(
                            "CTA2 should be destroyed",
                            mActivity2State,
                            Matchers.is(ActivityState.DESTROYED));
                });

        // Send a main intent to restart ChromeTabbedActivity2.
        Intent CTA2MainIntent = new Intent(Intent.ACTION_MAIN);
        CTA2MainIntent.setClassName(CTA2PackageName, CTA2ClassName);
        CTA2MainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        InstrumentationRegistry.getInstrumentation().startActivitySync(CTA2MainIntent);

        mNewCTA2CallbackHelper.waitForCallback(0);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mNewCTA2.areTabModelsInitialized(), Matchers.is(true));
                    Criteria.checkThat(
                            mNewCTA2.getTabModelSelector().isTabStateInitialized(),
                            Matchers.is(true));
                });

        // Check that a merge occurred.
        Assert.assertEquals(
                "Wrong number of tabs after restart.",
                mMergeIntoActivity2ExpectedTabs.length,
                mNewCTA2.getTabModelSelector().getModel(false).getCount());

        // TODO(twellington): When manually testing with "Don't keep activities" turned on in
        // developer settings, tabs are merged in the right order. In this test, however, the
        // order isn't quite as expected. Investigate replacing #finish() with something that
        // better simulates the activity being killed in the background due to OOM.

        // Clean up.
        mNewCTA2.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE, RESTRICTION_TYPE_NON_LOW_END_DEVICE})
    @DisabledTest(message = "https://crbug.com/1275082")
    public void testMergeWhileInTabSwitcher() {
        LayoutTestUtils.startShowingAndWaitForLayout(
                mActivity1.getLayoutManager(), LayoutType.TAB_SWITCHER, false);

        mergeTabsAndAssert(mActivity1, mMergeIntoActivity1ExpectedTabs);
        Assert.assertTrue("Overview mode should still be showing", mActivity1.isInOverviewMode());
        mActivity1.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @DisabledTest(message = "https://crbug.com/1275082")
    public void testMergeWithNoTabs() {
        // Enter the tab switcher before closing all tabs with grid tab switcher enabled, otherwise
        // the activity is killed and the test fails.
        LayoutTestUtils.startShowingAndWaitForLayout(
                mActivity1.getLayoutManager(), LayoutType.TAB_SWITCHER, false);

        // Close all tabs and wait for the callback.
        ChromeTabUtils.closeAllTabs(InstrumentationRegistry.getInstrumentation(), mActivity1);

        String[] expectedTabUrls = new String[3];
        for (int i = 0; i < 3; i++) {
            expectedTabUrls[i] = mMergeIntoActivity2ExpectedTabs[i];
        }

        // The first tab should be selected after the merge.
        mergeTabsAndAssert(mActivity1, expectedTabUrls, 3, expectedTabUrls[0]);
        mActivity1.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @Feature({"TabPersistentStore", "MultiWindow"})
    @DisabledTest(message = "https://crbug.com/1190012")
    public void testMergingIncognitoTabs() {
        // Incognito tabs must be fully loaded so that their tab states are written out.
        ChromeTabUtils.fullyLoadUrlInNewTab(
                InstrumentationRegistry.getInstrumentation(), mActivity1, TEST_URL_5, true);
        ChromeTabUtils.fullyLoadUrlInNewTab(
                InstrumentationRegistry.getInstrumentation(), mActivity2, TEST_URL_6, true);

        // Save state.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity1.saveState();
                    mActivity2.saveState();
                });

        Assert.assertEquals(
                "Wrong number of incognito tabs in ChromeTabbedActivity",
                1,
                mActivity1.getTabModelSelector().getModel(true).getCount());
        Assert.assertEquals(
                "Wrong number of tabs in ChromeTabbedActivity",
                5,
                mActivity1.getTabModelSelector().getTotalTabCount());
        Assert.assertEquals(
                "Wrong number of incognito tabs in ChromeTabbedActivity2",
                1,
                mActivity2.getTabModelSelector().getModel(true).getCount());
        Assert.assertEquals(
                "Wrong number of tabs in ChromeTabbedActivity2",
                4,
                mActivity2.getTabModelSelector().getTotalTabCount());

        String selectedUrl =
                ChromeTabUtils.getUrlStringOnUiThread(
                        mActivity1.getTabModelSelector().getCurrentTab());
        mergeTabsAndAssert(mActivity1, mMergeIntoActivity1ExpectedTabs, 9, selectedUrl);
    }

    @Test
    @LargeTest
    @DisableIf.Build(sdk_is_less_than = VERSION_CODES.P)
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338997261
    public void testMergeOnMultiDisplay_CTA_Resumed_CTA2_Not_Resumed() throws TimeoutException {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity1.saveState();
                    mActivity2.saveState();
                });
        MultiInstanceManager m1 = mActivity1.getMultiInstanceMangerForTesting();
        MultiInstanceManager m2 = mActivity2.getMultiInstanceMangerForTesting();

        // Ensure Activity 1 is resumed on the front.
        Intent intent = new Intent(mActivity1, mActivity1.getClass());
        intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        mActivity1.startActivity(intent);
        waitForActivityStateChange(ActivityState.RESUMED, mActivity2, false);
        waitForActivityStateChange(ActivityState.RESUMED, mActivity1, true);

        MultiInstanceManager.setTestDisplayIds(Collections.singletonList(0));
        m1.setCurrentDisplayIdForTesting(0);
        m2.setCurrentDisplayIdForTesting(1);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    m1.getDisplayListenerForTesting().onDisplayRemoved(1);
                    m2.getDisplayListenerForTesting().onDisplayRemoved(1);
                });

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Total tab count incorrect.",
                            mActivity1.getTabModelSelector().getTotalTabCount(),
                            Matchers.is(7));
                });
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "CTA should not be destroyed",
                            mActivity1State,
                            Matchers.not(ActivityState.DESTROYED));
                    Criteria.checkThat(
                            "CTA2 should be destroyed",
                            mActivity2State,
                            Matchers.is(ActivityState.DESTROYED));
                });
        mActivity1.finishAndRemoveTask();
        mActivity2.finishAndRemoveTask();
    }

    @Test
    @LargeTest
    @DisableIf.Build(sdk_is_less_than = VERSION_CODES.P)
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338997261
    public void testMergeOnMultiDisplay_OnDisplayChanged() throws TimeoutException {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity1.saveState();
                    mActivity2.saveState();
                });
        MultiInstanceManager m1 = mActivity1.getMultiInstanceMangerForTesting();
        MultiInstanceManager m2 = mActivity2.getMultiInstanceMangerForTesting();

        // Ensure Activity 1 is resumed on the front.
        Intent intent = new Intent(mActivity1, mActivity1.getClass());
        intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        mActivity1.startActivity(intent);
        waitForActivityStateChange(ActivityState.RESUMED, mActivity2, false);
        waitForActivityStateChange(ActivityState.RESUMED, mActivity1, true);

        MultiInstanceManager.setTestDisplayIds(Collections.singletonList(0));
        m1.setCurrentDisplayIdForTesting(0);
        m2.setCurrentDisplayIdForTesting(1);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    m1.getDisplayListenerForTesting().onDisplayChanged(1);
                    m2.getDisplayListenerForTesting().onDisplayChanged(1);
                });

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Total tab count incorrect.",
                            mActivity1.getTabModelSelector().getTotalTabCount(),
                            Matchers.is(7));
                });

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "CTA should not be destroyed",
                            mActivity1State,
                            Matchers.not(ActivityState.DESTROYED));
                    Criteria.checkThat(
                            "CTA2 should be destroyed",
                            mActivity2State,
                            Matchers.is(ActivityState.DESTROYED));
                });

        mActivity1.finishAndRemoveTask();
        mActivity2.finishAndRemoveTask();
    }
}