// Copyright 2015 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 org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_LOW_END_DEVICE;
import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.browser.customtabs.PrefetchOptions;
import androidx.test.core.app.ApplicationProvider;
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.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
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.EnableFeatures;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.browserservices.verification.ChromeOriginVerifier;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.prefetch.settings.PreloadPagesSettingsBridge;
import org.chromium.chrome.browser.prefetch.settings.PreloadPagesState;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer.OnEvaluateJavaScriptResultHelper;
import org.chromium.content_public.browser.test.util.WebContentsUtils;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.ServerCertificate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/** Tests for CustomTabsConnection. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class CustomTabsConnectionTest {
private CustomTabsConnection mCustomTabsConnection;
private static final String URL = "http://www.google.com";
private static final String URL2 = "https://www.android.com";
private static final String URL3 = "https://example.com";
private static final String INVALID_SCHEME_URL = "intent://www.google.com";
@Rule
public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();
@Before
public void setUp() {
LibraryLoader.getInstance().ensureInitialized();
mCustomTabsConnection = CustomTabsTestUtils.setUpConnection();
}
@After
public void tearDown() {
CustomTabsTestUtils.cleanupSessions(mCustomTabsConnection);
ThreadUtils.runOnUiThreadBlocking(
() -> WarmupManager.getInstance().destroySpareWebContents());
ThreadUtils.runOnUiThreadBlocking(() -> WarmupManager.getInstance().destroySpareTab());
}
/**
* Tests that we can create a new session. Registering with a null callback fails. Registering a
* session with an {@linkplain CustomTabsSessionToken#equals equal} session token will update
* the callback for the session.
*/
@Test
@SmallTest
public void testNewSession() {
Assert.assertFalse(mCustomTabsConnection.newSession(null));
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
// Request to update callback for the session.
Assert.assertTrue(mCustomTabsConnection.newSession(token));
}
/** Tests that we can create several sessions. */
@Test
@SmallTest
public void testSeveralSessions() {
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
CustomTabsSessionToken token2 = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token2));
}
/**
* Tests that {@link CustomTabsConnection#warmup(long)} succeeds and can be issued multiple
* times.
*/
@Test
@SmallTest
public void testCanWarmup() throws Exception {
CustomTabsTestUtils.warmUpAndWait();
CustomTabsTestUtils.warmUpAndWait();
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testCreateSpareRenderer() throws Exception {
CustomTabsTestUtils.warmUpAndWait();
ThreadUtils.runOnUiThreadBlocking(this::assertSpareWebContentsNotNullAndDestroy);
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_LOW_END_DEVICE)
public void testDoNotCreateSpareRendererOnLowEnd() throws Exception {
CustomTabsTestUtils.warmUpAndWait();
// On UI thread because:
// 1. takeSpareWebContents needs to be called from the UI thread.
// 2. warmup() is non-blocking and posts tasks to the UI thread, it ensures proper ordering.
ThreadUtils.runOnUiThreadBlocking(
() -> {
WarmupManager warmupManager = WarmupManager.getInstance();
Assert.assertFalse(warmupManager.hasSpareWebContents());
});
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testCreateSpareRendererCanBeRecreated() throws Exception {
CustomTabsTestUtils.warmUpAndWait();
ThreadUtils.runOnUiThreadBlocking(
() -> {
assertSpareWebContentsNotNullAndDestroy();
Assert.assertFalse(WarmupManager.getInstance().hasSpareWebContents());
});
CustomTabsTestUtils.warmUpAndWait();
ThreadUtils.runOnUiThreadBlocking(this::assertSpareWebContentsNotNullAndDestroy);
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testHiddenTabTakessSpareRenderer() throws Exception {
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
mCustomTabsConnection.newSession(token);
mCustomTabsConnection.setShouldSpeculateLoadOnCellularForSession(token, true);
assertWarmupAndMayLaunchUrl(token, URL, true);
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertFalse(WarmupManager.getInstance().hasSpareWebContents());
});
}
/*
* Tests that when the disconnection notification comes from a non-UI thread, Chrome doesn't
* crash. Non-regression test for crbug.com/623128.
*/
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testPrerenderAndDisconnectOnOtherThread() throws Exception {
final CustomTabsSessionToken token = assertWarmupAndMayLaunchUrl(null, URL, true);
final Thread otherThread = new Thread(() -> mCustomTabsConnection.cleanUpSession(token));
ThreadUtils.runOnUiThreadBlocking(otherThread::start);
// Should not crash, hence no assertions below.
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testMayLaunchUrlKeepsSpareRendererWithoutHiddenTab() throws Exception {
CustomTabsTestUtils.warmUpAndWait();
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setShouldSpeculateLoadOnCellularForSession(token, true);
mCustomTabsConnection.setCanUseHiddenTabForSession(token, false);
Assert.assertTrue(mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
ThreadUtils.runOnUiThreadBlocking(() -> assertSpareWebContentsNotNullAndDestroy());
}
@Test
@SmallTest
public void testMayLaunchUrlNullOrEmptyUrl() throws Exception {
assertWarmupAndMayLaunchUrl(null, null, true);
CustomTabsTestUtils.cleanupSessions(mCustomTabsConnection); // Resets throttling.
assertWarmupAndMayLaunchUrl(null, "", true);
}
/** Tests that a new mayLaunchUrl() call destroys the previous hidden tab. */
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
@CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
public void testOnlyOneHiddenTab() throws Exception {
Assert.assertTrue("Failed warmup()", mCustomTabsConnection.warmup(0));
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue("Failed newSession()", mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setCanUseHiddenTabForSession(token, true);
// First hidden tab, add an observer to check that it's destroyed.
Assert.assertTrue(
"Failed first mayLaunchUrl()",
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
final CallbackHelper tabDestroyedHelper = new CallbackHelper();
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertNotNull(
"Null speculation, first one",
mCustomTabsConnection.getSpeculationParamsForTesting());
Tab tab = mCustomTabsConnection.getSpeculationParamsForTesting().tab;
Assert.assertNotNull("No first tab", tab);
tab.addObserver(
new EmptyTabObserver() {
@Override
public void onDestroyed(Tab destroyedTab) {
tabDestroyedHelper.notifyCalled();
}
});
});
// New hidden tab.
mCustomTabsConnection.resetThrottling(Process.myUid());
Assert.assertTrue(
"Failed second mayLaunchUrl()",
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL2), null, null));
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertNotNull(
"Null speculation, new hidden tab",
mCustomTabsConnection.getSpeculationParamsForTesting());
Assert.assertNotNull(
"No second tab",
mCustomTabsConnection.getSpeculationParamsForTesting().tab);
Assert.assertEquals(
URL2, mCustomTabsConnection.getSpeculationParamsForTesting().url);
});
tabDestroyedHelper.waitForCallback("The first hidden tab should have been destroyed", 0);
// Clears the second hidden tab.
mCustomTabsConnection.resetThrottling(Process.myUid());
Assert.assertTrue(
"Failed cleanup mayLaunchUrl()",
mCustomTabsConnection.mayLaunchUrl(token, null, null, null));
}
/** Tests that if the renderer backing a hidden tab is killed, the speculation is canceled. */
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
@CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
public void testKillHiddenTabRenderer() throws Exception {
Assert.assertTrue("Failed warmup()", mCustomTabsConnection.warmup(0));
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue("Failed newSession()", mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setShouldSpeculateLoadOnCellularForSession(token, true);
Assert.assertTrue(
"Failed first mayLaunchUrl()",
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
final CallbackHelper tabDestroyedHelper = new CallbackHelper();
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertNotNull(
"Null speculation",
mCustomTabsConnection.getSpeculationParamsForTesting());
Tab speculationTab = mCustomTabsConnection.getSpeculationParamsForTesting().tab;
Assert.assertNotNull("Null speculation tab", speculationTab);
speculationTab.addObserver(
new EmptyTabObserver() {
@Override
public void onDestroyed(Tab tab) {
tabDestroyedHelper.notifyCalled();
}
});
WebContentsUtils.simulateRendererKilled(speculationTab.getWebContents());
});
tabDestroyedHelper.waitForCallback("The speculated tab was not destroyed", 0);
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testUnderstandsLowConfidenceMayLaunchUrl() {
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
List<Bundle> urls = new ArrayList<>();
Bundle urlBundle = new Bundle();
urlBundle.putParcelable(CustomTabsService.KEY_URL, Uri.parse(URL));
urls.add(urlBundle);
mCustomTabsConnection.mayLaunchUrl(token, null, null, urls);
ThreadUtils.runOnUiThreadBlocking(this::assertSpareWebContentsNotNullAndDestroy);
}
@Test
@SmallTest
public void testLowConfidenceMayLaunchUrlOnlyAcceptUris() throws Exception {
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
CustomTabsTestUtils.warmUpAndWait();
final List<Bundle> urlsAsString = new ArrayList<>();
Bundle urlStringBundle = new Bundle();
urlStringBundle.putString(CustomTabsService.KEY_URL, URL);
urlsAsString.add(urlStringBundle);
final List<Bundle> urlsAsUri = new ArrayList<>();
Bundle urlUriBundle = new Bundle();
urlUriBundle.putParcelable(CustomTabsService.KEY_URL, Uri.parse(URL));
urlsAsUri.add(urlUriBundle);
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertFalse(
mCustomTabsConnection.lowConfidenceMayLaunchUrl(urlsAsString));
Assert.assertTrue(mCustomTabsConnection.lowConfidenceMayLaunchUrl(urlsAsUri));
});
}
@Test
@SmallTest
public void testLowConfidenceMayLaunchUrlDoesntCrash() throws Exception {
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
CustomTabsTestUtils.warmUpAndWait();
final List<Bundle> invalidBundles = new ArrayList<>();
Bundle invalidBundle = new Bundle();
invalidBundle.putParcelable(CustomTabsService.KEY_URL, new Intent());
invalidBundles.add(invalidBundle);
ThreadUtils.runOnUiThreadBlocking(
() -> mCustomTabsConnection.lowConfidenceMayLaunchUrl(invalidBundles));
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testStillHighConfidenceMayLaunchUrlWithSeveralUrls() {
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setShouldSpeculateLoadOnCellularForSession(token, true);
List<Bundle> urls = new ArrayList<>();
Bundle urlBundle = new Bundle();
urlBundle.putParcelable(CustomTabsService.KEY_URL, Uri.parse(URL));
urls.add(urlBundle);
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, urls);
ThreadUtils.runOnUiThreadBlocking(
() ->
Assert.assertNull(
WarmupManager.getInstance().takeSpareWebContents(false, false)));
}
/**
* Tests that the tab used my mayLaunchUrl's high confidence mode uses a separate, empty, cookie
* jar from the normal navigations.
*/
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
@EnableFeatures(ChromeFeatureList.MAYLAUNCHURL_USES_SEPARATE_STORAGE_PARTITION)
@CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
public void testMayLaunchUrlUsesSeparateCookieJar() throws Exception {
// This test has 4 major parts.
// 1. Launch a custom tab and set a cookie.
// 2. Launch a hidden tab to the same site via mayLaunchUrl, set a different cookie, and
// confirm only that cookie is accessible.
// 3. Launch a second hidden tab, set a third cookie, and confirm only that cookie is
// accessible.
// 4. Launch a second custom tab and confirm that it sees the third cookie.
// 5. Launch a third custom tab and confirm that it sees the first cookie.
Context context = ApplicationProvider.getApplicationContext();
Assert.assertTrue("Failed warmup()", mCustomTabsConnection.warmup(0));
EmbeddedTestServer server =
EmbeddedTestServer.createAndStartHTTPSServer(context, ServerCertificate.CERT_OK);
final String url = server.getURL("/chrome/test/data/android/simple.html");
final OnEvaluateJavaScriptResultHelper JsHelper = new OnEvaluateJavaScriptResultHelper();
// Launch a custom tab and load the url.
Assert.assertTrue("Failed warmup()", mCustomTabsConnection.warmup(0));
Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
mCustomTabActivityTestRule.launchActivity(intent);
Tab normalTab = mCustomTabActivityTestRule.getActivity().getActivityTab();
// We can check if the page title is correct to know if the tab is done loading.
CriteriaHelper.pollUiThread(
() ->
Criteria.checkThat(
normalTab.getWebContents().getTitle(),
Matchers.is("Activity test page")));
// Set a cookie.
ThreadUtils.runOnUiThreadBlocking(
() -> {
JsHelper.evaluateJavaScriptForTests(
normalTab.getWebContents(),
"document.cookie = \"foo=bar; max-age = 1000 \";" + " document.cookie");
});
JsHelper.waitUntilHasValue(5, TimeUnit.SECONDS);
Assert.assertTrue("Failed to retrieve JavaScript evaluation results.", JsHelper.hasValue());
// Verify the tab has the expected cookie.
Assert.assertEquals("\"foo=bar\"", JsHelper.getJsonResultAndClear());
mCustomTabActivityTestRule.getActivity().finish();
// Launch the first hidden tab. This tab should use a separate storage partition and
// therefore shouldn't see the first cookie.
Assert.assertTrue("Failed warmup()", mCustomTabsConnection.warmup(0));
Intent intent2 = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent2);
Assert.assertTrue("Failed newSession()", mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setCanUseHiddenTabForSession(token, true);
Assert.assertTrue(
"Failed first mayLaunchUrl()",
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(url), null, null));
CriteriaHelper.pollUiThread(
() ->
Criteria.checkThat(
mCustomTabsConnection.getSpeculationParamsForTesting(),
Matchers.notNullValue()));
Tab hiddenTab = mCustomTabsConnection.getSpeculationParamsForTesting().tab;
CriteriaHelper.pollUiThread(
() ->
Criteria.checkThat(
hiddenTab.getWebContents().getTitle(),
Matchers.is("Activity test page")),
20000,
50);
ThreadUtils.runOnUiThreadBlocking(
() -> {
JsHelper.evaluateJavaScriptForTests(
hiddenTab.getWebContents(),
"document.cookie = \"foo_hidden=bar; max-age =1000 \";"
+ " document.cookie");
});
JsHelper.waitUntilHasValue(5, TimeUnit.SECONDS);
Assert.assertTrue("Failed to retrieve JavaScript evaluation results.", JsHelper.hasValue());
// The hidden tab should only see the cookie it set.
Assert.assertEquals("\"foo_hidden=bar\"", JsHelper.getJsonResultAndClear());
// Launch another hidden tab. Doing this closes the first hidden tab and causes the cookie
// jar to be cleared.
mCustomTabsConnection.resetThrottling(Process.myUid());
Assert.assertTrue(
"Failed second mayLaunchUrl()",
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(url), null, null));
CriteriaHelper.pollUiThread(
() ->
Criteria.checkThat(
mCustomTabsConnection.getSpeculationParamsForTesting(),
Matchers.notNullValue()));
Tab hiddenTab2 = mCustomTabsConnection.getSpeculationParamsForTesting().tab;
CriteriaHelper.pollUiThread(
() ->
Criteria.checkThat(
hiddenTab2.getWebContents().getTitle(),
Matchers.is("Activity test page")),
20000,
50);
ThreadUtils.runOnUiThreadBlocking(
() -> {
JsHelper.evaluateJavaScriptForTests(
hiddenTab2.getWebContents(), "document.cookie=\"foo_hidden2=baz\"");
});
JsHelper.waitUntilHasValue(5, TimeUnit.SECONDS);
Assert.assertTrue("Failed to retrieve JavaScript evaluation results.", JsHelper.hasValue());
// The second hidden tab should only have the cookie it set.
Assert.assertEquals("\"foo_hidden2=baz\"", JsHelper.getJsonResultAndClear());
// Launch the second custom tab. Because there is already a hidden tab for the same url this
// custom tab should just re-use the hidden tab. This means that this tab will use the same
// storage partition and therefore access the same cookie.
mCustomTabActivityTestRule.launchActivity(intent2);
Tab normalTab2 = mCustomTabActivityTestRule.getActivity().getActivityTab();
CriteriaHelper.pollUiThread(
() ->
Criteria.checkThat(
normalTab2.getWebContents().getTitle(),
Matchers.is("Activity test page")));
ThreadUtils.runOnUiThreadBlocking(
() -> {
JsHelper.evaluateJavaScriptForTests(
normalTab2.getWebContents(), "document.cookie");
});
JsHelper.waitUntilHasValue(5, TimeUnit.SECONDS);
Assert.assertTrue("Failed to retrieve JavaScript evaluation results.", JsHelper.hasValue());
// This custom tab should see the third cookie set.
Assert.assertEquals("\"foo_hidden2=baz\"", JsHelper.getJsonResultAndClear());
// Finally, launch a third custom tab. Because there isn't an associated mayLaunchUrl this
// custom tab will use the default storage partition and will access the first cookie.
Assert.assertTrue("Failed warmup()", mCustomTabsConnection.warmup(0));
Intent intent3 = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
mCustomTabActivityTestRule.launchActivity(intent3);
Tab normalTab3 = mCustomTabActivityTestRule.getActivity().getActivityTab();
CriteriaHelper.pollUiThread(
() ->
Criteria.checkThat(
normalTab3.getWebContents().getTitle(),
Matchers.is("Activity test page")));
ThreadUtils.runOnUiThreadBlocking(
() -> {
JsHelper.evaluateJavaScriptForTests(
normalTab3.getWebContents(), "document.cookie");
});
JsHelper.waitUntilHasValue(5, TimeUnit.SECONDS);
Assert.assertTrue("Failed to retrieve JavaScript evaluation results.", JsHelper.hasValue());
// This custom tab should see the third cookie set.
Assert.assertEquals("\"foo=bar\"", JsHelper.getJsonResultAndClear());
}
private void assertSpareWebContentsNotNullAndDestroy() {
if (ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_PREWARM_TAB)) {
Assert.assertTrue(
WarmupManager.getInstance()
.hasSpareTab(ProfileManager.getLastUsedRegularProfile()));
WarmupManager.getInstance().destroySpareTab();
} else {
WebContents webContents =
WarmupManager.getInstance().takeSpareWebContents(false, false);
Assert.assertNotNull(webContents);
webContents.destroy();
}
}
/**
* Calls warmup() and mayLaunchUrl(), checks for the expected result (success or failure) and
* returns the result code.
*/
private CustomTabsSessionToken assertWarmupAndMayLaunchUrl(
CustomTabsSessionToken token, String url, boolean shouldSucceed) throws Exception {
CustomTabsTestUtils.warmUpAndWait();
if (token == null) {
token = CustomTabsSessionToken.createMockSessionTokenForTesting();
mCustomTabsConnection.newSession(token);
}
Uri uri = url == null ? null : Uri.parse(url);
boolean succeeded = mCustomTabsConnection.mayLaunchUrl(token, uri, null, null);
Assert.assertEquals(shouldSucceed, succeeded);
return shouldSucceed ? token : null;
}
/**
* Tests that {@link CustomTabsConnection#mayLaunchUrl( CustomTabsSessionToken, Uri,
* android.os.Bundle, java.util.List)} returns an error when called with an invalid session ID.
*/
@Test
@SmallTest
public void testNoMayLaunchUrlWithInvalidSessionId() throws Exception {
assertWarmupAndMayLaunchUrl(
CustomTabsSessionToken.createMockSessionTokenForTesting(), URL, false);
}
/**
* Tests that {@link CustomTabsConnection#mayLaunchUrl(CustomTabsSessionToken, Uri, Bundle,
* List)} rejects invalid URL schemes.
*/
@Test
@SmallTest
public void testNoMayLaunchUrlWithInvalidScheme() throws Exception {
assertWarmupAndMayLaunchUrl(null, INVALID_SCHEME_URL, false);
}
/**
* Tests that {@link CustomTabsConnection#mayLaunchUrl(CustomTabsSessionToken, Uri, Bundle,
* List)} succeeds.
*/
@Test
@SmallTest
public void testMayLaunchUrl() throws Exception {
assertWarmupAndMayLaunchUrl(null, URL, true);
}
/**
* Tests that {@link CustomTabsConnection#mayLaunchUrl(CustomTabsSessionToken, Uri, Bundle,
* List)} can be called several times with the same, and different URLs.
*/
@Test
@SmallTest
public void testMultipleMayLaunchUrl() throws Exception {
CustomTabsSessionToken token = assertWarmupAndMayLaunchUrl(null, URL, true);
mCustomTabsConnection.resetThrottling(Process.myUid());
assertWarmupAndMayLaunchUrl(token, URL, true);
mCustomTabsConnection.resetThrottling(Process.myUid());
assertWarmupAndMayLaunchUrl(token, URL2, true);
}
/** Tests that sessions are forgotten properly. */
@Test
@SmallTest
public void testForgetsSession() throws Exception {
CustomTabsSessionToken token = assertWarmupAndMayLaunchUrl(null, URL, true);
CustomTabsTestUtils.cleanupSessions(mCustomTabsConnection);
assertWarmupAndMayLaunchUrl(token, URL, false);
}
/** Tests that whether we can detect access rights to /proc/pid/. */
@Test
@SmallTest
public void testCanGetSchedulerGroup() {
// self is always accessible.
Assert.assertTrue(CustomTabsConnection.canGetSchedulerGroup(Process.myPid()));
// PID 1 always exists, yet should never be accessible by regular apps.
Assert.assertFalse(CustomTabsConnection.canGetSchedulerGroup(1));
}
/** Tests that predictions are throttled. */
@Test
@SmallTest
public void testThrottleMayLaunchUrl() throws Exception {
CustomTabsSessionToken token = assertWarmupAndMayLaunchUrl(null, URL, true);
int successfulRequests = 0;
// Send a burst of requests instead of checking for precise delays to avoid flakiness.
while (successfulRequests < 10) {
if (!mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null)) break;
successfulRequests++;
}
Assert.assertTrue("10 requests in a row should not all succeed.", successfulRequests < 10);
}
/** Tests that the mayLaunchUrl() throttling is reset after a long enough wait. */
@Test
@SmallTest
public void testThrottlingIsReset() throws Exception {
CustomTabsSessionToken token = assertWarmupAndMayLaunchUrl(null, URL, true);
// Depending on the timing, the delay should be 100 or 200ms here.
assertWarmupAndMayLaunchUrl(token, URL, true);
// assertWarmUpAndMayLaunchUrl() can take longer than the throttling delay.
Assert.assertFalse(mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
// Wait for more than 2 * MAX_POSSIBLE_DELAY to clear the delay
try {
Thread.sleep(450); // 2 * MAX_POSSIBLE_DELAY + 50ms
} catch (InterruptedException e) {
Assert.fail();
return;
}
assertWarmupAndMayLaunchUrl(token, URL, true);
// Check that the delay has been reset, by waiting for 100ms.
try {
Thread.sleep(150); // MIN_DELAY + 50ms margin
} catch (InterruptedException e) {
Assert.fail();
return;
}
assertWarmupAndMayLaunchUrl(token, URL, true);
}
/** Tests that throttling applies across sessions. */
@Test
@SmallTest
public void testThrottlingAcrossSessions() throws Exception {
CustomTabsSessionToken token = assertWarmupAndMayLaunchUrl(null, URL, true);
mCustomTabsConnection.resetThrottling(Process.myUid());
CustomTabsSessionToken token2 = assertWarmupAndMayLaunchUrl(null, URL, true);
mCustomTabsConnection.resetThrottling(Process.myUid());
for (int i = 0; i < 10; i++) {
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null);
}
Assert.assertFalse(mCustomTabsConnection.mayLaunchUrl(token2, Uri.parse(URL), null, null));
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testBanningWorks() {
mCustomTabsConnection.ban(Process.myUid());
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
Assert.assertTrue(mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
ThreadUtils.runOnUiThreadBlocking(this::assertSpareWebContentsNotNullAndDestroy);
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testBanningDisabledForCellular() {
mCustomTabsConnection.ban(Process.myUid());
final CustomTabsSessionToken token =
CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setShouldSpeculateLoadOnCellularForSession(token, true);
Assert.assertTrue(mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
ThreadUtils.runOnUiThreadBlocking(
() ->
Assert.assertNull(
WarmupManager.getInstance().takeSpareWebContents(false, false)));
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testCellularPrerenderingDoesntOverrideSettings() throws Exception {
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setShouldSpeculateLoadOnCellularForSession(token, true);
CustomTabsTestUtils.warmUpAndWait();
// Needs the browser process to be initialized.
@PreloadPagesState
int state =
ThreadUtils.runOnUiThreadBlocking(
() -> {
Profile profile = ProfileManager.getLastUsedRegularProfile();
@PreloadPagesState
int oldState = PreloadPagesSettingsBridge.getState(profile);
PreloadPagesSettingsBridge.setState(
profile, PreloadPagesState.NO_PRELOADING);
return oldState;
});
try {
Assert.assertTrue(
mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
ThreadUtils.runOnUiThreadBlocking(this::assertSpareWebContentsNotNullAndDestroy);
} finally {
ThreadUtils.runOnUiThreadBlocking(
() ->
PreloadPagesSettingsBridge.setState(
ProfileManager.getLastUsedRegularProfile(), state));
}
}
@Test
@SmallTest
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testHiddenTabTakesSpareRenderer() throws Exception {
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
mCustomTabsConnection.setShouldSpeculateLoadOnCellularForSession(token, true);
CustomTabsTestUtils.warmUpAndWait();
if (ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_PREWARM_TAB)) {
ThreadUtils.runOnUiThreadBlocking(
() ->
Assert.assertTrue(
WarmupManager.getInstance()
.hasSpareTab(
ProfileManager.getLastUsedRegularProfile())));
} else {
ThreadUtils.runOnUiThreadBlocking(
() -> Assert.assertTrue(WarmupManager.getInstance().hasSpareWebContents()));
}
Assert.assertTrue(mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
if (ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_PREWARM_TAB)) {
ThreadUtils.runOnUiThreadBlocking(
() ->
Assert.assertFalse(
WarmupManager.getInstance()
.hasSpareTab(
ProfileManager.getLastUsedRegularProfile())));
} else {
ThreadUtils.runOnUiThreadBlocking(
() -> Assert.assertFalse(WarmupManager.getInstance().hasSpareWebContents()));
}
}
@Test
@SmallTest
public void testWarmupNotificationIsSent() throws Exception {
final AtomicReference<CustomTabsClient> clientReference = new AtomicReference<>(null);
final CallbackHelper waitForConnection = new CallbackHelper();
CustomTabsClient.bindCustomTabsService(
ApplicationProvider.getApplicationContext(),
ApplicationProvider.getApplicationContext().getPackageName(),
new CustomTabsServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {}
@Override
public void onCustomTabsServiceConnected(
ComponentName name, CustomTabsClient client) {
clientReference.set(client);
waitForConnection.notifyCalled();
}
});
waitForConnection.waitForCallback(0);
CustomTabsClient client = clientReference.get();
final CallbackHelper warmupWaiter = new CallbackHelper();
newSessionWithWarmupWaiter(client, warmupWaiter);
newSessionWithWarmupWaiter(client, warmupWaiter);
// Both sessions should be notified.
Assert.assertTrue(mCustomTabsConnection.warmup(0));
warmupWaiter.waitForCallback(0, 2, 20, TimeUnit.SECONDS);
// Notifications should be sent even if warmup() has already been called.
Assert.assertTrue(mCustomTabsConnection.warmup(0));
warmupWaiter.waitForCallback(2, 2);
}
private static CustomTabsSession newSessionWithWarmupWaiter(
CustomTabsClient client, final CallbackHelper waiter) {
return client.newSession(
new CustomTabsCallback() {
@Override
public void extraCallback(String callbackName, Bundle args) {
if (callbackName.equals(CustomTabsConnection.ON_WARMUP_COMPLETED)) {
waiter.notifyCalled();
}
}
});
}
/** Tests that prefetch() with valid Uri and default PrefetchOptions succeeds. */
@Test
@SmallTest
@EnableFeatures({
ChromeFeatureList.PREFETCH_BROWSER_INITIATED_TRIGGERS,
ChromeFeatureList.CCT_NAVIGATIONAL_PREFETCH
})
public void testPrefetch() throws Exception {
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
Assert.assertTrue(mCustomTabsConnection.warmup(0));
Assert.assertTrue(
mCustomTabsConnection.prefetch(
token, Uri.parse(URL), new PrefetchOptions.Builder().build()));
}
/** Tests that prefetch() with invalid Uri fails. */
@Test
@SmallTest
@EnableFeatures({
ChromeFeatureList.PREFETCH_BROWSER_INITIATED_TRIGGERS,
ChromeFeatureList.CCT_NAVIGATIONAL_PREFETCH
})
public void testPrefetchWithInvalidUri() throws Exception {
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
Assert.assertTrue(mCustomTabsConnection.warmup(0));
Assert.assertFalse(
mCustomTabsConnection.prefetch(
token,
Uri.parse(INVALID_SCHEME_URL),
new PrefetchOptions.Builder().build()));
}
@Test
@SmallTest
public void testverifySourceOriginOfPrefetch() throws Exception {
String sourceOrigin = URL;
String invalidSourceOrigin = URL2;
String packageName = "app";
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
mCustomTabsConnection.overridePackageNameForSessionForTesting(token, packageName);
ThreadUtils.runOnUiThreadBlocking(
() ->
ChromeOriginVerifier.addVerificationOverride(
packageName,
Origin.create(sourceOrigin),
CustomTabsService.RELATION_USE_AS_ORIGIN));
PrefetchOptions prefetchOptionsEmptySourceOrigin = new PrefetchOptions.Builder().build();
PrefetchOptions prefetchOptionsValidSourceOrigin =
new PrefetchOptions.Builder().setSourceOrigin(Uri.parse(sourceOrigin)).build();
PrefetchOptions prefetchOptionsInvalidSourceOrigin =
new PrefetchOptions.Builder()
.setSourceOrigin(Uri.parse(invalidSourceOrigin))
.build();
Assert.assertFalse(
mCustomTabsConnection.verifySourceOriginOfPrefetch(
token, prefetchOptionsEmptySourceOrigin.sourceOrigin));
Assert.assertTrue(
mCustomTabsConnection.verifySourceOriginOfPrefetch(
token, prefetchOptionsValidSourceOrigin.sourceOrigin));
Assert.assertFalse(
mCustomTabsConnection.verifySourceOriginOfPrefetch(
token, prefetchOptionsInvalidSourceOrigin.sourceOrigin));
}
/** Tests that prefetch() with valid Uri and valid sourceOrigin succeeds. */
@Test
@SmallTest
@EnableFeatures({
ChromeFeatureList.PREFETCH_BROWSER_INITIATED_TRIGGERS,
ChromeFeatureList.CCT_NAVIGATIONAL_PREFETCH
})
public void testPrefetchWithValidSourceOriginUri() throws Exception {
String sourceOrigin = URL;
String prefetchUrl = URL2;
String packageName = "app";
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
Assert.assertTrue(mCustomTabsConnection.warmup(0));
mCustomTabsConnection.overridePackageNameForSessionForTesting(token, packageName);
ThreadUtils.runOnUiThreadBlocking(
() ->
ChromeOriginVerifier.addVerificationOverride(
packageName,
Origin.create(sourceOrigin),
CustomTabsService.RELATION_USE_AS_ORIGIN));
Assert.assertTrue(
mCustomTabsConnection.prefetch(
token,
Uri.parse(prefetchUrl),
new PrefetchOptions.Builder()
.setSourceOrigin(Uri.parse(sourceOrigin))
.build()));
}
/** Tests that prefetch() with valid Uri and invalid sourceOrigin succeeds. */
@Test
@SmallTest
@EnableFeatures({
ChromeFeatureList.PREFETCH_BROWSER_INITIATED_TRIGGERS,
ChromeFeatureList.CCT_NAVIGATIONAL_PREFETCH
})
public void testPrefetchWithInvalidSourceOriginUri() throws Exception {
String invalidSourceOrigin = URL;
String prefetchUrl = URL2;
String packageName = "app";
CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
Assert.assertTrue(mCustomTabsConnection.newSession(token));
Assert.assertTrue(mCustomTabsConnection.warmup(0));
mCustomTabsConnection.overridePackageNameForSessionForTesting(token, packageName);
Assert.assertTrue(
mCustomTabsConnection.prefetch(
token,
Uri.parse(prefetchUrl),
new PrefetchOptions.Builder()
.setSourceOrigin(Uri.parse(invalidSourceOrigin))
.build()));
}
}