chromium/chrome/android/javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityTest.java

// Copyright 2018 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.browserservices;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.createSession;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.createTrustedWebActivityIntent;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.isTrustedWebActivity;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.spoofVerification;

import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;

import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.TrustedWebUtils;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import androidx.test.filters.MediumTest;

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

import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
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.DisabledTest;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Restriction;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.browser.app.metrics.LaunchCauseMetrics;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabBrowserControlsConstraintsHelper;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.browser.ThemeTestUtils;
import org.chromium.content_public.common.ContentSwitches;
import org.chromium.net.test.EmbeddedTestServerRule;
import org.chromium.ui.test.util.UiRestriction;

import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/**
 * Instrumentation tests for launching {@link
 * org.chromium.chrome.browser.customtabs.CustomTabActivity} in Trusted Web Activity Mode.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DoNotBatch(reason = "https://crbug.com/1454648")
public class TrustedWebActivityTest {
    // TODO(peconn): Add test for navigating away from the trusted origin.
    public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();
    public EmbeddedTestServerRule mEmbeddedTestServerRule = new EmbeddedTestServerRule();

    @Rule
    public RuleChain mRuleChain =
            RuleChain.emptyRuleChain()
                    .around(mCustomTabActivityTestRule)
                    .around(mEmbeddedTestServerRule);

    private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
    private static final String PACKAGE_NAME =
            ContextUtils.getApplicationContext().getPackageName();

    private String mTestPage;

    @Before
    public void setUp() {
        // Native needs to be initialized to start the test server.
        LibraryLoader.getInstance().ensureInitialized();

        mEmbeddedTestServerRule.setServerUsesHttps(true); // TWAs only work with HTTPS.
        mTestPage = mEmbeddedTestServerRule.getServer().getURL(TEST_PAGE);

        // Map non-localhost-URLs to localhost. Navigations to non-localhost URLs will throw a
        // certificate error.
        Uri mapToUri = Uri.parse(mEmbeddedTestServerRule.getServer().getURL("/"));
        CommandLine.getInstance()
                .appendSwitchWithValue(
                        ContentSwitches.HOST_RESOLVER_RULES, "MAP * " + mapToUri.getAuthority());
    }

    private void assertLaunchCauseMetrics(boolean launchedTWA) {
        assertEquals(
                launchedTWA ? 1 : 0,
                RecordHistogram.getHistogramValueCountForTesting(
                        LaunchCauseMetrics.LAUNCH_CAUSE_HISTOGRAM,
                        LaunchCauseMetrics.LaunchCause.TWA));
        assertEquals(
                launchedTWA ? 0 : 1,
                RecordHistogram.getHistogramValueCountForTesting(
                        LaunchCauseMetrics.LAUNCH_CAUSE_HISTOGRAM,
                        LaunchCauseMetrics.LaunchCause.CUSTOM_TAB));
    }

    @Test
    @MediumTest
    public void launchesTwa() throws TimeoutException {
        Intent intent = createTrustedWebActivityIntent(mTestPage);
        launchCustomTabActivity(intent);

        assertTrue(isTrustedWebActivity(mCustomTabActivityTestRule.getActivity()));
        assertLaunchCauseMetrics(true);
    }

    @Test
    @MediumTest
    public void doesntLaunchTwa_WithoutFlag() throws TimeoutException {
        Intent intent = createTrustedWebActivityIntent(mTestPage);
        intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false);
        launchCustomTabActivity(intent);

        assertFalse(isTrustedWebActivity(mCustomTabActivityTestRule.getActivity()));
        assertLaunchCauseMetrics(false);
    }

    @Test
    @MediumTest
    public void leavesTwa_VerificationFailure() throws TimeoutException {
        Intent intent = createTrustedWebActivityIntent(mTestPage);
        createSession(intent, PACKAGE_NAME);

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        assertFalse(isTrustedWebActivity(mCustomTabActivityTestRule.getActivity()));
        assertLaunchCauseMetrics(true);
    }

    /**
     * Test that if the page provides a theme-color and the toolbar color was specified in the
     * intent that the page theme-color is used for the status bar.
     */
    @Test
    @MediumTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
    // Customizing status bar color is disallowed for tablets.
    public void testStatusBarColorHasPageThemeColor() throws ExecutionException, TimeoutException {
        final String pageWithThemeColor =
                mEmbeddedTestServerRule
                        .getServer()
                        .getURL("/chrome/test/data/android/theme_color_test.html");

        Intent intent = createTrustedWebActivityIntent(pageWithThemeColor);
        intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, Color.GREEN);
        launchCustomTabActivity(intent);
        CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
        ThemeTestUtils.waitForThemeColor(activity, Color.RED);
        ThemeTestUtils.assertStatusBarColor(activity, Color.RED);
    }

    /**
     * Test that if the page does not provide a theme-color and the toolbar color was specified in
     * the intent that the toolbar color is used for the status bar.
     */
    @Test
    @MediumTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
    public void testStatusBarColorNoPageThemeColor() throws ExecutionException, TimeoutException {
        final String pageWithThemeColor =
                mEmbeddedTestServerRule
                        .getServer()
                        .getURL("/chrome/test/data/android/theme_color_test.html");
        final String pageWithoutThemeColor =
                mEmbeddedTestServerRule.getServer().getURL("/chrome/test/data/android/about.html");

        // Navigate to page with a theme color so that we can later wait for the status bar color to
        // change back to the intent color.
        Intent intent = createTrustedWebActivityIntent(pageWithThemeColor);
        intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, Color.GREEN);
        launchCustomTabActivity(intent);
        CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
        ThemeTestUtils.waitForThemeColor(activity, Color.RED);
        ThemeTestUtils.assertStatusBarColor(activity, Color.RED);

        mCustomTabActivityTestRule.loadUrl(pageWithoutThemeColor);
        // Use longer-than-default timeout to give page time to finish loading.
        ThemeTestUtils.waitForThemeColor(activity, Color.GREEN, /* timeoutMs= */ 10000);
        ThemeTestUtils.assertStatusBarColor(activity, Color.GREEN);
    }

    /**
     * Test that if the page has a certificate error, that the system default color is used
     * (regardless of whether the page provided a theme-color or a toolbar color was specified in
     * the intent).
     */
    @Test
    @MediumTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
    @DisabledTest(message = "b/352624584")
    public void testStatusBarColorCertificateError() throws ExecutionException, TimeoutException {
        final String pageWithThemeColor =
                mEmbeddedTestServerRule
                        .getServer()
                        .getURL("/chrome/test/data/android/theme_color_test.html");
        final String pageWithThemeColorCertError =
                "https://certificateerror.com/chrome/test/data/android/theme_color_test2.html";

        // Initially don't set certificate error so that we can later wait for the status bar color
        // to change back to the default color.
        Intent intent = createTrustedWebActivityIntent(pageWithThemeColor);
        intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, Color.GREEN);
        addTrustedOriginToIntent(intent, pageWithThemeColorCertError);
        launchCustomTabActivity(intent);
        CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
        ThemeTestUtils.waitForThemeColor(activity, Color.RED);
        ThemeTestUtils.assertStatusBarColor(activity, Color.RED);

        spoofVerification(PACKAGE_NAME, pageWithThemeColorCertError);
        ChromeTabUtils.loadUrlOnUiThread(activity.getActivityTab(), pageWithThemeColorCertError);

        int defaultColor =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> ThemeTestUtils.getDefaultThemeColor(activity.getActivityTab()));
        int expectedColor = defaultColor;
        // Use longer-than-default timeout to give page time to finish loading.
        ThemeTestUtils.waitForThemeColor(activity, defaultColor, /* timeoutMs= */ 10000);
        ThemeTestUtils.assertStatusBarColor(activity, expectedColor);
    }

    public void launchCustomTabActivity(Intent intent) throws TimeoutException {
        String url = intent.getData().toString();
        spoofVerification(PACKAGE_NAME, url);
        createSession(intent, PACKAGE_NAME);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
    }

    /**
     * Test that trusted web activities show the toolbar when the page has a certificate error (and
     * origin verification succeeds).
     */
    @Test
    @MediumTest
    public void testToolbarVisibleCertificateError() throws ExecutionException, TimeoutException {
        final String pageWithoutCertError =
                mEmbeddedTestServerRule.getServer().getURL("/chrome/test/data/android/about.html");
        final String pageWithCertError =
                "https://certificateerror.com/chrome/test/data/android/theme_color_test.html";

        // Initially don't set certificate error so that we can later wait for the toolbar to hide.
        Intent intent = createTrustedWebActivityIntent(pageWithoutCertError);
        addTrustedOriginToIntent(intent, pageWithCertError);
        launchCustomTabActivity(createTrustedWebActivityIntent(pageWithoutCertError));
        Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();
        assertEquals(BrowserControlsState.HIDDEN, getBrowserControlConstraints(tab));

        spoofVerification(PACKAGE_NAME, pageWithCertError);
        ChromeTabUtils.loadUrlOnUiThread(tab, pageWithCertError);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            getBrowserControlConstraints(tab),
                            Matchers.is(BrowserControlsState.SHOWN));
                },
                10000,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    public void addTrustedOriginToIntent(Intent intent, String trustedOrigin) {
        ArrayList<String> additionalTrustedOrigins = new ArrayList<>();
        additionalTrustedOrigins.add(trustedOrigin);
        intent.putExtra(
                TrustedWebActivityIntentBuilder.EXTRA_ADDITIONAL_TRUSTED_ORIGINS,
                additionalTrustedOrigins);
    }

    private @BrowserControlsState int getBrowserControlConstraints(Tab tab) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> TabBrowserControlsConstraintsHelper.getConstraints(tab));
    }
}