[go: nahoru, domu]

blob: 237ee7461e0c05f927de02ac7603e2b77a4ce1b6 [file] [log] [blame]
// Copyright 2022 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.android_webview.test;
import static org.chromium.android_webview.test.AwActivityTestRule.WAIT_TIMEOUT_MS;
import android.webkit.JavascriptInterface;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.google.common.util.concurrent.SettableFuture;
import org.json.JSONObject;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwCookieManager;
import org.chromium.android_webview.AwSettings;
import org.chromium.android_webview.client_hints.AwUserAgentMetadata;
import org.chromium.android_webview.test.util.CookieUtils;
import org.chromium.android_webview.test.util.JSUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.content_public.common.ContentSwitches;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.ServerCertificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Test suite for user-agent client hints.
* Notes: When verifying sec-ch-ua-mobile client hints value on WebView tests, we can't assume
* mobile is always true because there is some test bots don't set to use mobile user-agent.
*/
@DoNotBatch(reason = "These tests conflict with each other.")
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
public class ClientHintsTest extends AwParameterizedTest {
@Rule public AwActivityTestRule mActivityTestRule;
private static final String[] USER_AGENT_CLIENT_HINTS = {
"sec-ch-ua",
"sec-ch-ua-arch",
"sec-ch-ua-platform",
"sec-ch-ua-model",
"sec-ch-ua-mobile",
"sec-ch-ua-full-version",
"sec-ch-ua-platform-version",
"sec-ch-ua-bitness",
"sec-ch-ua-full-version-list",
"sec-ch-ua-wow64",
"sec-ch-ua-form-factor"
};
private static final String ANDROID_WEBVIEW_BRAND_NAME = "Android WebView";
private static final String CHROME_PRODUCT_PATTERN = "Chrome/(\\d+).(\\d+).(\\d+).(\\d+)";
private static class ClientHintsTestResult {
public Map<String, String> mHttpHeaderClientHints;
public JSONObject mJsClientHints;
public ClientHintsTestResult(
Map<String, String> httpHeaderClientHints, JSONObject jsClientHints) {
this.mHttpHeaderClientHints = httpHeaderClientHints;
this.mJsClientHints = jsClientHints;
}
}
public ClientHintsTest(AwSettingsMutation param) {
this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testClientHintsDefault() throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
contents.getSettings().setJavaScriptEnabled(true);
// First round uses insecure server.
AwEmbeddedTestServer server =
AwEmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getTargetContext());
verifyClientHintBehavior(server, contents, contentsClient, false);
clearCookies();
server.stopAndDestroyServer();
// Second round uses secure server.
server =
AwEmbeddedTestServer.createAndStartHTTPSServer(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
ServerCertificate.CERT_OK);
verifyClientHintBehavior(server, contents, contentsClient, true);
clearCookies();
server.stopAndDestroyServer();
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({
"enable-features=ClientHintsPrefersReducedTransparency",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testAllClientHints() throws Throwable {
// Initial test setup.
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
contents.getSettings().setJavaScriptEnabled(true);
final AwEmbeddedTestServer server =
AwEmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// Please keep these here (and below) in the same order as web_client_hints_types.mojom.
final String[] activeClientHints = {
"device-memory",
"dpr",
"width",
"viewport-width",
"rtt",
"downlink",
"ect",
// "sec-ch-lang" was removed in M96
"sec-ch-ua",
"sec-ch-ua-arch",
"sec-ch-ua-platform",
"sec-ch-ua-model",
"sec-ch-ua-mobile",
"sec-ch-ua-full-version",
"sec-ch-ua-platform-version",
"sec-ch-prefers-color-scheme",
"sec-ch-ua-bitness",
"sec-ch-viewport-height",
"sec-ch-device-memory",
"sec-ch-dpr",
"sec-ch-width",
"sec-ch-viewport-width",
"sec-ch-ua-full-version-list",
"sec-ch-ua-wow64",
"save-data",
"sec-ch-prefers-reduced-motion",
"sec-ch-ua-form-factor",
"sec-ch-prefers-reduced-transparency",
// Add client hints above. The final row should have a trailing comma for cleaner
// diffs.
};
final String url =
server.getURL(
"/client-hints-header?accept-ch=" + String.join(",", activeClientHints));
// Load twice to be sure hints are returned, then parse the results.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
String textContent =
mActivityTestRule
.getJavaScriptResultBodyTextContent(contents, contentsClient)
.replaceAll("\\\\\"", "\"");
// Get client hints from HTTP request header.
HashMap<String, String> clientHintsMap = getClientHints(textContent);
// If you're here because this line broke, please update this test to verify whichever
// client hints were added or removed by changing `activeClientHints` above.
Assert.assertEquals(
"The number of client hints is unexpected. If you intentionally added "
+ "or removed a client hint, please update this test.",
activeClientHints.length,
clientHintsMap.size());
// All client hints must be verified for default behavior.
Assert.assertTrue(Integer.valueOf(clientHintsMap.get("device-memory")) > 0);
Assert.assertTrue(Double.valueOf(clientHintsMap.get("dpr")) > 0);
// This is only set for subresources.
Assert.assertEquals("HEADER_NOT_FOUND", clientHintsMap.get("width"));
Assert.assertTrue(Integer.valueOf(clientHintsMap.get("viewport-width")) > 0);
Assert.assertTrue(Integer.valueOf(clientHintsMap.get("rtt")) == 0);
Assert.assertTrue(Integer.valueOf(clientHintsMap.get("downlink")) == 0);
// This is the holdback value (the default in some cases).
Assert.assertEquals("4g", clientHintsMap.get("ect"));
// This client hint was removed.
Assert.assertNull(clientHintsMap.get("sec-ch-lang"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-arch"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-platform"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-model"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-mobile"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-full-version"));
// User agent client hints are active on android webview.
Assert.assertNotEquals(
"HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-platform-version"));
Assert.assertEquals("light", clientHintsMap.get("sec-ch-prefers-color-scheme"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-bitness"));
Assert.assertTrue(Integer.valueOf(clientHintsMap.get("sec-ch-viewport-height")) > 0);
Assert.assertTrue(Integer.valueOf(clientHintsMap.get("sec-ch-device-memory")) > 0);
Assert.assertTrue(Double.valueOf(clientHintsMap.get("sec-ch-dpr")) > 0);
// This is only set for subresources.
Assert.assertEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-width"));
Assert.assertTrue(Integer.valueOf(clientHintsMap.get("sec-ch-viewport-width")) > 0);
// User agent client hints are active on android webview.
Assert.assertNotEquals(
"HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-full-version-list"));
// User agent client hints are active on android webview.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-wow64"));
// This client hint isn't sent when data-saver is off.
Assert.assertEquals("HEADER_NOT_FOUND", clientHintsMap.get("save-data"));
Assert.assertNotEquals(
"HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-prefers-reduced-motion"));
Assert.assertEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-form-factor"));
Assert.assertNotEquals(
"HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-prefers-reduced-transparency"));
// Cleanup after test.
clearCookies();
server.stopAndDestroyServer();
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testEnableUserAgentClientHintsNoCustom() throws Throwable {
// Initial test setup.
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
verifyUserAgentOverrideClientHints(
/* contentsClient= */ contentsClient,
/* contents= */ contents,
/* customUserAgent= */ null,
/* expectHighEntropyClientHints= */ true);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testEnableUserAgentClientHintsCustomOverride() throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
verifyUserAgentOverrideClientHints(
/* contentsClient= */ contentsClient,
/* contents= */ contents,
/* customUserAgent= */ "CustomUserAgentOverride",
/* expectHighEntropyClientHints= */ false);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testEnableUserAgentClientHintsModifyDefaultUserAgent() throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
String defaultUserAgent = settings.getUserAgentString();
// Override user-agent with appending suffix.
verifyUserAgentOverrideClientHints(
/* contentsClient= */ contentsClient,
/* contents= */ contents,
/* customUserAgent= */ defaultUserAgent + "CustomUserAgentSuffix",
/* expectHighEntropyClientHints= */ true);
// Override user-agent with adding prefix.
verifyUserAgentOverrideClientHints(
/* contentsClient= */ contentsClient,
/* contents= */ contents,
/* customUserAgent= */ "CustomUserAgentPrefix" + defaultUserAgent,
/* expectHighEntropyClientHints= */ true);
// Override user-agent with adding both prefix and suffix.
verifyUserAgentOverrideClientHints(
/* contentsClient= */ contentsClient,
/* contents= */ contents,
/* customUserAgent= */ "CustomUserAgentPrefix"
+ defaultUserAgent
+ "CustomUserAgentSuffix",
/* expectHighEntropyClientHints= */ true);
// Override user-agent with empty string, it's assumed to use system default.
verifyUserAgentOverrideClientHints(
/* contentsClient= */ contentsClient,
/* contents= */ contents,
/* customUserAgent= */ "",
/* expectHighEntropyClientHints= */ true);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
@SkipMutations(reason = "This test depends on AwSettings.setUserAgentString()")
public void testEnableUserAgentClientHintsJavaScript() throws Throwable {
verifyClientHintsJavaScript(/* useCustomUserAgent= */ false);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testEnableUserAgentClientHintsOverrideJavaScript() throws Throwable {
verifyClientHintsJavaScript(/* useCustomUserAgent= */ true);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testCriticalClientHints() throws Throwable {
// Initial test setup.
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
contents.getSettings().setJavaScriptEnabled(true);
final AwEmbeddedTestServer server =
AwEmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// First we verify that sec-ch-device-memory (critical) is returned on the first load.
String url =
server.getURL(
"/critical-client-hints-header?accept-ch=sec-ch-device-memory&"
+ "critical-ch=sec-ch-device-memory");
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", true);
validateHeadersFromJSON(contents, contentsClient, "device-memory", false);
// Second we verify that device-memory (not critical) won't cause a reload.
url =
server.getURL(
"/critical-client-hints-header?accept-ch=sec-ch-device-memory,device-memory&"
+ "critical-ch=sec-ch-device-memory");
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", true);
validateHeadersFromJSON(contents, contentsClient, "device-memory", false);
// Third we verify that device-memory is returned on the final load even with no request.
url =
server.getURL(
"/critical-client-hints-header?accept-ch=sec-ch-device-memory&"
+ "critical-ch=sec-ch-device-memory");
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", true);
validateHeadersFromJSON(contents, contentsClient, "device-memory", true);
// Cleanup after test.
clearCookies();
server.stopAndDestroyServer();
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataGetApi() throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
Map<String, Object> defaultUserAgentMetadata = settings.getUserAgentMetadataMap();
// Override part of value in user-agent metadata.
settings.setUserAgentMetadataFromMap(
Map.of(AwUserAgentMetadata.MetadataKeys.PLATFORM, "fake_platform"));
// Verify getUserAgentMetadataMap API returns the correct value.
Map<String, Object> customUserAgentMetadata = settings.getUserAgentMetadataMap();
Assert.assertEquals(
"Android", defaultUserAgentMetadata.get(AwUserAgentMetadata.MetadataKeys.PLATFORM));
Assert.assertEquals(
"fake_platform",
customUserAgentMetadata.get(AwUserAgentMetadata.MetadataKeys.PLATFORM));
// Verify the remaining of entries are equals.
defaultUserAgentMetadata.remove(AwUserAgentMetadata.MetadataKeys.PLATFORM);
customUserAgentMetadata.remove(AwUserAgentMetadata.MetadataKeys.PLATFORM);
Assert.assertEquals(defaultUserAgentMetadata, customUserAgentMetadata);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataInvalidBitness() throws Throwable {
try {
getClientHintsWithOverrides(
Map.of(AwUserAgentMetadata.MetadataKeys.BITNESS, "foo"),
/* overrideUserAgent= */ null);
Assert.fail("Should have thrown exception.");
} catch (IllegalArgumentException e) {
Assert.assertEquals(
"AwUserAgentMetadata map does not have right type of "
+ "value for key: BITNESS",
e.getMessage());
}
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataDefaultBitness() throws Throwable {
// Override with bitness 0, we expect it return an empty string.
ClientHintsTestResult clientHintsResult =
getClientHintsWithOverrides(
Map.of(AwUserAgentMetadata.MetadataKeys.BITNESS, 0),
/* overrideUserAgent= */ null);
// Verify Http header client hints results.
Map<String, String> clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
Assert.assertEquals("\"\"", clientHintsMap.get("sec-ch-ua-bitness"));
Assert.assertEquals("\"Android\"", clientHintsMap.get("sec-ch-ua-platform"));
// Verify js client hints results.
JSONObject jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertEquals("", jsClientHints.getString("bitness"));
Assert.assertEquals("Android", jsClientHints.getString("platform"));
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataValidBitness() throws Throwable {
ClientHintsTestResult clientHintsResult =
getClientHintsWithOverrides(
Map.of(AwUserAgentMetadata.MetadataKeys.BITNESS, 32),
/* overrideUserAgent= */ null);
// Verify Http header client hints results.
Map<String, String> clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
Assert.assertEquals("\"32\"", clientHintsMap.get("sec-ch-ua-bitness"));
// Verify js client hints results.
JSONObject jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertEquals("32", jsClientHints.getString("bitness"));
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataOverrideBrand() throws Throwable {
// override with empty full version
ClientHintsTestResult clientHintsResult =
getClientHintsWithOverrides(
Map.of(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
new String[][] {{"brand1", "1", "1.1.1"}, {"brand2", "2", ""}}),
/* overrideUserAgent= */ null);
// Verify Http header client hints results.
Map<String, String> clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
Assert.assertEquals(
"\"brand1\";v=\"1\", \"brand2\";v=\"2\"", clientHintsMap.get("sec-ch-ua"));
Assert.assertEquals(
"\"brand1\";v=\"1.1.1\"", clientHintsMap.get("sec-ch-ua-full-version-list"));
Assert.assertEquals("\"Android\"", clientHintsMap.get("sec-ch-ua-platform"));
// Verify js client hints results.
JSONObject jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"2\"}]",
jsClientHints.getString("brands"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1.1.1\"}]",
jsClientHints.getString("fullVersionList"));
Assert.assertEquals("Android", jsClientHints.getString("platform"));
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataInvalidBrand() throws Throwable {
// Test invalid input brand array: size only 2.
try {
getClientHintsWithOverrides(
Map.of(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
new String[][] {{"brand1", "1", "1.1.1"}, {"brand2", "2"}}),
/* overrideUserAgent= */ null);
Assert.fail("Should have thrown exception.");
} catch (IllegalArgumentException e) {
Assert.assertEquals(
"AwUserAgentMetadata map does not have right type of value "
+ "for key: "
+ AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST
+ ", expect brand item length:3, actual:2",
e.getMessage());
}
// Test invalid input brand array with null.
try {
getClientHintsWithOverrides(
Map.of(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
new String[][] {{"brand1", "1", "1.1.1"}, {"brand2", "2", null}}),
/* overrideUserAgent= */ null);
Assert.fail("Should have thrown exception.");
} catch (IllegalArgumentException e) {
Assert.assertEquals(
"AwUserAgentMetadata map does not have right type of value "
+ "for key: "
+ AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST
+ ", brand item should not set as null",
e.getMessage());
}
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataClearOverride() throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
contents.getSettings().setJavaScriptEnabled(true);
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
// 1. Override platform, brands, and wow64
settings.setUserAgentMetadataFromMap(
Map.of(
AwUserAgentMetadata.MetadataKeys.PLATFORM,
"fake_platform",
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
new String[][] {{"brand1", "1", "1.1.1"}, {"brand2", "2", "2.2.2"}},
AwUserAgentMetadata.MetadataKeys.WOW64,
true));
final AwEmbeddedTestServer server =
AwEmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// Make first request and verify client hints.
ClientHintsTestResult clientHintsResult =
makeRequestAndGetClientHints(server, contents, contentsClient);
// Verify Http header client hints results.
Map<String, String> clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
Assert.assertEquals("\"fake_platform\"", clientHintsMap.get("sec-ch-ua-platform"));
Assert.assertEquals(
"\"brand1\";v=\"1\", \"brand2\";v=\"2\"", clientHintsMap.get("sec-ch-ua"));
Assert.assertEquals(
"\"brand1\";v=\"1.1.1\", \"brand2\";v=\"2.2.2\"",
clientHintsMap.get("sec-ch-ua-full-version-list"));
Assert.assertEquals("?1", clientHintsMap.get("sec-ch-ua-wow64"));
// Verify js client hints results.
JSONObject jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertEquals("fake_platform", jsClientHints.getString("platform"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"2\"}]",
jsClientHints.getString("brands"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1.1.1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"2.2.2\"}]",
jsClientHints.getString("fullVersionList"));
Assert.assertTrue(jsClientHints.getBoolean("wow64"));
// 2. Reset previous overrides for platform.
HashMap<String, Object> overrideResetMap = new HashMap<>();
overrideResetMap.put(AwUserAgentMetadata.MetadataKeys.PLATFORM, null);
overrideResetMap.put(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
new String[][] {{"brand1", "2", "2.1.1"}, {"brand2", "3", "3.2.2"}});
overrideResetMap.put(AwUserAgentMetadata.MetadataKeys.BITNESS, 100);
settings.setUserAgentMetadataFromMap(overrideResetMap);
// Make second request and verify clear overrides result.
clientHintsResult = makeRequestAndGetClientHints(server, contents, contentsClient);
// Verify Http header client hints results.
clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
// Platform should be reset as default.
Assert.assertEquals("\"Android\"", clientHintsMap.get("sec-ch-ua-platform"));
// Brand should use the latest override value.
Assert.assertEquals(
"\"brand1\";v=\"2\", \"brand2\";v=\"3\"", clientHintsMap.get("sec-ch-ua"));
Assert.assertEquals(
"\"brand1\";v=\"2.1.1\", \"brand2\";v=\"3.2.2\"",
clientHintsMap.get("sec-ch-ua-full-version-list"));
// Wow64 has been reset to default.
Assert.assertEquals("?0", clientHintsMap.get("sec-ch-ua-wow64"));
// Bitness should use the latest override value.
Assert.assertEquals("\"100\"", clientHintsMap.get("sec-ch-ua-bitness"));
// Verify js client hints results.
jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertEquals("Android", jsClientHints.getString("platform"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"2\"},"
+ "{\"brand\":\"brand2\",\"version\":\"3\"}]",
jsClientHints.getString("brands"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"2.1.1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"3.2.2\"}]",
jsClientHints.getString("fullVersionList"));
Assert.assertFalse(jsClientHints.getBoolean("wow64"));
Assert.assertEquals("100", jsClientHints.getString("bitness"));
// Cleanup after test.
clearCookies();
server.stopAndDestroyServer();
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataClearOverrideWithCustomUA() throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
contents.getSettings().setJavaScriptEnabled(true);
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
// 1. Override user-agent metadata and overridden user-agent doesn't contains default
// user-agent.
settings.setUserAgentMetadataFromMap(
Map.of(
AwUserAgentMetadata.MetadataKeys.PLATFORM,
"fake_platform",
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
new String[][] {{"brand1", "1", "1.1.1"}, {"brand2", "2", "2.2.2"}},
AwUserAgentMetadata.MetadataKeys.WOW64,
true));
settings.setUserAgentString("testCustomUserAgent");
final AwEmbeddedTestServer server =
AwEmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// Make first request and verify client hints.
ClientHintsTestResult clientHintsResult =
makeRequestAndGetClientHints(server, contents, contentsClient);
// Verify Http header client hints results.
Map<String, String> clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
Assert.assertEquals("\"fake_platform\"", clientHintsMap.get("sec-ch-ua-platform"));
Assert.assertEquals(
"\"brand1\";v=\"1\", \"brand2\";v=\"2\"", clientHintsMap.get("sec-ch-ua"));
Assert.assertEquals(
"\"brand1\";v=\"1.1.1\", \"brand2\";v=\"2.2.2\"",
clientHintsMap.get("sec-ch-ua-full-version-list"));
Assert.assertEquals("?1", clientHintsMap.get("sec-ch-ua-wow64"));
// Verify js client hints results.
JSONObject jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertEquals("fake_platform", jsClientHints.getString("platform"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"2\"}]",
jsClientHints.getString("brands"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1.1.1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"2.2.2\"}]",
jsClientHints.getString("fullVersionList"));
Assert.assertTrue(jsClientHints.getBoolean("wow64"));
// 2. Clear previous overrides for platform, and make second request and verify clear
// overrides result.
settings.setUserAgentMetadataFromMap(null);
clientHintsResult = makeRequestAndGetClientHints(server, contents, contentsClient);
// Verify Http header client hints results only generate system default low-entropy client
// hints, high-entropy client hints are empty.
clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
Assert.assertEquals("\"Android\"", clientHintsMap.get("sec-ch-ua-platform"));
Assert.assertTrue(
clientHintsMap.get("sec-ch-ua").indexOf(ANDROID_WEBVIEW_BRAND_NAME) != -1);
Assert.assertTrue(clientHintsMap.get("sec-ch-ua-full-version-list").isEmpty());
// Verify js client hints results only generate system default low-entropy client
// hints, high-entropy client hints are empty.
jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertFalse(jsClientHints.getString("brands").isEmpty());
Assert.assertTrue(
jsClientHints.getString("brands").indexOf(ANDROID_WEBVIEW_BRAND_NAME) != -1);
Assert.assertFalse(jsClientHints.getString("mobile").isEmpty());
Assert.assertEquals("Android", jsClientHints.getString("platform"));
Assert.assertEquals("[]", jsClientHints.getString("fullVersionList"));
// Cleanup after test.
clearCookies();
server.stopAndDestroyServer();
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataClearOverrideVerifyGetApi() throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
// Override platform in user-agent metadata.
String[][] overrideBrands =
new String[][] {{"brand1", "1", "1.1.1"}, {"brand2", "2", "2.2.2"}};
settings.setUserAgentMetadataFromMap(
Map.of(
AwUserAgentMetadata.MetadataKeys.PLATFORM,
"fake_platform",
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
overrideBrands,
AwUserAgentMetadata.MetadataKeys.WOW64,
true));
Map<String, Object> customUserAgentMetadata = settings.getUserAgentMetadataMap();
Assert.assertEquals(
"fake_platform",
customUserAgentMetadata.get(AwUserAgentMetadata.MetadataKeys.PLATFORM));
Assert.assertEquals(
new Boolean(true),
customUserAgentMetadata.get(AwUserAgentMetadata.MetadataKeys.WOW64));
Assert.assertEquals(
Arrays.deepToString(overrideBrands),
Arrays.deepToString(
(String[][])
customUserAgentMetadata.get(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST)));
// Update the outside brand, the deep copy brand version list shouldn't change.
overrideBrands[0][0] = "updated_brand";
Assert.assertNotEquals(
Arrays.deepToString(overrideBrands),
Arrays.deepToString(
(String[][])
customUserAgentMetadata.get(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST)));
// Reset the previous override, the user-agent metadata should be the default value.
HashMap<String, Object> overrideResetMap = new HashMap<>();
overrideResetMap.put(AwUserAgentMetadata.MetadataKeys.PLATFORM, null);
settings.setUserAgentMetadataFromMap(overrideResetMap);
customUserAgentMetadata = settings.getUserAgentMetadataMap();
Assert.assertEquals(
"Android", customUserAgentMetadata.get(AwUserAgentMetadata.MetadataKeys.PLATFORM));
Assert.assertEquals(
new Boolean(false),
customUserAgentMetadata.get(AwUserAgentMetadata.MetadataKeys.WOW64));
String[][] actualOverrideBrands =
(String[][])
customUserAgentMetadata.get(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST);
List<String> brands = new ArrayList<>();
for (String[] bv : actualOverrideBrands) {
brands.add(bv[0]);
}
Assert.assertTrue(brands.contains("Android WebView"));
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataFullWithoutUAOverrides() throws Throwable {
// Test overriding full set of user-agent metadata and has no user-agent overrides.
verifyOverrideUaAndOverrideUaMetadata(null);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataFullWithCustomUa() throws Throwable {
// Test overriding full set of user-agent metadata and overriding user-agent doesn't
// contains default user-agent.
verifyOverrideUaAndOverrideUaMetadata("customUserAgent");
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataFullWithDefaultUA() throws Throwable {
// Test overriding full set of user-agent metadata and overriding user-agent contains
// default user-agent.
verifyOverrideUaAndOverrideUaMetadata(getDefaultUserAgent() + "_Suffix");
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Preferences"})
@CommandLineFlags.Add({
"enable-features=ClientHintsFormFactor",
ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"
})
public void testOverrideUserAgentMetadataNullWithCustomUserAgent() throws Throwable {
// High-entropy client hints should not be populated when overridden user-agent
// doesn't contain default ua, and users also don't override user-agent metadata.
ClientHintsTestResult clientHintsResult =
getClientHintsWithOverrides(
/* uaMetadataOverrides= */ null,
/* overrideUserAgent= */ "customUserAgent");
Map<String, String> clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
// Verify http header low-entropy client hints result.
Assert.assertFalse(clientHintsMap.get("sec-ch-ua").isEmpty());
Assert.assertFalse(clientHintsMap.get("sec-ch-ua-mobile").isEmpty());
Assert.assertEquals("\"Android\"", clientHintsMap.get("sec-ch-ua-platform"));
// Verify http header high-entropy client hints result, here we take some client hints
// should not be empty if generated as examples to verify.
Assert.assertEquals("\"\"", clientHintsMap.get("sec-ch-ua-full-version"));
Assert.assertEquals("", clientHintsMap.get("sec-ch-ua-full-version-list"));
Assert.assertEquals("\"\"", clientHintsMap.get("sec-ch-ua-platform-version"));
// Verify js low-entropy client hints result.
JSONObject jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertFalse(jsClientHints.getString("brands").isEmpty());
Assert.assertFalse(jsClientHints.getString("mobile").isEmpty());
Assert.assertEquals("Android", jsClientHints.getString("platform"));
// Verify js high-entropy client hints result.
Assert.assertEquals("[]", jsClientHints.getString("fullVersionList"));
Assert.assertEquals("", jsClientHints.getString("uaFullVersion"));
Assert.assertEquals("", jsClientHints.getString("platformVersion"));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@SkipMutations(reason = "This test depends on AwSettings.setUserAgentString()")
public void testDefaultUserAgentDefaultReductionOverride() throws Throwable {
String defaultUserAgent = getDefaultUserAgent();
// Verify user-agent minor version not reduced.
Matcher uaMatcher = Pattern.compile(CHROME_PRODUCT_PATTERN).matcher(defaultUserAgent);
Assert.assertTrue(uaMatcher.find());
Assert.assertNotEquals(
"0.0.0",
String.format(
"%s.%s.%s", uaMatcher.group(2), uaMatcher.group(3), uaMatcher.group(4)));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@SkipMutations(reason = "This test depends on AwSettings.setUserAgentString()")
@CommandLineFlags.Add({"enable-features=ReduceUserAgentMinorVersion"})
public void testDefaultUserAgentEnableReductionOverride() throws Throwable {
String defaultUserAgent = getDefaultUserAgent();
// Verify user-agent minor version is reduced.
Matcher uaMatcher = Pattern.compile(CHROME_PRODUCT_PATTERN).matcher(defaultUserAgent);
Assert.assertTrue(uaMatcher.find());
Assert.assertEquals(
"0.0.0",
String.format(
"%s.%s.%s", uaMatcher.group(2), uaMatcher.group(3), uaMatcher.group(4)));
}
private void verifyOverrideUaAndOverrideUaMetadata(String overrideUserAgent) throws Throwable {
ClientHintsTestResult clientHintsResult =
getClientHintsWithOverrides(
makeFakeMetadata(), /* overrideUserAgent= */ overrideUserAgent);
Map<String, String> clientHintsMap = clientHintsResult.mHttpHeaderClientHints;
// Verify http header client hints result.
Assert.assertEquals(
"\"brand1\";v=\"1\", \"brand2\";v=\"2\"", clientHintsMap.get("sec-ch-ua"));
Assert.assertEquals(
"\"brand1\";v=\"1.1.1\", \"brand2\";v=\"2.2.2\"",
clientHintsMap.get("sec-ch-ua-full-version-list"));
Assert.assertEquals("\"2.2.2\"", clientHintsMap.get("sec-ch-ua-full-version"));
Assert.assertEquals("\"overrideTest\"", clientHintsMap.get("sec-ch-ua-platform"));
Assert.assertEquals("\"1.2.3\"", clientHintsMap.get("sec-ch-ua-platform-version"));
Assert.assertEquals("\"x86_123\"", clientHintsMap.get("sec-ch-ua-arch"));
Assert.assertEquals("\"foo_model\"", clientHintsMap.get("sec-ch-ua-model"));
Assert.assertEquals("?1", clientHintsMap.get("sec-ch-ua-mobile"));
Assert.assertEquals("\"128\"", clientHintsMap.get("sec-ch-ua-bitness"));
Assert.assertEquals("?1", clientHintsMap.get("sec-ch-ua-wow64"));
Assert.assertEquals(
"\"Automotive\", \"Tablet\"", clientHintsMap.get("sec-ch-ua-form-factor"));
// Verify js client hints result.
JSONObject jsClientHints = clientHintsResult.mJsClientHints;
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"2\"}]",
jsClientHints.getString("brands"));
Assert.assertEquals(
"[{\"brand\":\"brand1\",\"version\":\"1.1.1\"},"
+ "{\"brand\":\"brand2\",\"version\":\"2.2.2\"}]",
jsClientHints.getString("fullVersionList"));
Assert.assertEquals("2.2.2", jsClientHints.getString("uaFullVersion"));
Assert.assertEquals("overrideTest", jsClientHints.getString("platform"));
Assert.assertEquals("1.2.3", jsClientHints.getString("platformVersion"));
Assert.assertEquals("x86_123", jsClientHints.getString("architecture"));
Assert.assertEquals("foo_model", jsClientHints.getString("model"));
Assert.assertTrue(jsClientHints.getBoolean("mobile"));
Assert.assertEquals("128", jsClientHints.getString("bitness"));
Assert.assertTrue(jsClientHints.getBoolean("wow64"));
Assert.assertEquals("[\"Automotive\",\"Tablet\"]", jsClientHints.getString("formFactor"));
}
private Map<String, Object> makeFakeMetadata() {
HashMap<String, Object> settings = new HashMap<>();
settings.put(
AwUserAgentMetadata.MetadataKeys.BRAND_VERSION_LIST,
new String[][] {{"brand1", "1", "1.1.1"}, {"brand2", "2", "2.2.2"}});
settings.put(AwUserAgentMetadata.MetadataKeys.FULL_VERSION, "2.2.2");
settings.put(AwUserAgentMetadata.MetadataKeys.PLATFORM, "overrideTest");
settings.put(AwUserAgentMetadata.MetadataKeys.PLATFORM_VERSION, "1.2.3");
settings.put(AwUserAgentMetadata.MetadataKeys.ARCHITECTURE, "x86_123");
settings.put(AwUserAgentMetadata.MetadataKeys.MODEL, "foo_model");
settings.put(AwUserAgentMetadata.MetadataKeys.MOBILE, true);
settings.put(AwUserAgentMetadata.MetadataKeys.BITNESS, 128);
settings.put(AwUserAgentMetadata.MetadataKeys.WOW64, true);
settings.put(
AwUserAgentMetadata.MetadataKeys.FORM_FACTOR,
new String[] {"Automotive", "Tablet"});
return settings;
}
private String getDefaultUserAgent() throws Throwable {
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(new TestAwContentsClient())
.getAwContents();
return mActivityTestRule.getAwSettingsOnUiThread(contents).getUserAgentString();
}
private ClientHintsTestResult getClientHintsWithOverrides(
Map<String, Object> uaMetadataOverrides, String overrideUserAgent) throws Throwable {
final TestAwContentsClient contentsClient = new TestAwContentsClient();
final AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentsClient)
.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
contents.getSettings().setJavaScriptEnabled(true);
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
if (uaMetadataOverrides != null) {
settings.setUserAgentMetadataFromMap(uaMetadataOverrides);
}
if (overrideUserAgent != null) {
settings.setUserAgentString(overrideUserAgent);
}
final AwEmbeddedTestServer server =
AwEmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getTargetContext());
ClientHintsTestResult clientHintsResult =
makeRequestAndGetClientHints(server, contents, contentsClient);
// Cleanup after test.
clearCookies();
server.stopAndDestroyServer();
return clientHintsResult;
}
private ClientHintsTestResult makeRequestAndGetClientHints(
final AwEmbeddedTestServer server,
final AwContents contents,
final TestAwContentsClient contentsClient)
throws Throwable {
final SettableFuture<String> highEntropyResultFuture = SettableFuture.create();
Object injectedObject =
new Object() {
@JavascriptInterface
public void setUserAgentClientHints(String ua) {
highEntropyResultFuture.set(ua);
}
};
AwActivityTestRule.addJavascriptInterfaceOnUiThread(
contents, injectedObject, "injectedObject");
final String url =
server.getURL(
"/client-hints-header?accept-ch="
+ String.join(",", USER_AGENT_CLIENT_HINTS));
// Load twice to be sure hints are returned, then parse the results.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
String textContent =
mActivityTestRule
.getJavaScriptResultBodyTextContent(contents, contentsClient)
.replaceAll("\\\\\"", "\"");
// Get client hints from HTTP request header.
HashMap<String, String> clientHintsMap = getClientHints(textContent);
// Get client hints from JS API.
JSUtils.executeJavaScriptAndWaitForResult(
InstrumentationRegistry.getInstrumentation(),
contents,
contentsClient.getOnEvaluateJavaScriptResultHelper(),
"navigator.userAgentData"
+ ".getHighEntropyValues(['architecture', 'bitness', 'brands', "
+ "'mobile', 'model', 'platform', 'platformVersion', 'uaFullVersion', "
+ "'fullVersionList', 'wow64', 'formFactor'])"
+ ".then(ua => { "
+ " injectedObject.setUserAgentClientHints(JSON.stringify(ua)); "
+ "})");
JSONObject jsonObject =
new JSONObject(AwActivityTestRule.waitForFuture(highEntropyResultFuture));
return new ClientHintsTestResult(clientHintsMap, jsonObject);
}
private void verifyClientHintBehavior(
final AwEmbeddedTestServer server,
final AwContents contents,
final TestAwContentsClient contentsClient,
boolean isSecure)
throws Throwable {
final String localhostURL =
server.getURL("/client-hints-header?accept-ch=sec-ch-device-memory");
final String fooURL =
server.getURLWithHostName(
"foo.test", "/client-hints-header?accept-ch=sec-ch-device-memory");
// First load of the localhost shouldn't have the hint as it wasn't requested before.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), localhostURL);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", false);
// Second load of the localhost does have the hint as it was persisted.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), localhostURL);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", true);
// Clearing cookies to clear out per-origin client hint preferences.
clearCookies();
// Third load of the localhost shouldn't have the hint as hint prefs were cleared.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), localhostURL);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", false);
// Fourth load of the localhost does have the hint as it was persisted.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), localhostURL);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", true);
// Fifth load of the localhost won't have the hint as JavaScript will be off.
contents.getSettings().setJavaScriptEnabled(false);
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), localhostURL);
contents.getSettings().setJavaScriptEnabled(true);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", false);
// First load of foo.test shouldn't have the hint as it wasn't requested before.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), fooURL);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", false);
// Second load of foo.test might have the hint if it the site is secure.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), fooURL);
validateHeadersFromJSON(contents, contentsClient, "sec-ch-device-memory", isSecure);
}
private void loadUrlSync(
final AwContents contents, CallbackHelper onPageFinishedHelper, final String url)
throws Throwable {
int currentCallCount = onPageFinishedHelper.getCallCount();
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> contents.loadUrl(url));
onPageFinishedHelper.waitForCallback(
currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
private void validateHeadersFromJSON(
final AwContents contents,
final TestAwContentsClient contentsClient,
String name,
boolean isPresent)
throws Throwable {
String textContent =
mActivityTestRule
.getJavaScriptResultBodyTextContent(contents, contentsClient)
.replaceAll("\\\\\"", "\"");
HashMap<String, String> clientHintsMap = getClientHints(textContent);
String actualVaue = clientHintsMap.get(name);
if (isPresent) {
Assert.assertNotEquals("HEADER_NOT_FOUND", actualVaue);
} else {
Assert.assertEquals("HEADER_NOT_FOUND", actualVaue);
}
}
private void clearCookies() throws Throwable {
CookieUtils.clearCookies(
InstrumentationRegistry.getInstrumentation(), new AwCookieManager());
}
private void verifyUserAgentOverrideClientHints(
final TestAwContentsClient contentsClient,
final AwContents contents,
String customUserAgent,
boolean expectHighEntropyClientHints)
throws Throwable {
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
contents.getSettings().setJavaScriptEnabled(true);
if (customUserAgent != null) {
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
settings.setUserAgentString(customUserAgent);
}
final AwEmbeddedTestServer server =
AwEmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getTargetContext());
final String url =
server.getURL(
"/client-hints-header?accept-ch="
+ String.join(",", USER_AGENT_CLIENT_HINTS));
// Load twice to be sure hints are returned, then parse the results.
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
loadUrlSync(contents, contentsClient.getOnPageFinishedHelper(), url);
String textContent =
mActivityTestRule
.getJavaScriptResultBodyTextContent(contents, contentsClient)
.replaceAll("\\\\\"", "\"");
// JSONObject can't support parsing client hint values (like sec-ch-ua) have quote("). We
// writes a custom parser function to approximately get the user-agent client hints in the
// content text.
HashMap<String, String> clientHintsMap = getClientHints(textContent);
if (expectHighEntropyClientHints) {
// All user-agent client hints should be in the request header.
for (String hint : USER_AGENT_CLIENT_HINTS) {
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get(hint));
}
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-mobile"));
Assert.assertEquals("\"Android\"", clientHintsMap.get("sec-ch-ua-platform"));
} else {
// Low-entropy client hints should be available.
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua"));
Assert.assertNotEquals("HEADER_NOT_FOUND", clientHintsMap.get("sec-ch-ua-mobile"));
Assert.assertEquals("\"Android\"", clientHintsMap.get("sec-ch-ua-platform"));
// High-entropy user-agent client hints should be empty.
Assert.assertEquals("\"\"", clientHintsMap.get("sec-ch-ua-platform-version"));
Assert.assertEquals("\"\"", clientHintsMap.get("sec-ch-ua-full-version"));
Assert.assertEquals("", clientHintsMap.get("sec-ch-ua-full-version-list"));
}
// Cleanup after test.
clearCookies();
server.stopAndDestroyServer();
}
private void verifyClientHintsJavaScript(boolean useCustomUserAgent) throws Throwable {
final TestAwContentsClient contentClient = new TestAwContentsClient();
AwContents contents =
mActivityTestRule
.createAwTestContainerViewOnMainSync(contentClient)
.getAwContents();
if (useCustomUserAgent) {
AwSettings settings = mActivityTestRule.getAwSettingsOnUiThread(contents);
settings.setUserAgentString("testCustomUserAgent");
}
AwActivityTestRule.enableJavaScriptOnUiThread(contents);
final SettableFuture<String> highEntropyResultFuture = SettableFuture.create();
Object injectedObject =
new Object() {
@JavascriptInterface
public void setUserAgentClientHints(String ua) {
highEntropyResultFuture.set(ua);
}
};
AwActivityTestRule.addJavascriptInterfaceOnUiThread(
contents, injectedObject, "injectedObject");
EmbeddedTestServer testServer =
EmbeddedTestServer.createAndStartHTTPSServer(
InstrumentationRegistry.getInstrumentation().getContext(),
ServerCertificate.CERT_OK);
try {
String targetUrl = testServer.getURL("/android_webview/test/data/client_hints.html");
loadUrlSync(contents, contentClient.getOnPageFinishedHelper(), targetUrl);
AwActivityTestRule.pollInstrumentationThread(
() -> !"running".equals(mActivityTestRule.getTitleOnUiThread(contents)));
String actualTitle = mActivityTestRule.getTitleOnUiThread(contents);
String[] uaItems = actualTitle.split("\\|");
// 3 navigator.userAgentData priorities.
int expect_total_hints = 3;
Assert.assertEquals(expect_total_hints, uaItems.length);
// System default low-entropy user-agent client hints will always be available in
// Javascript API even if users change the user-agent to a totally different value.
// Verify navigator.userAgentData.platform.
Assert.assertEquals("Android", uaItems[0]);
// Verify navigator.userAgentData.mobile.
Assert.assertFalse(uaItems[1].isEmpty());
// Verify navigator.userAgentData.brands.
Assert.assertFalse(uaItems[2].isEmpty());
Assert.assertTrue(uaItems[2].indexOf(ANDROID_WEBVIEW_BRAND_NAME) != -1);
JSUtils.executeJavaScriptAndWaitForResult(
InstrumentationRegistry.getInstrumentation(),
contents,
contentClient.getOnEvaluateJavaScriptResultHelper(),
"navigator.userAgentData"
+ ".getHighEntropyValues(['architecture', 'bitness', 'brands', "
+ "'mobile', 'model', 'platform', 'platformVersion', 'uaFullVersion', "
+ "'fullVersionList', 'wow64', 'formFactor'])"
+ ".then(ua => { "
+ " injectedObject.setUserAgentClientHints(JSON.stringify(ua)); "
+ "})");
JSONObject jsonObject =
new JSONObject(AwActivityTestRule.waitForFuture(highEntropyResultFuture));
// Verify getHighEntropyValues API.
Assert.assertEquals(USER_AGENT_CLIENT_HINTS.length, jsonObject.length());
if (useCustomUserAgent) {
// low-entropy client hints should be available.
// brands should not be empty.
String brands = jsonObject.getString("brands");
Assert.assertFalse(brands.isEmpty());
Assert.assertTrue(brands.indexOf(ANDROID_WEBVIEW_BRAND_NAME) != -1);
// mobile should not be empty.
Assert.assertFalse(jsonObject.getString("mobile").isEmpty());
// platform should return Android.
Assert.assertEquals("Android", jsonObject.getString("platform"));
// architecture is empty string.
Assert.assertTrue(jsonObject.getString("architecture").isEmpty());
// bitness is empty string.
Assert.assertTrue(jsonObject.getString("bitness").isEmpty());
// model is empty string.
Assert.assertTrue(jsonObject.getString("model").isEmpty());
// platformVersion is empty string.
Assert.assertTrue(jsonObject.getString("platformVersion").isEmpty());
// uaFullVersion is empty string.
Assert.assertTrue(jsonObject.getString("uaFullVersion").isEmpty());
// fullVersionList is empty list.
Assert.assertEquals("[]", jsonObject.getString("fullVersionList"));
// wow64 returns default value false.
Assert.assertFalse(jsonObject.getBoolean("wow64"));
} else {
// architecture is empty string on Android.
Assert.assertTrue(jsonObject.getString("architecture").isEmpty());
// bitness is empty string on Android.
Assert.assertTrue(jsonObject.getString("bitness").isEmpty());
// brands should not be empty.
String brands = jsonObject.getString("brands");
Assert.assertFalse(brands.isEmpty());
Assert.assertTrue(brands.indexOf(ANDROID_WEBVIEW_BRAND_NAME) != -1);
// mobile should not be empty.
Assert.assertFalse(jsonObject.getString("mobile").isEmpty());
// model should not be empty on Android.
Assert.assertFalse(jsonObject.getString("model").isEmpty());
// platform should return Android.
Assert.assertEquals("Android", jsonObject.getString("platform"));
// platformVersion should not be empty.
Assert.assertFalse(jsonObject.getString("platformVersion").isEmpty());
// uaFullVersion should not be empty.
Assert.assertFalse(jsonObject.getString("uaFullVersion").isEmpty());
// fullVersionList should not be empty.
String fullVersionList = jsonObject.getString("fullVersionList");
Assert.assertFalse(fullVersionList.isEmpty());
Assert.assertTrue(fullVersionList.indexOf(ANDROID_WEBVIEW_BRAND_NAME) != -1);
// wow64 returns false on Android.
Assert.assertFalse(jsonObject.getBoolean("wow64"));
}
} finally {
clearCookies();
testServer.stopAndDestroyServer();
}
}
/**
* WARNING: JSONObject can't support parsing client hint values (like sec-ch-ua) have quote(").
* Here is the a custom parser function to approximately get the user-agent client hints in the
* content text.
*/
private HashMap<String, String> getClientHints(String textContent) throws Throwable {
HashMap<String, String> result = new HashMap<>();
if (textContent == null || textContent.length() < 2) {
return result;
}
String text = textContent.substring(1, textContent.length() - 1);
// Instead of using comma as separator, we use `,"` to parser the input to get the client
// hints name and value pair. Some special case: "Sec-CH-UA": "Not/A)Brand";v="99", "Google
// Chrome";v="115","Sec-CH-UA-Platform": "macOS".
String[] hintPairs = text.split(",\"");
int userAgentClientHintsCount = 0;
for (String hintPair : hintPairs) {
// Make sure we only split into two parts at the first occurrence for `:` in order to
// handle correctly for cases when the brand value can contains special char `:`.
String[] hints = hintPair.split(":", 2);
if (hints.length < 2) {
continue;
}
// Since we use `,"` as the separator, the client hints name could start without
// quote(").
String clientHintName =
hints[0].startsWith("\"")
? hints[0].substring(1, hints[0].length() - 1)
: hints[0].substring(0, hints[0].length() - 1);
String clientHintValue = hints[1].substring(1, hints[1].length() - 1);
if (clientHintName.startsWith("sec-ch-ua")) {
userAgentClientHintsCount++;
}
result.put(clientHintName, clientHintValue);
}
// If you're here because this line broke, please update USER_AGENT_CLIENT_HINTS to include
// all the enabled user-agent client hints.
Assert.assertEquals(userAgentClientHintsCount, USER_AGENT_CLIENT_HINTS.length);
return result;
}
}