chromium/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityEphemeralTest.java

// Copyright 2024 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.customtabs;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import static org.chromium.chrome.browser.customtabs.CustomTabsIntentTestUtils.createMinimalCustomTabIntent;

import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;

import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;

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.chromium.base.ApplicationStatus;
import org.chromium.base.CallbackController;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.test.util.Batch;
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.Features;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthController;
import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthManager;
import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthSettingUtils;
import org.chromium.chrome.browser.lifecycle.StartStopWithNativeObserver;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.toolbar.ToolbarDataProvider;
import org.chromium.chrome.browser.ui.appmenu.AppMenuCoordinator;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.browser.ui.appmenu.AppMenuTestSupport;
import org.chromium.chrome.test.AutomotiveContextWrapperTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.net.test.EmbeddedTestServerRule;

import java.util.concurrent.TimeoutException;

/** Instrumentation tests for {@link CustomTabActivity} launched in ephemeral mode. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@EnableFeatures(ChromeFeatureList.CCT_EPHEMERAL_MODE)
@Batch(Batch.PER_CLASS)
public class CustomTabActivityEphemeralTest {
    private static final String HISTOGRAM_NAME = "CustomTabs.IncognitoCCTCallerId";

    private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
    private String mTestPage;

    @Rule
    public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();

    @Rule public EmbeddedTestServerRule mEmbeddedTestServerRule = new EmbeddedTestServerRule();

    @Rule
    public AutomotiveContextWrapperTestRule mAutomotiveRule =
            new AutomotiveContextWrapperTestRule();

    private CustomTabsConnection mConnectionToCleanup;

    @Before
    public void setUp() throws TimeoutException {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(true));
        mAutomotiveRule.setIsAutomotive(false);
        mTestPage = mEmbeddedTestServerRule.getServer().getURL(TEST_PAGE);
        LibraryLoader.getInstance().ensureInitialized();
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));

        // finish() is called on a non-UI thread by the testing harness. Must hide the menu
        // first, otherwise the UI is manipulated on a non-UI thread.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (mCustomTabActivityTestRule.getActivity() == null) return;
                    AppMenuCoordinator coordinator =
                            mCustomTabActivityTestRule.getAppMenuCoordinator();
                    // CCT doesn't always have a menu (ex. in the media viewer).
                    if (coordinator == null) return;
                    AppMenuHandler handler = coordinator.getAppMenuHandler();
                    if (handler != null) handler.hideAppMenu();
                });

        if (mConnectionToCleanup != null) {
            CustomTabsTestUtils.cleanupSessions(mConnectionToCleanup);
        }
    }

    private Intent createEphemeralCustomTabIntent() {
        return createMinimalCustomTabIntent(ApplicationProvider.getApplicationContext(), mTestPage)
                .putExtra(IntentHandler.EXTRA_ENABLE_EPHEMERAL_BROWSING, true);
    }

    private static int getThemeColor(CustomTabActivity activity) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> ChromeColors.getDefaultThemeColor(activity, false));
    }

    private static int getToolbarColor(CustomTabActivity activity) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    CustomTabToolbar toolbar = activity.findViewById(R.id.toolbar);
                    return toolbar.getBackground().getColor();
                });
    }

    private void setCanUseHiddenTabForSession(
            CustomTabsConnection connection, CustomTabsSessionToken token, boolean useHiddenTab) {
        assert mConnectionToCleanup == null || mConnectionToCleanup == connection;
        // Save the connection. In case the hidden tab is not consumed by the test, ensure that it
        // is properly cleaned up after the test.
        mConnectionToCleanup = connection;
        connection.mClientManager.setHideDomainForSession(token, true);
        connection.setCanUseHiddenTabForSession(token, useHiddenTab);
    }

    private CustomTabActivity launchEphemeralCustomTabActivity() {
        Intent intent = createEphemeralCustomTabIntent();
        return launchCustomTabActivity(intent);
    }

    private CustomTabActivity launchCustomTabActivity(Intent intent) {
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        return mCustomTabActivityTestRule.getActivity();
    }

    private void launchMenuItem(CustomTabActivity activity) {
        CustomTabsTestUtils.openAppMenuAndAssertMenuShown(activity);
    }

    private void launchAndTestMenuItemIsNotVisible(
            CustomTabActivity activity, int itemId, String failureMessage) {
        launchMenuItem(activity);
        assertNull(
                failureMessage,
                AppMenuTestSupport.getMenuItemPropertyModel(
                        mCustomTabActivityTestRule.getAppMenuCoordinator(), itemId));
    }

    private void launchAndTestMenuItemIsVisible(
            CustomTabActivity activity, int itemId, String failureMessage) {
        launchMenuItem(activity);
        assertNotNull(
                failureMessage,
                AppMenuTestSupport.getMenuItemPropertyModel(
                        mCustomTabActivityTestRule.getAppMenuCoordinator(), itemId));
    }

    @Test
    @MediumTest
    public void testEphemeralTabLaunchesInOTRProfileWhenEnabled() {
        CustomTabActivity activity = launchEphemeralCustomTabActivity();
        Profile profile = activity.getActivityTab().getProfile();
        assertTrue(profile.isOffTheRecord());
        assertFalse(profile.isIncognitoBranded());
        assertFalse(profile.isPrimaryOTRProfile());
    }

    @Test
    @MediumTest
    @Features.DisableFeatures(ChromeFeatureList.CCT_EPHEMERAL_MODE)
    public void testEphemeralTabLaunchesInRegularProfileWhenDisabled() {
        CustomTabActivity activity = launchEphemeralCustomTabActivity();
        Profile profile = activity.getActivityTab().getProfile();
        assertFalse(profile.isOffTheRecord());
        assertFalse(profile.isIncognitoBranded());
        assertFalse(profile.isPrimaryOTRProfile());
    }

    @Test
    @MediumTest
    public void testToolbarDoesNotHaveIncognitoLogo() {
        launchEphemeralCustomTabActivity();
        onView(withId(R.id.incognito_cct_logo_image_view)).check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    public void testToolbarHasOffTheRecordProfile() {
        CustomTabActivity activity = launchEphemeralCustomTabActivity();

        CustomTabToolbar customTabToolbar = activity.findViewById(R.id.toolbar);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ToolbarDataProvider toolbarDataProvider =
                            customTabToolbar.getToolbarDataProvider();
                    assertTrue(toolbarDataProvider.isOffTheRecord());
                    assertFalse(toolbarDataProvider.isIncognitoBranded());
                });
    }

    @Test
    @MediumTest
    public void testToolbarHasDefaultThemeColor() {
        Intent intent = createEphemeralCustomTabIntent();
        CustomTabActivity activity = launchCustomTabActivity(intent);
        assertEquals(getThemeColor(activity), getToolbarColor(activity));
    }

    @Test
    @MediumTest
    public void testCanCustomizeToolbarColor() {
        Intent intent = createEphemeralCustomTabIntent();
        intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, Color.RED);
        CustomTabActivity activity = launchCustomTabActivity(intent);
        assertEquals(Color.RED, getToolbarColor(activity));
    }

    @Test
    @MediumTest
    public void testCloseAllIncognitoNotificationIsNotDisplayed() {
        // It may happen that some previous incognito notification from tabbed activity may be
        // already be lying around. So, we test the delta instead to be 0.
        Context context = ContextUtils.getApplicationContext();
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        int startCount = notificationManager.getActiveNotifications().length;

        // Launch Ephemeral CCT
        launchEphemeralCustomTabActivity();

        int endCount = notificationManager.getActiveNotifications().length;
        assertEquals(0, endCount - startCount);
    }

    @Test
    @MediumTest
    public void testHiddenTabCreationIsBlocked() throws Exception {
        // mayLaunchUrl should be blocked for ephemeral mode since it runs with always regular
        // profile. Need to update the test if the mayLaunchUrl is ever
        // allowed in OTR profiles. (crbug.com/1106757)
        Intent intent = createEphemeralCustomTabIntent();
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        final CustomTabsSessionToken token =
                CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        Assert.assertTrue(connection.newSession(token));
        // Passes the launch intent to the connection.
        setCanUseHiddenTabForSession(connection, token, true);
        Assert.assertFalse(
                connection.mayLaunchUrl(token, Uri.parse(mTestPage), intent.getExtras(), null));
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Hidden tab was created",
                            connection.getHiddenTabForTesting(),
                            Matchers.nullValue());
                });
        launchCustomTabActivity(intent);
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.APP_SPECIFIC_HISTORY)
    public void testHistoryMenuItemIsHidden() {
        Intent intent = createEphemeralCustomTabIntent();
        intent.putExtra(IntentHandler.EXTRA_LAUNCHED_FROM_PACKAGE, "com.foo.bar");
        CustomTabActivity activity = launchCustomTabActivity(intent);

        launchAndTestMenuItemIsNotVisible(
                activity, R.id.open_history_menu_id, "History item is visible");
    }

    @Test
    @MediumTest
    public void testDownloadTopIconIsHidden() {
        CustomTabActivity activity = launchEphemeralCustomTabActivity();

        launchAndTestMenuItemIsNotVisible(
                activity, R.id.offline_page_id, "Download icon is visible");
    }

    @Test
    @MediumTest
    public void testOpenInChromeIncognitoMenuItemIsVisible() {
        CustomTabActivity activity = launchEphemeralCustomTabActivity();

        launchAndTestMenuItemIsVisible(
                activity, R.id.open_in_browser_id, "Open in browser not visible");
        onView(withText(R.string.menu_open_in_incognito_chrome)).check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.INCOGNITO_REAUTHENTICATION_FOR_ANDROID)
    public void testIncognitoReauthPageNotShown() throws Exception {
        IncognitoReauthManager.setIsIncognitoReauthFeatureAvailableForTesting(true);
        IncognitoReauthSettingUtils.setIsDeviceScreenLockEnabledForTesting(true);

        Intent intent = createEphemeralCustomTabIntent();
        CustomTabActivity customTabActivity = launchCustomTabActivity(intent);
        CallbackHelper callbackHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    OneshotSupplier<IncognitoReauthController>
                            incognitoReauthControllerOneshotSupplier =
                                    customTabActivity
                                            .getRootUiCoordinatorForTesting()
                                            .getIncognitoReauthControllerSupplier();
                    CallbackController callbackController = new CallbackController();
                    incognitoReauthControllerOneshotSupplier.onAvailable(
                            callbackController.makeCancelable(
                                    incognitoReauthController -> {
                                        assertNotNull(incognitoReauthController);
                                        callbackHelper.notifyCalled();
                                    }));
                });
        callbackHelper.waitForCallback(0);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    UserPrefs.get(ProfileManager.getLastUsedRegularProfile())
                            .setBoolean(Pref.INCOGNITO_REAUTHENTICATION_FOR_ANDROID, true);
                    IncognitoReauthController incognitoReauthController =
                            customTabActivity
                                    .getRootUiCoordinatorForTesting()
                                    .getIncognitoReauthControllerSupplier()
                                    .get();

                    // Fake Chrome going background and coming back to foreground.
                    ApplicationStatus.TaskVisibilityListener visibilityListener =
                            (ApplicationStatus.TaskVisibilityListener) incognitoReauthController;
                    visibilityListener.onTaskVisibilityChanged(
                            customTabActivity.getTaskId(), false);

                    StartStopWithNativeObserver observer =
                            (StartStopWithNativeObserver) incognitoReauthController;
                    observer.onStartWithNative();

                    assertFalse(
                            "Re-auth screen should not be shown.",
                            incognitoReauthController.isReauthPageShowing());

                    UserPrefs.get(ProfileManager.getLastUsedRegularProfile())
                            .setBoolean(Pref.INCOGNITO_REAUTHENTICATION_FOR_ANDROID, false);
                });

        IncognitoReauthManager.setIsIncognitoReauthFeatureAvailableForTesting(false);
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.SEARCH_IN_CCT)
    public void testNonInteractiveOmnibox() {
        CustomTabActivity activity = launchEphemeralCustomTabActivity();
        var tab = activity.getActivityTab();
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage);

        var titleBar = activity.findViewById(R.id.title_url_container);
        Assert.assertFalse(titleBar.hasOnClickListeners());
    }

    @Test
    @MediumTest
    public void recordsHistogramEphemeral() {
        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        HISTOGRAM_NAME, IntentHandler.IncognitoCCTCallerId.EPHEMERAL_TAB);
        launchEphemeralCustomTabActivity();
        histogramWatcher.assertExpected();
    }
}