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() {