chromium/chrome/android/javatests/src/org/chromium/chrome/browser/multiwindow/MultiWindowUtilsTest.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.multiwindow;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;

import static org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper.createSecondChromeTabbedActivity;

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

import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ThreadUtils;
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.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.ChromeTabbedActivity2;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.AutomotiveContextWrapperTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.ui.test.util.UiDisableIf;

import java.util.concurrent.TimeoutException;

/** Class for testing MultiWindowUtils. */
@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 MultiWindowUtilsTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Rule
    public AutomotiveContextWrapperTestRule mAutomotiveContextWrapperTestRule =
            new AutomotiveContextWrapperTestRule();

    @Mock private MultiWindowUtils mMultiWindowUtils;

    @Before
    public void setUp() throws InterruptedException {
        mActivityTestRule.startMainActivityOnBlankPage();
        mMultiWindowUtils = Mockito.spy(MultiWindowUtils.getInstance());
    }

    @After
    public void teardown() {
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(false);
    }

    /** Tests that ChromeTabbedActivity2 is used for intents when EXTRA_WINDOW_ID is set to 2. */
    @Test
    @SmallTest
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338976206
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentWithExtraWindowId() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        createSecondChromeTabbedActivity(activity1);

        Intent intent = activity1.getIntent();
        intent.putExtra(IntentHandler.EXTRA_WINDOW_ID, 2);

        Assert.assertEquals(
                "ChromeTabbedActivity2 should be used when EXTRA_WINDOW_ID is set to 2.",
                ChromeTabbedActivity2.class,
                MultiWindowUtils.getInstance().getTabbedActivityForIntent(intent, activity1));
    }

    /**
     * Tests that if two ChromeTabbedActivities are running the one that was resumed most recently
     * is used as the class name for new intents.
     */
    @Test
    @SmallTest
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338976206
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentLastResumedActivity() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        final ChromeTabbedActivity2 activity2 = createSecondChromeTabbedActivity(activity1);

        Assert.assertFalse(
                "ChromeTabbedActivity should not be resumed",
                ApplicationStatus.getStateForActivity(activity1) == ActivityState.RESUMED);
        Assert.assertTrue(
                "ChromeTabbedActivity2 should be resumed",
                ApplicationStatus.getStateForActivity(activity2) == ActivityState.RESUMED);

        // Wait for profile to be initialized.
        CriteriaHelper.pollUiThread(() -> activity2.getCurrentTabModel().getProfile() != null);

        // Open settings and wait for ChromeTabbedActivity2 to pause.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    activity2.onMenuOrKeyboardAction(R.id.preferences_id, true);
                });
        int expected = ActivityState.PAUSED;
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ApplicationStatus.getStateForActivity(activity2),
                            Matchers.is(expected));
                });

        Assert.assertEquals(
                "The most recently resumed ChromeTabbedActivity should be used for intents.",
                ChromeTabbedActivity2.class,
                MultiWindowUtils.getInstance()
                        .getTabbedActivityForIntent(activity1.getIntent(), activity1));
    }

    /**
     * Tests that if only ChromeTabbedActivity is running it is used as the class name for intents.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    @DisabledTest(message = "https://crbug.com/1417018")
    public void testTabbedActivityForIntentOnlyActivity1IsRunning() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        ChromeTabbedActivity2 activity2 = createSecondChromeTabbedActivity(activity1);
        activity2.finishAndRemoveTask();

        Assert.assertEquals(
                "ChromeTabbedActivity should be used for intents if ChromeTabbedActivity2 is "
                        + "not running.",
                ChromeTabbedActivity.class,
                MultiWindowUtils.getInstance()
                        .getTabbedActivityForIntent(activity1.getIntent(), activity1));
    }

    /**
     * Tests that if only ChromeTabbedActivity2 is running it is used as the class name for intents.
     */
    @Test
    @SmallTest
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338976206
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentOnlyActivity2IsRunning() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        createSecondChromeTabbedActivity(activity1);
        activity1.finishAndRemoveTask();

        Assert.assertEquals(
                "ChromeTabbedActivity2 should be used for intents if ChromeTabbedActivity is "
                        + "not running.",
                ChromeTabbedActivity2.class,
                MultiWindowUtils.getInstance()
                        .getTabbedActivityForIntent(activity1.getIntent(), activity1));
    }

    /**
     * Tests that if no ChromeTabbedActivities are running ChromeTabbedActivity is used as the
     * default for intents.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentNoActivitiesAlive() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        activity1.finishAndRemoveTask();

        Assert.assertEquals(
                "ChromeTabbedActivity should be used as the default for external intents.",
                ChromeTabbedActivity.class,
                MultiWindowUtils.getInstance()
                        .getTabbedActivityForIntent(activity1.getIntent(), activity1));
    }

    /** Tests that MultiWindowUtils properly tracks whether ChromeTabbedActivity2 is running. */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    @DisabledTest(message = "https://crbug.com/1417018")
    public void testTabbedActivity2TaskRunning() {
        ChromeTabbedActivity activity2 =
                createSecondChromeTabbedActivity(mActivityTestRule.getActivity());
        Assert.assertTrue(MultiWindowUtils.getInstance().getTabbedActivity2TaskRunning());

        activity2.finishAndRemoveTask();
        MultiWindowUtils.getInstance()
                .getTabbedActivityForIntent(
                        mActivityTestRule.getActivity().getIntent(),
                        mActivityTestRule.getActivity());
        Assert.assertFalse(MultiWindowUtils.getInstance().getTabbedActivity2TaskRunning());
    }

    /**
     * Tests that {@link MultiWindowUtils#areMultipleChromeInstancesRunning} behaves correctly in
     * the case the second instance is killed first.
     */
    @Test
    @SmallTest
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338976206
    @Feature("MultiWindow")
    public void testAreMultipleChromeInstancesRunningSecondInstanceKilledFirst()
            throws TimeoutException {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
        Assert.assertFalse(
                "Only a single instance should be running at the start.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity1StoppedCallback = new CallbackHelper();
        CallbackHelper activity1DestroyedCallback = new CallbackHelper();
        CallbackHelper activity1ResumedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity1StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.STOPPED:
                                activity1StoppedCallback.notifyCalled();
                                break;
                            case ActivityState.DESTROYED:
                                activity1DestroyedCallback.notifyCalled();
                                break;
                            case ActivityState.RESUMED:
                                activity1ResumedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ApplicationStatus.registerStateListenerForActivity(
                            activity1StateListener, activity1);
                });

        // Starting activity2 will stop activity1 as this is not truly multi-window mode.
        int activity1CallCount = activity1StoppedCallback.getCallCount();
        ChromeTabbedActivity activity2 = createSecondChromeTabbedActivity(activity1);
        activity1StoppedCallback.waitForCallback(activity1CallCount);
        Assert.assertTrue(
                "Both instances should be running now that the second has started.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity2DestroyedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity2StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.DESTROYED:
                                activity2DestroyedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ApplicationStatus.registerStateListenerForActivity(
                            activity2StateListener, activity2);
                });

        // activity1 may have been destroyed in the background. After destroying activity2 it is
        // necessary to make sure activity1 gets resumed.
        activity1CallCount = activity1ResumedCallback.getCallCount();
        activity2.finishAndRemoveTask();
        activity2DestroyedCallback.waitForOnly();
        activity1ResumedCallback.waitForCallback(activity1CallCount);
        Assert.assertFalse(
                "Only a single instance should be running after the second is killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        // activity1 may have been destroyed in the background and now it is in the foreground.
        // Wait on the next destroyed call rather than the first.
        activity1CallCount = activity1DestroyedCallback.getCallCount();
        activity1.finishAndRemoveTask();
        activity1DestroyedCallback.waitForCallback(activity1CallCount);
        Assert.assertFalse(
                "No instances should be running as all instances are killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));
    }

    /**
     * Tests that {@link MultiWindowUtils#areMultipleChromeInstancesRunning} behaves correctly in
     * the case the first instance is killed first.
     *
     * <p>TODO(crbug.com/40129069): This testcase is restricted to O+ as on Android N calling {@link
     * Activity#finishAndRemoveTask()} on the backgrounded activity1 will not cause it to be
     * DESTROYED it until after activity2 is PAUSED. On O+ activity1 will be DESTROYED immediately.
     * This test should be changed such that it works on N.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    @DisableIf.Build(
            sdk_is_less_than = Build.VERSION_CODES.O,
            message = "https://crbug.com/1077249")
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338976206
    public void testAreMultipleChromeInstancesRunningFirstInstanceKilledFirst()
            throws TimeoutException {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
        Assert.assertFalse(
                "Only a single instance should be running at the start.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity1StoppedCallback = new CallbackHelper();
        CallbackHelper activity1DestroyedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity1StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.STOPPED:
                                activity1StoppedCallback.notifyCalled();
                                break;
                            case ActivityState.DESTROYED:
                                activity1DestroyedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ApplicationStatus.registerStateListenerForActivity(
                            activity1StateListener, activity1);
                });

        // Starting activity2 will stop activity1 as this is not truly multi-window mode.
        // activity1 may be killed in the background, but since it is never foregrounded again
        // there should be only one call for both stopped and destroyed in this test.
        ChromeTabbedActivity activity2 = createSecondChromeTabbedActivity(activity1);
        activity1StoppedCallback.waitForOnly();
        Assert.assertTrue(
                "Both instances should be running now that the second has started.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity2DestroyedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity2StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.DESTROYED:
                                activity2DestroyedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ApplicationStatus.registerStateListenerForActivity(
                            activity2StateListener, activity2);
                });

        activity1.finishAndRemoveTask();
        activity1DestroyedCallback.waitForOnly();
        Assert.assertFalse(
                "Only a single instance should be running after the first is killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity2));

        // activity2 is always in the foreground so this should be the first time it is destroyed.
        activity2.finishAndRemoveTask();
        activity2DestroyedCallback.waitForOnly();
        Assert.assertFalse(
                "No instances should be running as all instances are killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity2));
    }

    /**
     * These tests check that MultiWindowUtils properly checks whether opening tabs in other windows
     * is supported.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testIsOpenInOtherWindowSupported_isNotInMultiWindowDisplayMode_returnsFalse() {
        assertFalse(
                doTestIsOpenInOtherWindowSupported(
                        /* isAutomotive= */ false,
                        /* isInMultiWindowMode= */ false,
                        /* isInMultiDisplayMode= */ false,
                        /* openInOtherWindowActivity= */ ChromeTabbedActivity.class));
    }

    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testIsOpenInOtherWindowSupported_isAutomotive_returnsFalse() {
        assertFalse(
                doTestIsOpenInOtherWindowSupported(
                        /* isAutomotive= */ true,
                        /* isInMultiWindowMode= */ true,
                        /* isInMultiDisplayMode= */ true,
                        /* openInOtherWindowActivity= */ ChromeTabbedActivity.class));
    }

    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testIsOpenInOtherWindowSupported_otherWindowActivityIsNull_returnsFalse() {
        assertFalse(
                doTestIsOpenInOtherWindowSupported(
                        /* isAutomotive= */ false,
                        /* isInMultiWindowMode= */ true,
                        /* isInMultiDisplayMode= */ true,
                        /* openInOtherWindowActivity= */ null));
    }

    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testIsOpenInOtherWindowSupported_otherWindowActivityIsNotNull_returnsTrue() {
        assertTrue(
                doTestIsOpenInOtherWindowSupported(
                        /* isAutomotive= */ false,
                        /* isInMultiWindowMode= */ true,
                        /* isInMultiDisplayMode= */ true,
                        /* openInOtherWindowActivity= */ ChromeTabbedActivity.class));
    }

    public boolean doTestIsOpenInOtherWindowSupported(
            boolean isAutomotive,
            boolean isInMultiWindowMode,
            boolean isInMultiDisplayMode,
            Class<? extends Activity> openInOtherWindowActivity) {
        mAutomotiveContextWrapperTestRule.setIsAutomotive(isAutomotive);

        doReturn(isInMultiWindowMode).when(mMultiWindowUtils).isInMultiWindowMode(any());
        doReturn(isInMultiDisplayMode).when(mMultiWindowUtils).isInMultiDisplayMode(any());
        doReturn(openInOtherWindowActivity)
                .when(mMultiWindowUtils)
                .getOpenInOtherWindowActivity(any());

        return mMultiWindowUtils.isOpenInOtherWindowSupported(mActivityTestRule.getActivity());
    }

    /**
     * These tests check that MultiWindowUtils properly checks whether Chrome can enter multi-window
     * mode.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testCanEnterMultiWindowMode_isAutomotive_returnsFalse() {
        assertFalse(
                doTestCanEnterMultiWindowMode(
                        /* isAutomotive= */ true,
                        /* aospMultiWindowModeSupported= */ false,
                        /* customMultiWindowModeSupported= */ false));
    }

    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testCanEnterMultiWindowMode_noSupport_returnsFalse() {
        assertFalse(
                doTestCanEnterMultiWindowMode(
                        /* isAutomotive= */ false,
                        /* aospMultiWindowModeSupported= */ false,
                        /* customMultiWindowModeSupported= */ false));
    }

    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testCanEnterMultiWindowMode_aospMultiWindowModeSupported_returnsFalse() {
        assertTrue(
                doTestCanEnterMultiWindowMode(
                        /* isAutomotive= */ false,
                        /* aospMultiWindowModeSupported= */ true,
                        /* customMultiWindowModeSupported= */ false));
    }

    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testCanEnterMultiWindowMode_customMultiWindowModeSupported_returnsFalse() {
        assertTrue(
                doTestCanEnterMultiWindowMode(
                        /* isAutomotive= */ false,
                        /* aospMultiWindowModeSupported= */ false,
                        /* customMultiWindowModeSupported= */ true));
    }

    public boolean doTestCanEnterMultiWindowMode(
            boolean isAutomotive,
            boolean aospMultiWindowModeSupported,
            boolean customMultiWindowModeSupported) {
        mAutomotiveContextWrapperTestRule.setIsAutomotive(isAutomotive);

        doReturn(aospMultiWindowModeSupported)
                .when(mMultiWindowUtils)
                .aospMultiWindowModeSupported();
        doReturn(customMultiWindowModeSupported)
                .when(mMultiWindowUtils)
                .customMultiWindowModeSupported();

        return mMultiWindowUtils.canEnterMultiWindowMode(mActivityTestRule.getActivity());
    }
}