chromium/chrome/android/javatests/src/org/chromium/chrome/browser/dragdrop/DragAndDropLauncherActivityTest.java

// Copyright 2023 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.dragdrop;

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

import androidx.annotation.RequiresApi;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.Stage;

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.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.ApplicationTestUtils;
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.DoNotBatch;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.Matchers;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.base.test.util.UserActionTester;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.ui.dragdrop.DragDropMetricUtils.DragDropType;
import org.chromium.ui.dragdrop.DragDropMetricUtils.UrlIntentSource;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.util.concurrent.ExecutionException;

/** Tests for {@link DragAndDropLauncherActivity}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DoNotBatch(reason = "This class tests activity start behavior and thus cannot be batched.")
@MinAndroidSdkLevel(VERSION_CODES.S)
@RequiresApi(VERSION_CODES.S)
public class DragAndDropLauncherActivityTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private Context mContext;
    private String mLinkUrl;
    private final CallbackHelper mTabAddedCallback = new CallbackHelper();
    private UserActionTester mActionTester;

    @Before
    public void setUp() {
        mActivityTestRule.startMainActivityOnBlankPage();
        mContext = ContextUtils.getApplicationContext();
        mLinkUrl = JUnitTestGURLs.EXAMPLE_URL.getSpec();
        mActionTester = new UserActionTester();
    }

    @After
    public void tearDown() {
        mActionTester.tearDown();
    }

    /**
     * Tests that a dragged link intent is launched in a new Chrome window from
     * DragAndDropLauncherActivity.
     */
    @Test
    @LargeTest
    public void testDraggedLink_newWindow() throws Exception {
        Intent intent = createLinkDragDropIntent(mLinkUrl, MultiWindowUtils.INVALID_INSTANCE_ID);
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DragDrop.Tab.Type", DragDropType.LINK_TO_NEW_INSTANCE);
        ChromeTabbedActivity lastAccessedActivity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> mContext.startActivity(intent));

        // Verify that a new Chrome instance is created.
        Assert.assertEquals(
                "Number of Chrome instances should be correct.",
                2,
                MultiWindowUtils.getInstanceCount());

        CriteriaHelper.pollUiThread(
                () -> {
                    Tab activityTab = lastAccessedActivity.getActivityTab();
                    Criteria.checkThat(
                            "Activity tab should be non-null.",
                            activityTab,
                            Matchers.notNullValue());
                });

        // Verify that the link is opened in the activity tab of the new Chrome instance.
        Tab activityTab = ThreadUtils.runOnUiThreadBlocking(lastAccessedActivity::getActivityTab);
        Assert.assertEquals(
                "Activity tab URL should match the dragged link URL.",
                new GURL(mLinkUrl).getSpec(),
                ChromeTabUtils.getUrlOnUiThread(activityTab).getSpec());
        Assert.assertTrue(
                "User action should be logged.",
                mActionTester
                        .getActions()
                        .contains(DragAndDropLauncherActivity.LAUNCHED_FROM_LINK_USER_ACTION));
        // Verify metric is recorded.
        histogramExpectation.assertExpected();

        lastAccessedActivity.finish();
    }

    /**
     * Tests that a dragged link intent is launched in a new tab on the Chrome window with the
     * specified windowId from DragAndDropLauncherActivity when the max number of Chrome instances
     * is open.
     */
    @Test
    @LargeTest
    public void testDraggedLink_existingWindow_maxInstances() throws Exception {
        // Simulate creation of max Chrome instances (= 2 for the purpose of testing) and establish
        // the last accessed instance. Actual max # of instances will not be created as this would
        // cause a significant overhead for testing this scenario where a link is opened in an
        // existing instance.
        Intent intent = createLinkDragDropIntent(mLinkUrl, MultiWindowUtils.INVALID_INSTANCE_ID);
        ChromeTabbedActivity lastAccessedActivity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> mContext.startActivity(intent));
        MultiWindowUtils.setMaxInstancesForTesting(2);
        int lastAccessedInstanceId =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> MultiWindowUtils.getInstanceIdForLinkIntent(lastAccessedActivity));
        addTabModelSelectorObserver(lastAccessedActivity);

        // Simulate an attempt to open a new window from a dragged link intent when max instances
        // are open. Do this by setting the EXTRA_WINDOW_ID extra on the intent that is reflective
        // of this state.
        Intent newIntent =
                createLinkDragDropIntent(JUnitTestGURLs.MAPS_URL.getSpec(), lastAccessedInstanceId);
        mContext.startActivity(newIntent);
        mTabAddedCallback.waitForCallback(0);

        // Verify that no new Chrome instance is created.
        Assert.assertEquals(
                "Number of Chrome instances should be correct.",
                2,
                MultiWindowUtils.getInstanceCount());

        // Verify that the link is opened in the activity tab of the last accessed Chrome instance.
        CriteriaHelper.pollUiThread(
                () -> {
                    Tab activityTab = lastAccessedActivity.getActivityTab();
                    Criteria.checkThat(
                            "Activity tab URL should match the dragged link URL.",
                            ChromeTabUtils.getUrlOnUiThread(activityTab).getSpec(),
                            Matchers.is(JUnitTestGURLs.MAPS_URL.getSpec()));
                });

        lastAccessedActivity.finish();
    }

    /**
     * Tests that a dragged link intent is dropped by DragAndDropLauncherActivity when the intent
     * creation timestamp is invalid.
     */
    @Test
    @LargeTest
    public void testDraggedLink_invalidIntentCreationTimestamp() throws Exception {
        DragAndDropLauncherActivity.setDropTimeoutMsForTesting(500L);
        Intent intent = createLinkDragDropIntent(mLinkUrl, MultiWindowUtils.INVALID_INSTANCE_ID);
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newBuilder().expectNoRecords("Android.DragDrop.Tab.Type").build();
        Thread.sleep(DragAndDropLauncherActivity.getDropTimeoutMs() + 1);
        mContext.startActivity(intent);
        // Verify that no new Chrome instance is created.
        Assert.assertEquals(
                "Number of instances should be correct.", 1, MultiWindowUtils.getInstanceCount());
        // Verify metric is not recorded.
        histogramExpectation.assertExpected();
    }

    /**
     * Tests that a dragged tab intent is launched by DragAndDropLauncherActivity in a new Chrome
     * window with successful tab reparenting.
     */
    @Test
    @LargeTest
    @EnableFeatures(ChromeFeatureList.DRAG_DROP_TAB_TEARING)
    public void testDraggedTab_newWindow() throws Exception {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        ChromeTabbedActivity.HISTOGRAM_DRAGGED_TAB_OPENED_NEW_WINDOW, true);
        var sourceActivity = mActivityTestRule.getActivity();

        // Open a new tab in the current activity, that will be used as the dragged tab.
        ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(), sourceActivity);

        var draggedTab = ThreadUtils.runOnUiThreadBlocking(sourceActivity::getActivityTab);
        var initialTabCountInSourceActivity =
                sourceActivity.getTabModelSelector().getTotalTabCount();

        // Simulate a tab drag/drop event to launch an intent in a new Chrome instance.
        Intent intent = createTabDragDropIntent(draggedTab);
        ChromeTabbedActivity newActivity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> mContext.startActivity(intent));

        // Verify that a new Chrome instance is created.
        Assert.assertEquals(
                "Number of Chrome instances should be correct.",
                2,
                MultiWindowUtils.getInstanceCount());

        CriteriaHelper.pollUiThread(
                () -> {
                    Tab activityTab = newActivity.getActivityTab();
                    Criteria.checkThat(
                            "Activity tab should be non-null.",
                            activityTab,
                            Matchers.notNullValue());
                    Criteria.checkThat(
                            "Tab should be moved from the source window.",
                            sourceActivity.getTabModelSelector().getTotalTabCount(),
                            Matchers.is(initialTabCountInSourceActivity - 1));
                });

        // Verify that the dragged tab is reparented in the new instance.
        int tabCountInNewActivity =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> newActivity.getTabModelSelector().getTotalTabCount());
        Assert.assertEquals(
                "New window should have only the dragged tab.", 1, tabCountInNewActivity);
        Tab newActivityTab = ThreadUtils.runOnUiThreadBlocking(newActivity::getActivityTab);
        Assert.assertEquals(
                "New activity tab should be the same as the dragged tab.",
                draggedTab,
                newActivityTab);

        // Verify metrics are recorded.
        Assert.assertTrue(
                "User action should be logged.",
                mActionTester
                        .getActions()
                        .contains(DragAndDropLauncherActivity.LAUNCHED_FROM_TAB_USER_ACTION));
        histogramExpectation.assertExpected();

        newActivity.finish();
    }

    private void addTabModelSelectorObserver(ChromeTabbedActivity activity) {
        TabModelSelector tabModelSelector = activity.getTabModelSelector();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    TabModelSelectorObserver tabModelSelectorObserver =
                            new TabModelSelectorObserver() {
                                @Override
                                public void onNewTabCreated(
                                        Tab tab, @TabCreationState int creationState) {
                                    mTabAddedCallback.notifyCalled();
                                }
                            };
                    tabModelSelector.addObserver(tabModelSelectorObserver);
                });
    }

    private Intent createLinkDragDropIntent(String linkUrl, Integer windowId)
            throws ExecutionException {
        return ThreadUtils.runOnUiThreadBlocking(
                () ->
                        DragAndDropLauncherActivity.getLinkLauncherIntent(
                                mContext, linkUrl, windowId, UrlIntentSource.LINK));
    }

    private Intent createTabDragDropIntent(Tab tab) throws ExecutionException {
        return ThreadUtils.runOnUiThreadBlocking(
                () ->
                        DragAndDropLauncherActivity.getTabIntent(
                                mContext, tab, MultiWindowUtils.INVALID_INSTANCE_ID));
    }
}