Add API to override user-agent metadata
Introduce a new API to let apps to override the user-agent metadata to populate the user-agent client hints.
Ensure it sends the right value apps override.
Bug: b/294183509
Test: :webkit:integration-tests:instrumentation:connectedAndroidTest
Change-Id: I37f309c5e9c1ea9628163b17f172c6cc5c4e65fa
diff --git a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebSettingsCompatUserAgentMetadataTest.java b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebSettingsCompatUserAgentMetadataTest.java
new file mode 100644
index 0000000..8ce6e03
--- /dev/null
+++ b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebSettingsCompatUserAgentMetadataTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+
+import android.os.Build;
+import android.webkit.WebSettings;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * TODO(b/294183509): Add user-agent client hints HTTP header verification tests when unhide the
+ * override user-agent metadata APIs.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+public class WebSettingsCompatUserAgentMetadataTest {
+ private WebViewOnUiThread mWebViewOnUiThread;
+
+ @Before
+ public void setUp() throws Exception {
+ mWebViewOnUiThread = new androidx.webkit.WebViewOnUiThread();
+ }
+
+ @After
+ public void tearDown() {
+ if (mWebViewOnUiThread != null) {
+ mWebViewOnUiThread.cleanUp();
+ }
+ }
+
+ @Test
+ public void testSetUserAgentMetadataDefault() throws Throwable {
+ WebkitUtils.checkFeature(WebViewFeature.USER_AGENT_METADATA);
+
+ WebSettings settings = mWebViewOnUiThread.getSettings();
+ UserAgentMetadata defaultSetting = WebSettingsCompat.getUserAgentMetadata(
+ settings);
+ // Check brand version list.
+ List<String> brands = new ArrayList<>();
+ Assert.assertNotNull(defaultSetting.getBrandVersionList());
+ for (UserAgentMetadata.BrandVersion bv : defaultSetting.getBrandVersionList()) {
+ brands.add(bv.getBrand());
+ }
+ Assert.assertTrue("The default brand should contains Android WebView.",
+ brands.contains("Android WebView"));
+ // Check platform, bitness and wow64.
+ assertEquals("The default platform is Android.", "Android",
+ defaultSetting.getPlatform());
+ assertEquals("The default bitness is 0.", UserAgentMetadata.BITNESS_DEFAULT,
+ defaultSetting.getBitness());
+ assertFalse("The default wow64 is false.", defaultSetting.isWow64());
+ }
+
+ @Test
+ public void testSetUserAgentMetadataFullOverrides() throws Throwable {
+ WebkitUtils.checkFeature(WebViewFeature.USER_AGENT_METADATA);
+
+ WebSettings settings = mWebViewOnUiThread.getSettings();
+ // Overrides user-agent metadata.
+ UserAgentMetadata overrideSetting = new UserAgentMetadata.Builder()
+ .setBrandVersionList(Collections.singletonList(
+ new UserAgentMetadata.BrandVersion(
+ "myBrand", "1", "1.1.1.1")))
+ .setFullVersion("1.1.1.1")
+ .setPlatform("myPlatform").setPlatformVersion("2.2.2.2").setArchitecture("myArch")
+ .setMobile(true).setModel("myModel").setBitness(32)
+ .setWow64(false).setFormFactor("myFormFactor").build();
+
+ WebSettingsCompat.setUserAgentMetadata(settings, overrideSetting);
+ assertEquals(
+ "After override set the user-agent metadata, it should be returned",
+ overrideSetting, WebSettingsCompat.getUserAgentMetadata(
+ settings));
+ }
+
+ @Test
+ public void testSetUserAgentMetadataPartialOverride() throws Throwable {
+ WebkitUtils.checkFeature(WebViewFeature.USER_AGENT_METADATA);
+
+ WebSettings settings = mWebViewOnUiThread.getSettings();
+ // Overrides without setting user-agent metadata platform and bitness.
+ UserAgentMetadata overrideSetting = new UserAgentMetadata.Builder()
+ .setBrandVersionList(Collections.singletonList(
+ new UserAgentMetadata.BrandVersion(
+ "myBrand", "1", "1.1.1.1")))
+ .setFullVersion("1.1.1.1")
+ .setPlatformVersion("2.2.2.2").setArchitecture("myArch").setMobile(true)
+ .setModel("myModel").setWow64(false).setFormFactor("myFormFactor").build();
+
+ WebSettingsCompat.setUserAgentMetadata(settings, overrideSetting);
+ UserAgentMetadata actualSetting = WebSettingsCompat.getUserAgentMetadata(
+ settings);
+ assertEquals("Platform should reset to system default if no overrides.",
+ "Android", actualSetting.getPlatform());
+ assertEquals("Bitness should reset to system default if no overrides.",
+ UserAgentMetadata.BITNESS_DEFAULT, actualSetting.getBitness());
+ assertEquals("FormFactor should be overridden value.",
+ "myFormFactor", WebSettingsCompat.getUserAgentMetadata(
+ settings).getFormFactor());
+ }
+
+ @Test
+ public void testSetUserAgentMetadataBlankBrandVersion() throws Throwable {
+ WebkitUtils.checkFeature(WebViewFeature.USER_AGENT_METADATA);
+
+ try {
+ WebSettings settings = mWebViewOnUiThread.getSettings();
+ UserAgentMetadata uaMetadata = new UserAgentMetadata.Builder()
+ .setBrandVersionList(Collections.singletonList(
+ new UserAgentMetadata.BrandVersion(
+ "", "", ""))).build();
+ WebSettingsCompat.setUserAgentMetadata(settings, uaMetadata);
+ Assert.fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("Brand name, major version and full version should not "
+ + "be blank.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testSetUserAgentMetadataEmptyBrandVersionList() throws Throwable {
+ WebkitUtils.checkFeature(WebViewFeature.USER_AGENT_METADATA);
+
+ try {
+ WebSettings settings = mWebViewOnUiThread.getSettings();
+ UserAgentMetadata uaMetadata = new UserAgentMetadata.Builder()
+ .setBrandVersionList(new ArrayList<>()).build();
+ WebSettingsCompat.setUserAgentMetadata(settings, uaMetadata);
+ Assert.fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("Brand version list should not be empty.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testSetUserAgentMetadataBlankFullVersion() throws Throwable {
+ WebkitUtils.checkFeature(WebViewFeature.USER_AGENT_METADATA);
+
+ try {
+ WebSettings settings = mWebViewOnUiThread.getSettings();
+ UserAgentMetadata uaMetadata = new UserAgentMetadata.Builder()
+ .setFullVersion(" ").build();
+ WebSettingsCompat.setUserAgentMetadata(settings, uaMetadata);
+ Assert.fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("Full version should not be blank.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testSetUserAgentMetadataBlankPlatform() throws Throwable {
+ WebkitUtils.checkFeature(WebViewFeature.USER_AGENT_METADATA);
+
+ try {
+ WebSettings settings = mWebViewOnUiThread.getSettings();
+ UserAgentMetadata uaMetadata = new UserAgentMetadata.Builder()
+ .setPlatform(" ").build();
+ WebSettingsCompat.setUserAgentMetadata(settings, uaMetadata);
+ Assert.fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("Platform should not be blank.", e.getMessage());
+ }
+ }
+}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/UserAgentMetadata.java b/webkit/webkit/src/main/java/androidx/webkit/UserAgentMetadata.java
new file mode 100644
index 0000000..cbcc3d3
--- /dev/null
+++ b/webkit/webkit/src/main/java/androidx/webkit/UserAgentMetadata.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Holds user-agent metadata information and uses to generate user-agent client
+ * hints.
+ * <p>
+ * This class is functionally equivalent to
+ * <a href="https://wicg.github.io/ua-client-hints/#interface">UADataValues</a>.
+ * <p>
+ * TODO(b/294183509): unhide
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class UserAgentMetadata {
+ /**
+ * Use this value for bitness to use the platform's default bitness value, which is an empty
+ * string for Android WebView.
+ */
+ public static final int BITNESS_DEFAULT = 0;
+
+ private final List<BrandVersion> mBrandVersionList;
+
+ private final String mFullVersion;
+ private final String mPlatform;
+ private final String mPlatformVersion;
+ private final String mArchitecture;
+ private final String mModel;
+ private boolean mMobile = true;
+ private int mBitness = BITNESS_DEFAULT;
+ private boolean mWow64 = false;
+ private final String mFormFactor;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ private UserAgentMetadata(@Nullable List<BrandVersion> brandVersionList,
+ @Nullable String fullVersion, @Nullable String platform,
+ @Nullable String platformVersion, @Nullable String architecture,
+ @Nullable String model,
+ boolean mobile,
+ int bitness, boolean wow64, @Nullable String formFactor) {
+ mBrandVersionList = brandVersionList;
+ mFullVersion = fullVersion;
+ mPlatform = platform;
+ mPlatformVersion = platformVersion;
+ mArchitecture = architecture;
+ mModel = model;
+ mMobile = mobile;
+ mBitness = bitness;
+ mWow64 = wow64;
+ mFormFactor = formFactor;
+ }
+
+ /**
+ * Returns the current list of user-agent brand versions which are used to populate
+ * user-agent client hints {@code sec-ch-ua} and {@code sec-ch-ua-full-version-list}. Each
+ * {@link BrandVersion} object holds the brand name, brand major version and brand
+ * full version.
+ * <p>
+ * @see Builder#setBrandVersionList
+ *
+ */
+ @Nullable
+ public List<BrandVersion> getBrandVersionList() {
+ return mBrandVersionList;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-full-version} client hint.
+ * <p>
+ * @see Builder#setFullVersion
+ *
+ */
+ @Nullable
+ public String getFullVersion() {
+ return mFullVersion;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-platform} client hint.
+ * <p>
+ * @see Builder#setPlatform
+ *
+ */
+ @Nullable
+ public String getPlatform() {
+ return mPlatform;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-platform-version} client hint.
+ * <p>
+ * @see Builder#setPlatformVersion
+ *
+ * @return Platform version string.
+ */
+ @Nullable
+ public String getPlatformVersion() {
+ return mPlatformVersion;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-arch} client hint.
+ * <p>
+ * @see Builder#setArchitecture
+ *
+ */
+ @Nullable
+ public String getArchitecture() {
+ return mArchitecture;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-model} client hint.
+ * <p>
+ * @see Builder#setModel
+ *
+ */
+ @Nullable
+ public String getModel() {
+ return mModel;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-mobile} client hint.
+ * <p>
+ * @see Builder#setMobile
+ *
+ * @return A boolean indicates user-agent's device mobileness.
+ */
+ public boolean isMobile() {
+ return mMobile;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-bitness} client hint.
+ * <p>
+ * @see Builder#setBitness
+ *
+ * @return An integer indicates the CPU bitness, the integer value will convert to string
+ * when generating the user-agent client hint, and {@link UserAgentMetadata#BITNESS_DEFAULT}
+ * means an empty string.
+ */
+ public int getBitness() {
+ return mBitness;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-wow64} client hint.
+ * <p>
+ * @see Builder#setWow64
+ *
+ * @return A boolean to indicate whether user-agent's binary is running in 64-bit Windows.
+ */
+ public boolean isWow64() {
+ return mWow64;
+ }
+
+ /**
+ * Returns the value for the {@code sec-ch-ua-form-factor} client hint.
+ * <p>
+ * @see Builder#setFormFactor
+ *
+ */
+ @Nullable
+ public String getFormFactor() {
+ return mFormFactor;
+ }
+
+ /**
+ * Two UserAgentMetadata objects are equal only if all the metadata values are equal.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof UserAgentMetadata)) return false;
+ UserAgentMetadata that = (UserAgentMetadata) o;
+ return mMobile == that.mMobile && mBitness == that.mBitness && mWow64 == that.mWow64
+ && Objects.equals(mBrandVersionList, that.mBrandVersionList)
+ && Objects.equals(mFullVersion, that.mFullVersion)
+ && Objects.equals(mPlatform, that.mPlatform) && Objects.equals(
+ mPlatformVersion, that.mPlatformVersion) && Objects.equals(mArchitecture,
+ that.mArchitecture) && Objects.equals(mModel, that.mModel)
+ && Objects.equals(mFormFactor, that.mFormFactor);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mBrandVersionList, mFullVersion, mPlatform, mPlatformVersion,
+ mArchitecture, mModel, mMobile, mBitness, mWow64, mFormFactor);
+ }
+
+ /**
+ * Class that holds brand name, major version and full version. Brand name and major version
+ * used to generated user-agent client hint {@code sec-cu-ua}. Brand name and full version
+ * used to generated user-agent client hint {@code sec-ch-ua-full-version-list}.
+ * <p>
+ * This class is functionally equivalent to
+ * <a href="https://wicg.github.io/ua-client-hints/#interface">NavigatorUABrandVersion</a>.
+ *
+ */
+ public static class BrandVersion {
+ private final String mBrand;
+ private final String mMajorVersion;
+ private final String mFullVersion;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public BrandVersion(@NonNull String brand, @NonNull String majorVersion,
+ @NonNull String fullVersion) {
+ if (brand.trim().isEmpty() || majorVersion.trim().isEmpty()
+ || fullVersion.trim().isEmpty()) {
+ throw new IllegalArgumentException("Brand name, major version and full version "
+ + "should not be blank.");
+ }
+ mBrand = brand;
+ mMajorVersion = majorVersion;
+ mFullVersion = fullVersion;
+ }
+
+ /**
+ * Returns the brand of user-agent brand version tuple.
+ *
+ */
+ @NonNull
+ public String getBrand() {
+ return mBrand;
+ }
+
+ /**
+ * Returns the major version of user-agent brand version tuple.
+ *
+ */
+ @NonNull
+ public String getMajorVersion() {
+ return mMajorVersion;
+ }
+
+ /**
+ * Returns the full version of user-agent brand version tuple.
+ *
+ */
+ @NonNull
+ public String getFullVersion() {
+ return mFullVersion;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mBrand + "," + mMajorVersion + "," + mFullVersion;
+ }
+
+ /**
+ * Two BrandVersion objects are equal only if brand name, major version and full version
+ * are equal.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BrandVersion)) return false;
+ BrandVersion that = (BrandVersion) o;
+ return Objects.equals(mBrand, that.mBrand) && Objects.equals(mMajorVersion,
+ that.mMajorVersion) && Objects.equals(mFullVersion, that.mFullVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mBrand, mMajorVersion, mFullVersion);
+ }
+ }
+
+ /**
+ * Builder used to create {@link UserAgentMetadata} objects.
+ * <p>
+ * Examples:
+ * <pre class="prettyprint">
+ * // Create a setting with default options.
+ * new UserAgentMetadata.Builder().build();
+ *
+ * // Create a setting with a brand version contains brand name: myBrand, major version: 100,
+ * // full version: 100.1.1.1.
+ * new UserAgentMetadata.Builder().setBrandVersionList(
+ * Collections.singletonList(new BrandVersion("myBrand", "100", "100.1.1.1"))).build();
+ *
+ * // Create a setting brand version, platform, platform version and bitness.
+ * new UserAgentMetadata.Builder().setBrandVersionList(
+ * Collections.singletonList(new BrandVersion("myBrand", "100", "100.1.1.1")))
+ * .setPlatform("myPlatform")
+ * .setPlatform("1.1.1.1")
+ * .setBitness(BITNESS_64)
+ * .build();
+ * </pre>
+ */
+ public static final class Builder {
+ private List<BrandVersion> mBrandVersionList;
+ private String mFullVersion;
+ private String mPlatform;
+ private String mPlatformVersion;
+ private String mArchitecture;
+ private String mModel;
+ private boolean mMobile = true;
+ private int mBitness = BITNESS_DEFAULT;
+ private boolean mWow64 = false;
+ private String mFormFactor;
+
+ /**
+ * Create an empty UserAgentMetadata Builder.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Create a UserAgentMetadata Builder from an existing UserAgentMetadata object.
+ */
+ public Builder(@NonNull UserAgentMetadata uaMetadata) {
+ mBrandVersionList = uaMetadata.getBrandVersionList();
+ mFullVersion = uaMetadata.getFullVersion();
+ mPlatform = uaMetadata.getPlatform();
+ mPlatformVersion = uaMetadata.getPlatformVersion();
+ mArchitecture = uaMetadata.getArchitecture();
+ mModel = uaMetadata.getModel();
+ mMobile = uaMetadata.isMobile();
+ mBitness = uaMetadata.getBitness();
+ mWow64 = uaMetadata.isWow64();
+ mFormFactor = uaMetadata.getFormFactor();
+ }
+
+ /**
+ * Builds the current settings into a UserAgentMetadata object.
+ *
+ * @return The UserAgentMetadata object represented by this Builder
+ */
+ @NonNull
+ public UserAgentMetadata build() {
+ return new UserAgentMetadata(mBrandVersionList, mFullVersion, mPlatform,
+ mPlatformVersion, mArchitecture, mModel, mMobile, mBitness, mWow64,
+ mFormFactor);
+ }
+
+ /**
+ * Sets user-agent metadata brands and their versions. The brand name, major version and
+ * full version should not be blank.
+ *
+ * @param brandVersions a list of {@link BrandVersion} used to generated user-agent client
+ * hints {@code sec-cu-ua} and {@code sec-ch-ua-full-version-list}.
+ *
+ */
+ @NonNull
+ public Builder setBrandVersionList(@NonNull List<BrandVersion> brandVersions) {
+ if (brandVersions.isEmpty()) {
+ throw new IllegalArgumentException("Brand version list should not be empty.");
+ }
+ mBrandVersionList = brandVersions;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata full version. The full version should not be blank, even
+ * though the <a href="https://wicg.github.io/ua-client-hints">spec<a/> about brand full
+ * version could be empty. The blank full version could cause inconsistent brands when
+ * generating brand version related user-agent client hints. It also gives bad experience
+ * for developers when processing the brand full version.
+ *
+ * @param fullVersion The full version is used to generate user-agent client hint
+ * {@code sec-ch-ua-full-version}.
+ *
+ */
+ @NonNull
+ public Builder setFullVersion(@NonNull String fullVersion) {
+ if (fullVersion.trim().isEmpty()) {
+ throw new IllegalArgumentException("Full version should not be blank.");
+ }
+ mFullVersion = fullVersion;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata platform. The platform should not be blank.
+ *
+ * @param platform The platform is used to generate user-agent client hint
+ * {@code sec-ch-ua-platform}.
+ *
+ */
+ @NonNull
+ public Builder setPlatform(@NonNull String platform) {
+ if (platform.trim().isEmpty()) {
+ throw new IllegalArgumentException("Platform should not be blank.");
+ }
+ mPlatform = platform;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata platform version. The value should not be null but can be
+ * empty string.
+ *
+ * @param platformVersion The platform version is used to generate user-agent client
+ * hint {@code sec-ch-ua-platform-version}.
+ *
+ */
+ @NonNull
+ public Builder setPlatformVersion(@NonNull String platformVersion) {
+ mPlatformVersion = platformVersion;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata architecture. The value should not be null but can be
+ * empty string.
+ *
+ * @param architecture The architecture is used to generate user-agent client hint
+ * {@code sec-ch-ua-arch}.
+ *
+ */
+ @NonNull
+ public Builder setArchitecture(@NonNull String architecture) {
+ mArchitecture = architecture;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata model. The value should not be null but can be empty string.
+ *
+ * @param model The model is used to generate user-agent client hint
+ * {@code sec-ch-ua-model}.
+ *
+ */
+ @NonNull
+ public Builder setModel(@NonNull String model) {
+ mModel = model;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata mobile, the default value is true.
+ *
+ * @param mobile The mobile is used to generate user-agent client hint
+ * {@code sec-ch-ua-mobile}.
+ *
+ */
+ @NonNull
+ public Builder setMobile(boolean mobile) {
+ mMobile = mobile;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata bitness, the default value is
+ * {@link UserAgentMetadata#BITNESS_DEFAULT}, which indicates an empty string for
+ * {@code sec-ch-ua-bitness}.
+ *
+ * @param bitness The bitness is used to generate user-agent client hint
+ * {@code sec-ch-ua-bitness}.
+ *
+ */
+ @NonNull
+ public Builder setBitness(int bitness) {
+ mBitness = bitness;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata wow64, the default is false.
+ *
+ * @param wow64 The wow64 is used to generate user-agent client hint
+ * {@code sec-ch-ua-wow64}.
+ *
+ */
+ @NonNull
+ public Builder setWow64(boolean wow64) {
+ mWow64 = wow64;
+ return this;
+ }
+
+ /**
+ * Sets the user-agent metadata form factor. The value should not be null but can be
+ * empty string.
+ *
+ * @param formFactor The form factor is used to generate user-agent client hint
+ * {@code sec-ch-ua-form-factor}.
+ *
+ */
+ @NonNull
+ public Builder setFormFactor(@NonNull String formFactor) {
+ mFormFactor = formFactor;
+ return this;
+ }
+ }
+}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index f50fb6a..b447b9d 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -697,6 +697,78 @@
}
}
+ /**
+ * Sets the WebView's user-agent metadata to generate user-agent client hints.
+ * <p>
+ * UserAgentMetadata in WebView is used to populate user-agent client hints, they can provide
+ * the client’s branding and version information, the underlying operating system’s branding
+ * and major version, as well as details about the underlying device.
+ * <p>
+ * The user-agent string can be set with {@link WebSettings#setUserAgentString(String)}, here
+ * are the details on how this API interacts it to generate user-agent client hints.
+ * <p>
+ * If the UserAgentMetadata is null and the overridden user-agent contains the system default
+ * user-agent, the system default value will be used.
+ * <p>
+ * If the UserAgentMetadata is null but the overridden user-agent doesn't contain the system
+ * default user-agent, only the
+ * <a href="https://wicg.github.io/client-hints-infrastructure/#low-entropy-hint-table">low-entry user-agent client hints</a> will be generated.
+ *
+ * <p> See <a href="https://wicg.github.io/ua-client-hints/">
+ * this</a> for more information about User-Agent Client Hints.
+ *
+ * <p>
+ * This method should only be called if
+ * {@link WebViewFeature#isFeatureSupported(String)}
+ * returns true for {@link WebViewFeature#USER_AGENT_METADATA}.
+ *
+ * @param metadata the WebView's user-agent metadata.
+ *
+ * TODO(b/294183509): unhide
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresFeature(name = WebViewFeature.USER_AGENT_METADATA,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public static void setUserAgentMetadata(@NonNull WebSettings settings,
+ @NonNull UserAgentMetadata metadata) {
+ final ApiFeature.NoFramework feature =
+ WebViewFeatureInternal.USER_AGENT_METADATA;
+ if (feature.isSupportedByWebView()) {
+ getAdapter(settings).setUserAgentMetadata(metadata);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Get the WebView's user-agent metadata which used to generate user-agent client hints.
+ *
+ * <p> See <a href="https://wicg.github.io/ua-client-hints/"> this</a> for more information
+ * about User-Agent Client Hints.
+ *
+ * <p>
+ * This method should only be called if
+ * {@link WebViewFeature#isFeatureSupported(String)}
+ * returns true for {@link WebViewFeature#USER_AGENT_METADATA}.
+ *
+ * TODO(b/294183509): unhide
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresFeature(name = WebViewFeature.USER_AGENT_METADATA,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ @NonNull
+ public static UserAgentMetadata getUserAgentMetadata(@NonNull WebSettings settings) {
+ final ApiFeature.NoFramework feature =
+ WebViewFeatureInternal.USER_AGENT_METADATA;
+ if (feature.isSupportedByWebView()) {
+ return getAdapter(settings).getUserAgentMetadata();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
private static WebSettingsAdapter getAdapter(WebSettings settings) {
return WebViewGlueCommunicator.getCompatConverter().convertSettings(settings);
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 87f4b9e..c697ecd 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -102,6 +102,7 @@
ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY,
GET_COOKIE_INFO,
REQUESTED_WITH_HEADER_ALLOW_LIST,
+ USER_AGENT_METADATA,
})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -534,6 +535,18 @@
"REQUESTED_WITH_HEADER_ALLOW_LIST";
/**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getUserAgentMetadata(WebSettings)}, and
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setUserAgentMetadata(WebSettings, UserAgentMetadata)}.
+ *
+ * TODO(b/294183509): unhide
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final String USER_AGENT_METADATA = "USER_AGENT_METADATA";
+
+ /**
* Return whether a feature is supported at run-time. On devices running Android version {@link
* android.os.Build.VERSION_CODES#LOLLIPOP} and higher, this will check whether a feature is
* supported, depending on the combination of the desired feature, the Android version of
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/UserAgentMetadataInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/UserAgentMetadataInternal.java
new file mode 100644
index 0000000..96beee3
--- /dev/null
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/UserAgentMetadataInternal.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import androidx.annotation.NonNull;
+import androidx.webkit.UserAgentMetadata;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Internal implementation of translation between {@code Map<String, Object>} and
+ * {@link androidx.webkit.UserAgentMetadata}.
+ */
+public class UserAgentMetadataInternal {
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata mobile,
+ * used to generate user-agent client hint {@code sec-ch-ua-mobile}.
+ */
+ private static final String MOBILE = "MOBILE";
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata brand version list,
+ * used to generate user-agent client hints {@code sec-ch-ua}, and
+ * {@code sec-ch-ua-full-version-list}.
+ */
+ private static final String BRAND_VERSION_LIST = "BRAND_VERSION_LIST";
+
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata full version,
+ * used to generate user-agent client hint {@code sec-ch-ua-full-version}.
+ */
+ private static final String FULL_VERSION = "FULL_VERSION";
+
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata platform,
+ * used to generate user-agent client hint {@code sec-ch-ua-platform}.
+ */
+ private static final String PLATFORM = "PLATFORM";
+
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata platform version,
+ * used to generate user-agent client hint {@code sec-ch-ua-platform-version}.
+ */
+ private static final String PLATFORM_VERSION = "PLATFORM_VERSION";
+
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata architecture,
+ * used to generate user-agent client hint {@code sec-ch-ua-arch}.
+ */
+ private static final String ARCHITECTURE = "ARCHITECTURE";
+
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata model,
+ * used to generate user-agent client hint {@code sec-ch-ua-model}.
+ */
+ private static final String MODEL = "MODEL";
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata bitness,
+ * used to generate user-agent client hint {@code sec-ch-ua-bitness}.
+ */
+ private static final String BITNESS = "BITNESS";
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata wow64,
+ * used to generate user-agent client hint {@code sec-ch-ua-wow64}.
+ */
+ private static final String WOW64 = "WOW64";
+ /**
+ * Predefined set of name for user-agent metadata key.
+ * Key name for user-agent metadata form_factor,
+ * used to generate user-agent client hint {@code sec-ch-ua-form-factor}.
+ */
+ private static final String FORM_FACTOR = "FORM_FACTOR";
+ /**
+ * each brand should contains brand, major version and full version.
+ */
+ private static final int BRAND_VERSION_LENGTH = 3;
+
+ /**
+ * Convert the UserAgentMetadata setting to a map of object and pass down to chromium.
+ *
+ * @return A hashmap contains user-agent metadata key name, and corresponding objects.
+ */
+ @NonNull
+ static Map<String, Object> convertUserAgentMetadataToMap(
+ @NonNull UserAgentMetadata uaMetadata) {
+ Map<String, Object> item = new HashMap<>();
+ item.put(BRAND_VERSION_LIST, getBrandVersionArray(uaMetadata.getBrandVersionList()));
+ item.put(FULL_VERSION, uaMetadata.getFullVersion());
+ item.put(PLATFORM, uaMetadata.getPlatform());
+ item.put(PLATFORM_VERSION, uaMetadata.getPlatformVersion());
+ item.put(ARCHITECTURE, uaMetadata.getArchitecture());
+ item.put(MODEL, uaMetadata.getModel());
+ item.put(MOBILE, uaMetadata.isMobile());
+ item.put(BITNESS, uaMetadata.getBitness());
+ item.put(WOW64, uaMetadata.isWow64());
+ item.put(FORM_FACTOR, uaMetadata.getFormFactor());
+ return item;
+ }
+
+ private static String[][] getBrandVersionArray(
+ List<UserAgentMetadata.BrandVersion> brandVersionList) {
+ if (brandVersionList == null) {
+ return null;
+ }
+
+ String[][] brandVersionArray = new String[brandVersionList.size()][BRAND_VERSION_LENGTH];
+ for (int i = 0; i < brandVersionList.size(); i++) {
+ brandVersionArray[i][0] = brandVersionList.get(i).getBrand();
+ brandVersionArray[i][1] = brandVersionList.get(i).getMajorVersion();
+ brandVersionArray[i][2] = brandVersionList.get(i).getFullVersion();
+ }
+ return brandVersionArray;
+ }
+
+ /**
+ * Convert a map of object to an instance of UserAgentMetadata.
+ *
+ * @param uaMetadataMap A hashmap contains user-agent metadata key name, and corresponding
+ * objects.
+ * @return This UserAgentMetadata object
+ */
+ @NonNull
+ static UserAgentMetadata getUserAgentMetadataFromMap(
+ @NonNull Map<String, Object> uaMetadataMap) {
+ UserAgentMetadata.Builder builder = new UserAgentMetadata.Builder();
+
+ Object brandVersionValue = uaMetadataMap.get(BRAND_VERSION_LIST);
+ if (brandVersionValue != null) {
+ String[][] overrideBrandVersionList = (String[][]) brandVersionValue;
+ List<UserAgentMetadata.BrandVersion> branVersionList = new ArrayList<>();
+ for (String[] brandVersionInfo : overrideBrandVersionList) {
+ branVersionList.add(new UserAgentMetadata.BrandVersion(brandVersionInfo[0],
+ brandVersionInfo[1], brandVersionInfo[2]));
+ }
+ builder.setBrandVersionList(branVersionList);
+ }
+
+ String fullVersion = (String) uaMetadataMap.get(FULL_VERSION);
+ if (fullVersion != null) {
+ builder.setFullVersion(fullVersion);
+ }
+
+ String platform = (String) uaMetadataMap.get(PLATFORM);
+ if (platform != null) {
+ builder.setPlatform(platform);
+ }
+
+ String platformVersion = (String) uaMetadataMap.get(PLATFORM_VERSION);
+ if (platformVersion != null) {
+ builder.setPlatformVersion(platformVersion);
+ }
+
+ String architecture = (String) uaMetadataMap.get(ARCHITECTURE);
+ if (architecture != null) {
+ builder.setArchitecture(architecture);
+ }
+
+ String model = (String) uaMetadataMap.get(MODEL);
+ if (model != null) {
+ builder.setModel(model);
+ }
+
+ Boolean isMobile = (Boolean) uaMetadataMap.get(MOBILE);
+ if (isMobile != null) {
+ builder.setMobile(isMobile);
+ }
+
+ Integer bitness = (Integer) uaMetadataMap.get(BITNESS);
+ if (bitness != null) {
+ builder.setBitness(bitness);
+ }
+
+ Boolean isWow64 = (Boolean) uaMetadataMap.get(WOW64);
+ if (isWow64 != null) {
+ builder.setWow64(isWow64);
+ }
+
+ String formFactor = (String) uaMetadataMap.get(FORM_FACTOR);
+ if (formFactor != null) {
+ builder.setFormFactor(formFactor);
+ }
+ return builder.build();
+ }
+}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
index 041647d..6fb6231 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
@@ -19,6 +19,7 @@
import android.webkit.WebSettings;
import androidx.annotation.NonNull;
+import androidx.webkit.UserAgentMetadata;
import org.chromium.support_lib_boundary.WebSettingsBoundaryInterface;
@@ -153,4 +154,24 @@
public void setRequestedWithHeaderOriginAllowList(@NonNull Set<String> allowList) {
mBoundaryInterface.setRequestedWithHeaderOriginAllowList(allowList);
}
+
+ /**
+ * Adapter method for
+ * {@link androidx.webkit.WebSettingsCompat#getUserAgentMetadata(WebSettings)}.
+ */
+ @NonNull
+ public UserAgentMetadata getUserAgentMetadata() {
+ return UserAgentMetadataInternal.getUserAgentMetadataFromMap(
+ mBoundaryInterface.getUserAgentMetadataMap());
+ }
+
+ /**
+ * Adapter method for
+ * {@link androidx.webkit.WebSettingsCompat#setUserAgentMetadata(
+ * WebSettings, UserAgentMetadata)}.
+ */
+ public void setUserAgentMetadata(@NonNull UserAgentMetadata uaMetadata) {
+ mBoundaryInterface.setUserAgentMetadataFromMap(
+ UserAgentMetadataInternal.convertUserAgentMetadataToMap(uaMetadata));
+ }
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index 668dac6..a18ff3d 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -530,6 +530,16 @@
public static final ApiFeature.NoFramework REQUESTED_WITH_HEADER_ALLOW_LIST =
new ApiFeature.NoFramework(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
Features.REQUESTED_WITH_HEADER_ALLOW_LIST);
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.WebSettingsCompat#setUserAgentMetadata(WebSettings, UserAgentMetadata)} and
+ * {@link androidx.webkit.WebSettingsCompat#getUserAgentMetadata(WebSettings)}.
+ *
+ */
+ public static final ApiFeature.NoFramework USER_AGENT_METADATA =
+ new ApiFeature.NoFramework(WebViewFeature.USER_AGENT_METADATA,
+ Features.USER_AGENT_METADATA);
// --- Add new feature constants above this line ---
private WebViewFeatureInternal() {