diff --git a/DEPS b/DEPS
index 3aaa010..2747ac8a 100644
--- a/DEPS
+++ b/DEPS
@@ -308,11 +308,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'src_internal_revision': '3dff8792f0d51d1790b512505334cdc668e54606',
+  'src_internal_revision': 'f50f32ae0208beb1302fec22582ab6c8d36ba5eb',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': 'b40fdf12342f60a32930e1cc5758380d0c786756',
+  'skia_revision': '293de35a9d1e046bf919f32ec6eb693f82ee4df9',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -320,7 +320,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': '67fc293ab0b33e82cc151286f22d55a0781e9e86',
+  'angle_revision': 'e229afada1d2af012b7ec55395d10f53f1ebfe60',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -328,7 +328,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '2c66e07e9c3b52feee720c03aa11984895f09803',
+  'pdfium_revision': 'fde20e170bebdde902d38dd577a0543e11b6d4d4',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # and whatever else without interference from each other.
@@ -347,7 +347,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling googletest
   # and whatever else without interference from each other.
-  'googletest_revision': 'b1a777f31913f8a047f43b2a5f823e736e7f5082',
+  'googletest_revision': '5197b1a8e6a1ef9f214f4aa537b0be17cbf91946',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling lighttpd
   # and whatever else without interference from each other.
@@ -387,7 +387,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling chromium_variations
   # and whatever else without interference from each other.
-  'chromium_variations_revision': '9bbacc037e69792d5f5c70a389ce780e35b71f29',
+  'chromium_variations_revision': '5951e69a2333e36fbd7912bd160f4686347e6c5f',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling CrossBench
   # and whatever else without interference from each other.
@@ -427,7 +427,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '5864d1bef534d0e54f64d489c865db7f76deb2fe',
+  'dawn_revision': '37f756f60fb233f9fa1d622c3fe277816baab4cc',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -827,7 +827,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    '29f78538308b953b929c2d0fa89c036d889ad135',
+    'f022452f20012a03b32a2db96eeda986d644287f',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -982,7 +982,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'xqtv9T9O_VlqI8q1LyR_YXaaVp40OQok0fUUuiXgGPAC',
+          'version': 'W0fbeG8jMjuHI7-r6nQ8WsaiVJB9Jk4koL6eqkKa-OwC',
       },
     ],
     'condition': 'checkout_android',
@@ -1192,13 +1192,13 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '8f6d774a8d8ddcd5a4dc6e8aac06a35576c2b113',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '609288a46b37758cbbfd82aefc01359631aec81f',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
 
   'src/third_party/devtools-frontend-internal': {
-      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + '5674bb7b0c13888a0222377953c082b1abf53fbd',
+      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + '1c8f3583f1e10d91097074d41aba0b8f78837ae1',
     'condition': 'checkout_src_internal',
   },
 
@@ -1851,7 +1851,7 @@
     Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'eaa860a0b3a9049d2caf88cb1ce9d7fb82fb05f2',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '83a1c92bb409cb16dfa2cb60f7f3064db388aca6',
+    Var('webrtc_git') + '/src.git' + '@' + 'f2cdbc9b0717e67821826f7ff93ce7fe0e9485c3',
 
   # Wuffs' canonical repository is at github.com/google/wuffs, but we use
   # Skia's mirror of Wuffs, the same as in upstream Skia's DEPS file.
@@ -2018,7 +2018,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/projector_app/app',
-        'version': '922v3fUxqABuiuLoOKxgLPXgjnE9ZZWSMsBsTXVNzNEC',
+        'version': 'ds89jlakvYT-oXHipZIUSEQz8MgH15qf0_dcDINkHzIC',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
@@ -3935,7 +3935,7 @@
 
   'src/chrome/browser/platform_experience/win': {
       'url': Var('chrome_git') + '/chrome/browser/platform_experience/win.git' + '@' +
-        '065a550502a2de7532a2b3d8fb926af407611ab4',
+        'e2261501b3a54bb01cd879dfc0d9297a9a26aa0e',
       'condition': 'checkout_src_internal',
   },
 
@@ -4125,7 +4125,7 @@
 
   'src/ios_internal':  {
       'url': Var('chrome_git') + '/chrome/ios_internal.git' + '@' +
-        'b52f647176b2a69101f532235ff01e26a03708c2',
+        'a5aea7848cb458595e3fc715475154bc54ca53cd',
       'condition': 'checkout_ios and checkout_src_internal',
   },
 
diff --git a/android_webview/browser/aw_field_trials.cc b/android_webview/browser/aw_field_trials.cc
index c2759dd4..ed2af211 100644
--- a/android_webview/browser/aw_field_trials.cc
+++ b/android_webview/browser/aw_field_trials.cc
@@ -111,7 +111,7 @@
 
   // Disable Shared Storage on WebView.
   aw_feature_overrides.DisableFeature(blink::features::kSharedStorageAPI);
-  aw_feature_overrides.DisableFeature(blink::features::kSharedStorageAPIM124);
+  aw_feature_overrides.DisableFeature(blink::features::kSharedStorageAPIM125);
 
   // Disable scrollbar-color on WebView.
   aw_feature_overrides.DisableFeature(blink::features::kScrollbarColor);
diff --git a/android_webview/browser/safe_browsing/aw_ping_manager_factory.cc b/android_webview/browser/safe_browsing/aw_ping_manager_factory.cc
index 103e79c..bd40ecb8 100644
--- a/android_webview/browser/safe_browsing/aw_ping_manager_factory.cc
+++ b/android_webview/browser/safe_browsing/aw_ping_manager_factory.cc
@@ -39,6 +39,10 @@
   // Never fetch the access token for android_webview since ESB is unsupported
   auto get_should_fetch_access_token =
       base::BindRepeating([]() { return false; });
+  // Persisted report is not supported on WebView, because only download reports
+  // are persisted and WebView doesn't have download protection.
+  auto get_should_send_persisted_report =
+      base::BindRepeating([]() { return false; });
   return PingManager::Create(
       safe_browsing::GetV4ProtocolConfig(GetProtocolConfigClientName(),
                                          /*disable_auto_update=*/false),
@@ -51,7 +55,9 @@
       // threading the user population through for client reports
       /*get_user_population_callback=*/base::NullCallback(),
       /*get_page_load_token_callback_=*/base::NullCallback(),
-      /*hats_delegate=*/nullptr, /*persister_root_path=*/context->GetPath());
+      /*hats_delegate=*/nullptr, /*persister_root_path=*/context->GetPath(),
+      /*get_should_send_persisted_report=*/
+      std::move(get_should_send_persisted_report));
 }
 
 std::string AwPingManagerFactory::GetProtocolConfigClientName() const {
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
index 6c1d1ff..fe5452b 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
@@ -26,6 +26,7 @@
 
 import org.chromium.android_webview.AwContents;
 import org.chromium.android_webview.AwContentsClient;
+import org.chromium.android_webview.AwContentsClient.AwWebResourceRequest;
 import org.chromium.android_webview.AwSettings;
 import org.chromium.android_webview.policy.AwPolicyProvider;
 import org.chromium.android_webview.test.TestAwContentsClient.OnReceivedErrorHelper;
@@ -35,6 +36,7 @@
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.CriteriaNotSatisfiedException;
+import org.chromium.base.test.util.DoNotBatch;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.MinAndroidSdkLevel;
 import org.chromium.components.policy.AbstractAppRestrictionsProvider;
@@ -52,10 +54,13 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
 /** Tests for the WebViewClient.shouldOverrideUrlLoading() method. */
+@DoNotBatch(reason = "This test class is historically prone to flakes.")
 @RunWith(Parameterized.class)
 @UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
 public class AwContentsClientShouldOverrideUrlLoadingTest extends AwParameterizedTest {
@@ -69,13 +74,13 @@
             "com.android.browser:EnterpriseAuthenticationAppLinkPolicy";
 
     private TestWebServer mWebServer;
-    private TestAwContentsClient mContentsClient;
+    private ShouldOverrideUrlLoadingClient mContentsClient;
     private AwTestContainerView mTestContainerView;
     private AwContents mAwContents;
 
     private TestAwContentsClient.ShouldOverrideUrlLoadingHelper mShouldOverrideUrlLoadingHelper;
 
-    private static class TestDefaultContentsClient extends TestAwContentsClient {
+    private static class TestDefaultContentsClient extends ShouldOverrideUrlLoadingClient {
         @Override
         public boolean hasWebViewClient() {
             return false;
@@ -96,12 +101,39 @@
         mWebServer.shutdown();
     }
 
+    private static class ShouldOverrideUrlLoadingClient extends TestAwContentsClient {
+        private final BlockingQueue<AwWebResourceRequest> mShouldOverrideUrlLoadingQueue =
+                new LinkedBlockingQueue<>();
+        private final BlockingQueue<String> mOnPageFinishedQueue = new LinkedBlockingQueue<>();
+
+        @Override
+        public boolean shouldOverrideUrlLoading(AwWebResourceRequest request) {
+            boolean value = super.shouldOverrideUrlLoading(request);
+            mShouldOverrideUrlLoadingQueue.offer(request);
+            return value;
+        }
+
+        @Override
+        public void onPageFinished(String url) {
+            super.onPageFinished(url);
+            mOnPageFinishedQueue.offer(url);
+        }
+
+        public AwWebResourceRequest waitForShouldOverrideUrlLoading() throws Exception {
+            return AwActivityTestRule.waitForNextQueueElement(mShouldOverrideUrlLoadingQueue);
+        }
+
+        public String waitForOnPageFinished() throws Exception {
+            return AwActivityTestRule.waitForNextQueueElement(mOnPageFinishedQueue);
+        }
+    }
+
     private void standardSetup() {
-        setupWithProvidedContentsClient(new TestAwContentsClient());
+        setupWithProvidedContentsClient(new ShouldOverrideUrlLoadingClient());
         mShouldOverrideUrlLoadingHelper = mContentsClient.getShouldOverrideUrlLoadingHelper();
     }
 
-    private void setupWithProvidedContentsClient(TestAwContentsClient contentsClient) {
+    private void setupWithProvidedContentsClient(ShouldOverrideUrlLoadingClient contentsClient) {
         mContentsClient = contentsClient;
         mTestContainerView = mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient);
         mAwContents = mTestContainerView.getAwContents();
@@ -725,6 +757,29 @@
         Assert.assertFalse(mShouldOverrideUrlLoadingHelper.isOutermostMainFrame());
     }
 
+    private void waitForRedirectsToFinish(String redirectUrl, String redirectTarget) {
+        // Drain the onPageFinished callback queue until we get the final expected onPageFinished
+        // callback.
+        CriteriaHelper.pollInstrumentationThread(
+                () -> {
+                    String url = mContentsClient.waitForOnPageFinished();
+                    if (redirectUrl.equals(url)) {
+                        // We will sometimes receive onPageFinished for the first page load (such as
+                        // for "delayed" JavaScript redirects), but may not receive this for
+                        // "immediate" JavaScript redirects or 302 server-side redirects.
+                        return false;
+                    }
+                    if (redirectTarget.equals(url)) {
+                        // This should be the last onPageFinished callback.
+                        return true;
+                    }
+                    Assert.fail("Received an unexpected URL from onPageFinished: " + url);
+                    return false; // This is unreached, but the compiler still needs a return value.
+                },
+                AwActivityTestRule.WAIT_TIMEOUT_MS,
+                AwActivityTestRule.CHECK_INTERVAL);
+    }
+
     /**
      * Worker method for the various redirect tests.
      *
@@ -757,60 +812,75 @@
         //    on whether it was a real click done by the user, or has it been done by JS; on click,
         //    both the initial navigation and the redirect are reported via
         //    shouldOverrideUrlLoading.
-        int directLoadCallCount = mShouldOverrideUrlLoadingHelper.getCallCount();
         mActivityTestRule.loadUrlSync(
                 mAwContents, mContentsClient.getOnPageFinishedHelper(), redirectUrl);
-
-        mShouldOverrideUrlLoadingHelper.waitForCallback(directLoadCallCount, 1);
-        Assert.assertEquals(
-                redirectTarget, mShouldOverrideUrlLoadingHelper.getShouldOverrideUrlLoadingUrl());
-        Assert.assertEquals(serverSideRedirect, mShouldOverrideUrlLoadingHelper.isRedirect());
-        Assert.assertFalse(mShouldOverrideUrlLoadingHelper.hasUserGesture());
-        Assert.assertTrue(mShouldOverrideUrlLoadingHelper.isOutermostMainFrame());
+        AwWebResourceRequest request = mContentsClient.waitForShouldOverrideUrlLoading();
+        Assert.assertEquals(redirectTarget, request.url);
+        Assert.assertEquals(serverSideRedirect, request.isRedirect);
+        Assert.assertFalse(request.hasUserGesture);
+        Assert.assertTrue(request.isOutermostMainFrame);
+        waitForRedirectsToFinish(redirectUrl, redirectTarget);
 
         // Test clicking with JS, hasUserGesture must be false.
         int indirectLoadCallCount = mShouldOverrideUrlLoadingHelper.getCallCount();
         mActivityTestRule.loadUrlSync(
                 mAwContents, mContentsClient.getOnPageFinishedHelper(), pageWithLinkToRedirectUrl);
-        Assert.assertEquals(indirectLoadCallCount, mShouldOverrideUrlLoadingHelper.getCallCount());
+        // This assertion is redundant because loadUrlSync already waits for onPageFinished,
+        // however we still need to call waitForOnPageFinished() in order to drain the queue.
+        Assert.assertEquals(
+                "Expected onPageFinished for pageWithLinkToRedirectUrl",
+                pageWithLinkToRedirectUrl,
+                mContentsClient.waitForOnPageFinished());
+        Assert.assertEquals(
+                "shouldOverrideUrlLoading should not be invoked during loadUrlSync",
+                indirectLoadCallCount,
+                mShouldOverrideUrlLoadingHelper.getCallCount());
 
         clickOnLinkUsingJs();
 
-        mShouldOverrideUrlLoadingHelper.waitForCallback(indirectLoadCallCount, 1);
-        Assert.assertEquals(
-                redirectUrl, mShouldOverrideUrlLoadingHelper.getShouldOverrideUrlLoadingUrl());
-        Assert.assertFalse(mShouldOverrideUrlLoadingHelper.isRedirect());
-        Assert.assertFalse(mShouldOverrideUrlLoadingHelper.hasUserGesture());
-        Assert.assertTrue(mShouldOverrideUrlLoadingHelper.isOutermostMainFrame());
-        mShouldOverrideUrlLoadingHelper.waitForCallback(indirectLoadCallCount + 1, 1);
-        Assert.assertEquals(
-                redirectTarget, mShouldOverrideUrlLoadingHelper.getShouldOverrideUrlLoadingUrl());
-        Assert.assertEquals(serverSideRedirect, mShouldOverrideUrlLoadingHelper.isRedirect());
-        Assert.assertFalse(mShouldOverrideUrlLoadingHelper.hasUserGesture());
-        Assert.assertTrue(mShouldOverrideUrlLoadingHelper.isOutermostMainFrame());
+        request = mContentsClient.waitForShouldOverrideUrlLoading();
+        Assert.assertEquals(redirectUrl, request.url);
+        Assert.assertFalse(request.isRedirect);
+        Assert.assertFalse(request.hasUserGesture);
+        Assert.assertTrue(request.isOutermostMainFrame);
 
-        // Make sure the redirect target page has finished loading.
-        mActivityTestRule.pollUiThread(() -> !mAwContents.getTitle().equals(pageTitle));
+        request = mContentsClient.waitForShouldOverrideUrlLoading();
+        Assert.assertEquals(redirectTarget, request.url);
+        Assert.assertEquals(serverSideRedirect, request.isRedirect);
+        Assert.assertFalse(request.hasUserGesture);
+        Assert.assertTrue(request.isOutermostMainFrame);
+        waitForRedirectsToFinish(redirectUrl, redirectTarget);
+
         indirectLoadCallCount = mShouldOverrideUrlLoadingHelper.getCallCount();
-        mActivityTestRule.loadUrlAsync(mAwContents, pageWithLinkToRedirectUrl);
+        mActivityTestRule.loadUrlSync(
+                mAwContents, mContentsClient.getOnPageFinishedHelper(), pageWithLinkToRedirectUrl);
+        // This assertion is redundant because loadUrlSync already waits for onPageFinished,
+        // however we still need to call waitForOnPageFinished() in order to drain the queue.
+        Assert.assertEquals(
+                "Expected onPageFinished for pageWithLinkToRedirectUrl",
+                pageWithLinkToRedirectUrl,
+                mContentsClient.waitForOnPageFinished());
         mActivityTestRule.pollUiThread(() -> mAwContents.getTitle().equals(pageTitle));
-        Assert.assertEquals(indirectLoadCallCount, mShouldOverrideUrlLoadingHelper.getCallCount());
+        Assert.assertEquals(
+                "shouldOverrideUrlLoading should not be invoked during loadUrlSync",
+                indirectLoadCallCount,
+                mShouldOverrideUrlLoadingHelper.getCallCount());
 
         // Simulate touch, hasUserGesture must be true only on the first call.
         JSUtils.clickNodeWithUserGesture(mAwContents.getWebContents(), "link");
 
-        mShouldOverrideUrlLoadingHelper.waitForCallback(indirectLoadCallCount, 1);
-        Assert.assertEquals(
-                redirectUrl, mShouldOverrideUrlLoadingHelper.getShouldOverrideUrlLoadingUrl());
-        Assert.assertFalse(mShouldOverrideUrlLoadingHelper.isRedirect());
-        Assert.assertTrue(mShouldOverrideUrlLoadingHelper.hasUserGesture());
-        Assert.assertTrue(mShouldOverrideUrlLoadingHelper.isOutermostMainFrame());
-        mShouldOverrideUrlLoadingHelper.waitForCallback(indirectLoadCallCount + 1, 1);
-        Assert.assertEquals(
-                redirectTarget, mShouldOverrideUrlLoadingHelper.getShouldOverrideUrlLoadingUrl());
-        Assert.assertEquals(serverSideRedirect, mShouldOverrideUrlLoadingHelper.isRedirect());
-        Assert.assertFalse(mShouldOverrideUrlLoadingHelper.hasUserGesture());
-        Assert.assertTrue(mShouldOverrideUrlLoadingHelper.isOutermostMainFrame());
+        request = mContentsClient.waitForShouldOverrideUrlLoading();
+        Assert.assertEquals(redirectUrl, request.url);
+        Assert.assertFalse(request.isRedirect);
+        Assert.assertTrue(request.hasUserGesture);
+        Assert.assertTrue(request.isOutermostMainFrame);
+
+        request = mContentsClient.waitForShouldOverrideUrlLoading();
+        Assert.assertEquals(redirectTarget, request.url);
+        Assert.assertEquals(serverSideRedirect, request.isRedirect);
+        Assert.assertFalse(request.hasUserGesture);
+        Assert.assertTrue(request.isOutermostMainFrame);
+        waitForRedirectsToFinish(redirectUrl, redirectTarget);
     }
 
     @Test
@@ -913,7 +983,7 @@
     @SmallTest
     @Feature({"AndroidWebView"})
     public void testCallDestroyInCallback() throws Throwable {
-        class DestroyInCallbackClient extends TestAwContentsClient {
+        class DestroyInCallbackClient extends ShouldOverrideUrlLoadingClient {
             @Override
             public boolean shouldOverrideUrlLoading(AwContentsClient.AwWebResourceRequest request) {
                 mAwContents.destroy();
@@ -947,7 +1017,7 @@
     @SmallTest
     @Feature({"AndroidWebView", "Navigation"})
     public void testReloadingUrlDoesNotBreakBackForwardList() throws Throwable {
-        class ReloadInCallbackClient extends TestAwContentsClient {
+        class ReloadInCallbackClient extends ShouldOverrideUrlLoadingClient {
             @Override
             public boolean shouldOverrideUrlLoading(AwContentsClient.AwWebResourceRequest request) {
                 super.shouldOverrideUrlLoading(request);
@@ -1003,7 +1073,7 @@
     @Feature({"AndroidWebView"})
     public void testCallStopAndLoadJsInCallback() throws Throwable {
         final String globalJsVar = "window.testCallStopAndLoadJsInCallback";
-        class StopInCallbackClient extends TestAwContentsClient {
+        class StopInCallbackClient extends ShouldOverrideUrlLoadingClient {
             @Override
             public boolean shouldOverrideUrlLoading(AwContentsClient.AwWebResourceRequest request) {
                 mAwContents.stopLoading();
@@ -1052,7 +1122,7 @@
                 httpPath,
                 CommonResources.makeHtmlPageWithSimpleLinkTo(
                         getTestPageCommonHeaders(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL));
-        class StopInCallbackClient extends TestAwContentsClient {
+        class StopInCallbackClient extends ShouldOverrideUrlLoadingClient {
             @Override
             public boolean shouldOverrideUrlLoading(AwContentsClient.AwWebResourceRequest request) {
                 mAwContents.loadUrl(httpPathOnServer);
@@ -1341,7 +1411,7 @@
     private static final String BAD_SCHEME = "badscheme://";
 
     // AwContentsClient handling an invalid network scheme
-    private static class BadSchemeClient extends TestAwContentsClient {
+    private static class BadSchemeClient extends ShouldOverrideUrlLoadingClient {
         CountDownLatch mLatch = new CountDownLatch(1);
 
         @Override
diff --git a/ash/capture_mode/capture_mode_game_dashboard_unittests.cc b/ash/capture_mode/capture_mode_game_dashboard_unittests.cc
index c95a12d..6753daa 100644
--- a/ash/capture_mode/capture_mode_game_dashboard_unittests.cc
+++ b/ash/capture_mode/capture_mode_game_dashboard_unittests.cc
@@ -26,9 +26,11 @@
 #include "ash/shell.h"
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/style/pill_button.h"
+#include "ash/system/unified/feature_tile.h"
 #include "ash/test/ash_test_base.h"
 #include "ash/test/ash_test_util.h"
 #include "ash/wm/desks/desks_test_util.h"
+#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
 #include "base/system/sys_info.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
@@ -1110,6 +1112,40 @@
       histogram_name,
       /*sample=*/EndRecordingReason::kGameToolbarStopRecordingButton,
       /*expected_count=*/1);
+
+  // Testing stop recording by the tablet mode enum.
+  // Game dashboard is not available on the tablet mode.
+  if (GetParam()) {
+    return;
+  }
+
+  histogram_tester_.ExpectBucketCount(
+      histogram_name,
+      /*sample=*/EndRecordingReason::kGameDashboardTabletMode,
+      /*expected_count=*/0);
+  auto game_dashboard_test_api = std::make_unique<GameDashboardContextTestApi>(
+      GameDashboardController::Get()->GetGameDashboardContext(game_window()),
+      GetEventGenerator());
+
+  game_dashboard_test_api->OpenTheMainMenu();
+  LeftClickOn(game_dashboard_test_api->GetMainMenuRecordGameTile());
+  // Clicking on the record game tile closes the main menu, and asynchronously
+  // starts the capture session. Run until idle to ensure that the posted task
+  // runs synchronously and completes before proceeding.
+  base::RunLoop().RunUntilIdle();
+  LeftClickOn(GetStartRecordingButton());
+  WaitForRecordingToStart();
+  EXPECT_TRUE(CaptureModeController::Get()->is_recording_in_progress());
+  TabletModeControllerTestApi().EnterTabletMode();
+  WaitForCaptureFileToBeSaved();
+  // The histogram name becomes
+  // "ash.CaptureModeController.EndRecordingReason.TabletMode";
+  histogram_tester_.ExpectBucketCount(
+      BuildHistogramName(kHistogramNameBase,
+                         test_api.GetBehavior(BehaviorType::kDefault),
+                         /*append_ui_mode_suffix=*/true),
+      /*sample=*/EndRecordingReason::kGameDashboardTabletMode,
+      /*expected_count=*/1);
 }
 
 TEST_P(GameDashboardCaptureModeHistogramTest,
diff --git a/ash/capture_mode/capture_mode_metrics.h b/ash/capture_mode/capture_mode_metrics.h
index 6f05df8..bd9ae75 100644
--- a/ash/capture_mode/capture_mode_metrics.h
+++ b/ash/capture_mode/capture_mode_metrics.h
@@ -42,7 +42,8 @@
   kKeyboardShortcut,
   kGameDashboardStopRecordingButton,
   kGameToolbarStopRecordingButton,
-  kMaxValue = kGameToolbarStopRecordingButton,
+  kGameDashboardTabletMode,
+  kMaxValue = kGameDashboardTabletMode,
 };
 
 // Enumeration of capture bar buttons that can be pressed while in capture mode.
diff --git a/ash/events/peripheral_customization_event_rewriter.cc b/ash/events/peripheral_customization_event_rewriter.cc
index 8806917..0a1ebdbb 100644
--- a/ash/events/peripheral_customization_event_rewriter.cc
+++ b/ash/events/peripheral_customization_event_rewriter.cc
@@ -1133,7 +1133,8 @@
   auto modifier_key = ConvertDomCodeToModifierKey(key_event.code());
   int key_event_characteristic_flag =
       ConvertKeyCodeToFlags(key_event.key_code());
-  if (settings && modifier_key) {
+  // Modifiers only need to be remapped now if the rewriter fix is disabled.
+  if (!features::IsKeyboardRewriterFixEnabled() && settings && modifier_key) {
     auto iter = settings->modifier_remappings.find(*modifier_key);
     if (iter != settings->modifier_remappings.end()) {
       key_event_characteristic_flag = ConvertModifierKeyToFlags(iter->second);
diff --git a/ash/events/peripheral_customization_event_rewriter_unittest.cc b/ash/events/peripheral_customization_event_rewriter_unittest.cc
index fdff73e..4d7e6f22 100644
--- a/ash/events/peripheral_customization_event_rewriter_unittest.cc
+++ b/ash/events/peripheral_customization_event_rewriter_unittest.cc
@@ -911,6 +911,10 @@
 
 TEST_F(PeripheralCustomizationEventRewriterTest,
        RemappedModifierReleasedDuringSequence) {
+  // This test is only relevant when the keyboard rewriter fix is disabled.
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndDisableFeature(features::kEnableKeyboardRewriterFix);
+
   keyboard_->settings->modifier_remappings[ui::mojom::ModifierKey::kAlt] =
       ui::mojom::ModifierKey::kControl;
 
diff --git a/ash/game_dashboard/game_dashboard_context.cc b/ash/game_dashboard/game_dashboard_context.cc
index b47ff2b..c4991a8 100644
--- a/ash/game_dashboard/game_dashboard_context.cc
+++ b/ash/game_dashboard/game_dashboard_context.cc
@@ -39,6 +39,7 @@
 #include "ui/base/l10n/time_format.h"
 #include "ui/compositor/layer.h"
 #include "ui/events/event.h"
+#include "ui/events/keycodes/keyboard_codes_posix.h"
 #include "ui/events/types/event_type.h"
 #include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/rect.h"
@@ -105,6 +106,21 @@
       /*animate=*/true);
 }
 
+// Determines whether a given `key_code` will interact with the toolbar.
+bool WillToolbarViewProcessKeyCode(const ui::KeyboardCode key_code) {
+  switch (key_code) {
+    case ui::VKEY_RIGHT:
+    case ui::VKEY_LEFT:
+    case ui::VKEY_UP:
+    case ui::VKEY_DOWN:
+    case ui::VKEY_RETURN:
+    case ui::VKEY_SPACE:
+      return true;
+    default:
+      return false;
+  }
+}
+
 }  // namespace
 
 GameDashboardContext::GameDashboardContext(aura::Window* game_window)
@@ -262,6 +278,7 @@
             std::move(widget_delegate)));
     main_menu_widget_->AddObserver(this);
     main_menu_widget_->Show();
+    game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
     game_dashboard_button_->SetToggled(true);
     AddCursorHandler();
     RecordGameDashboardToggleMainMenu(app_id_, toggle_method,
@@ -302,6 +319,7 @@
     MaybeUpdateToolbarWidgetBounds();
 
     toolbar_widget_->ShowInactive();
+    game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
     // Display the toolbar behind the main menu view.
     EnsureMainMenuAboveToolbar();
     RecordGameDashboardToolbarToggleState(app_id_, /*toggled_on=*/true);
@@ -317,6 +335,7 @@
   DCHECK(toolbar_widget_);
   toolbar_view_ = nullptr;
   toolbar_widget_.reset();
+  game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
   RecordGameDashboardToolbarToggleState(app_id_, /*toggled_on=*/false);
 }
 
@@ -427,8 +446,26 @@
 void GameDashboardContext::OnEvent(ui::Event* event) {
   // Close the main menu if the user clicks outside of both the main menu
   // widget and the Game Dashboard button.
-  if (main_menu_widget_) {
-    switch (event->type()) {
+  auto event_type = event->type();
+  if (event_type == ui::ET_KEY_PRESSED) {
+    const ui::KeyEvent* key_event = event->AsKeyEvent();
+    if (toolbar_widget_ && toolbar_widget_->IsActive() && main_menu_widget_ &&
+        WillToolbarViewProcessKeyCode(key_event->key_code())) {
+      // Close the main menu if the toolbar processes the given key.
+      CloseMainMenu(GameDashboardMainMenuToggleMethod::kOthers);
+    } else if (ShouldNavigateToNewWidget(key_event)) {
+      const auto* currently_focused = views::Widget::GetWidgetForNativeWindow(
+          static_cast<aura::Window*>(event->target()));
+      const bool reverse = event->IsShiftDown();
+
+      // Manually move focus from the currently focused widget to the next in
+      // the widget list.
+      MoveFocus(game_dashboard_utils::GetNextWidgetToFocus(
+                    GetTraversableWidgets(), currently_focused, reverse),
+                event, reverse);
+    }
+  } else if (main_menu_widget_) {
+    switch (event_type) {
       case ui::ET_TOUCH_PRESSED:
       case ui::ET_MOUSE_PRESSED: {
         // TODO(b/328852471): Update logic to compare event target with native
@@ -756,6 +793,7 @@
   DCHECK(main_menu_view_);
   RemoveCursorHandler();
   main_menu_view_ = nullptr;
+  game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
   game_dashboard_button_->SetToggled(false);
 }
 
@@ -765,4 +803,66 @@
   }
 }
 
+bool GameDashboardContext::ShouldNavigateToNewWidget(
+    const ui::KeyEvent* event) const {
+  // Tab navigation between Game Dashboard sibling widgets is only supported
+  // when the GD button is enabled.
+  if (!game_dashboard_button_->GetEnabled() ||
+      event->type() != ui::ET_KEY_PRESSED ||
+      event->key_code() != ui::VKEY_TAB) {
+    return false;
+  }
+
+  if (auto* target_widget = views::Widget::GetWidgetForNativeWindow(
+          static_cast<aura::Window*>(event->target()))) {
+    if (auto* focus_manager = target_widget->GetFocusManager()) {
+      // If `GetNextFocusableView` returns null, navigation has reached the last
+      // focusable view in the given direction.
+      return !(focus_manager->GetNextFocusableView(
+          /*starting_view=*/focus_manager->GetFocusedView(),
+          /*starting_widget=*/target_widget,
+          /*reverse=*/event->IsShiftDown(),
+          /*dont_loop=*/true));
+    }
+  }
+
+  return false;
+}
+
+std::vector<views::Widget*> GameDashboardContext::GetTraversableWidgets()
+    const {
+  std::vector<views::Widget*> widget_list;
+  widget_list.emplace_back(game_dashboard_button_widget_.get());
+  if (main_menu_widget_) {
+    widget_list.emplace_back(main_menu_widget_.get());
+  }
+  if (toolbar_widget_) {
+    widget_list.emplace_back(toolbar_widget_.get());
+  }
+  if (widget_list.size() == 1) {
+    // If the toolbar and main menu widgets don't exist but focus is placed on
+    // the Game Dashboard button, manually move focus to the game window to
+    // avoid tab support looping just the Game Dashboard button.
+    widget_list.emplace_back(
+        views::Widget::GetWidgetForNativeWindow(game_window_.get()));
+  }
+
+  return widget_list;
+}
+
+void GameDashboardContext::MoveFocus(views::Widget* new_widget,
+                                     ui::Event* event,
+                                     bool reverse) {
+  CHECK(new_widget) << "Cannot move focus to a non-existent widget.";
+  auto* focus_manager = new_widget->GetFocusManager();
+  DCHECK(focus_manager) << "Cannot move focus without a focus manager";
+  focus_manager->ClearFocus();
+  // Avoid having the focus restored to the same view when the parent view
+  // is refocused.
+  focus_manager->SetStoredFocusView(nullptr);
+  focus_manager->AdvanceFocus(reverse);
+  event->StopPropagation();
+  event->SetHandled();
+}
+
 }  // namespace ash
diff --git a/ash/game_dashboard/game_dashboard_context.h b/ash/game_dashboard/game_dashboard_context.h
index 9b98050b..33a60c1e 100644
--- a/ash/game_dashboard/game_dashboard_context.h
+++ b/ash/game_dashboard/game_dashboard_context.h
@@ -224,6 +224,19 @@
   // Ensures that the main menu stacks above the toolbar.
   void EnsureMainMenuAboveToolbar();
 
+  // Determines whether it's required to tab navigate from one Game Dashboard
+  // widget to another widget. Returns false if tab-navigating within the same
+  // widget.
+  bool ShouldNavigateToNewWidget(const ui::KeyEvent* event) const;
+
+  // Returns a list of visible Game Dashboard widgets that are available to be
+  // traversed.
+  std::vector<views::Widget*> GetTraversableWidgets() const;
+
+  // Manually moves focus to the `new_widget`. If `reverse` is true, focus will
+  // move backwards.
+  void MoveFocus(views::Widget* new_widget, ui::Event* event, bool reverse);
+
   const raw_ptr<aura::Window> game_window_;
 
   const std::string app_id_;
diff --git a/ash/game_dashboard/game_dashboard_context_test_api.cc b/ash/game_dashboard/game_dashboard_context_test_api.cc
index 8f9a47f..6fa32063 100644
--- a/ash/game_dashboard/game_dashboard_context_test_api.cc
+++ b/ash/game_dashboard/game_dashboard_context_test_api.cc
@@ -254,6 +254,13 @@
           GameDashboardToolbarView::ToolbarViewId::kScreenshotButton)));
 }
 
+bool GameDashboardContextTestApi::IsToolbarExpanded() {
+  auto* toolbar_view = GetToolbarView();
+  CHECK(toolbar_view)
+      << "The toolbar must be opened first before checking expanded state.";
+  return toolbar_view->is_expanded_;
+}
+
 GameDashboardToolbarSnapLocation
 GameDashboardContextTestApi::GetToolbarSnapLocation() const {
   return context_->toolbar_snap_location_;
diff --git a/ash/game_dashboard/game_dashboard_context_test_api.h b/ash/game_dashboard/game_dashboard_context_test_api.h
index 1a2c570..80578a91 100644
--- a/ash/game_dashboard/game_dashboard_context_test_api.h
+++ b/ash/game_dashboard/game_dashboard_context_test_api.h
@@ -108,6 +108,7 @@
   IconButton* GetToolbarGameControlsButton();
   IconButton* GetToolbarRecordGameButton();
   IconButton* GetToolbarScreenshotButton();
+  bool IsToolbarExpanded();
 
   // Returns the quadrant that the toolbar is currently placed in.
   GameDashboardToolbarSnapLocation GetToolbarSnapLocation() const;
diff --git a/ash/game_dashboard/game_dashboard_context_unittest.cc b/ash/game_dashboard/game_dashboard_context_unittest.cc
index ac3fb09..d99b374 100644
--- a/ash/game_dashboard/game_dashboard_context_unittest.cc
+++ b/ash/game_dashboard/game_dashboard_context_unittest.cc
@@ -742,6 +742,14 @@
     EXPECT_EQ(toggled, game_button->toggled());
   }
 
+  void TabNavigateForward() {
+    GetEventGenerator()->PressAndReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
+  }
+
+  void TabNavigateBackward() {
+    GetEventGenerator()->PressAndReleaseKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
+  }
+
   void PressKeyAndVerify(ui::KeyboardCode key,
                          GameDashboardToolbarSnapLocation desired_location) {
     GetEventGenerator()->PressAndReleaseKey(key);
@@ -1636,6 +1644,133 @@
   ASSERT_TRUE(arc_gamepad_button->HasFocus());
 }
 
+TEST_F(GameDashboardContextTest, TabNavigationMainMenu) {
+  // Open the main menu and begin tab navigation.
+  CreateGameWindow(/*is_arc_window=*/false);
+  test_api_->OpenTheMainMenu();
+  TabNavigateForward();
+
+  // Verify focus is placed on the main menu's first element then move focus to
+  // the last element in the main menu.
+  views::Widget* main_menu_widget = test_api_->GetMainMenuWidget();
+  EXPECT_TRUE(main_menu_widget->IsActive());
+  EXPECT_TRUE(test_api_->GetMainMenuToolbarTile()->HasFocus());
+  main_menu_widget->GetFocusManager()->SetFocusedView(
+      test_api_->GetMainMenuSettingsButton());
+  EXPECT_TRUE(test_api_->GetMainMenuSettingsButton()->HasFocus());
+
+  // Tab navigate forward and verify focus is placed on the Game Dashboard
+  // Button.
+  TabNavigateForward();
+  EXPECT_TRUE(test_api_->GetGameDashboardButton()->HasFocus());
+
+  // Tab navigate forward and verify focus is placed back on the main menu's
+  // first element.
+  TabNavigateForward();
+  EXPECT_TRUE(test_api_->GetMainMenuToolbarTile()->HasFocus());
+
+  // Tab navigate backwards and verify focus is placed back on the Game
+  // Dashboard button.
+  TabNavigateBackward();
+  EXPECT_TRUE(test_api_->GetGameDashboardButton()->HasFocus());
+
+  // Tab navigate backwards and verify focus is placed on the last element in
+  // the main menu.
+  TabNavigateBackward();
+  EXPECT_TRUE(test_api_->GetMainMenuSettingsButton()->HasFocus());
+}
+
+TEST_F(GameDashboardContextTest, TabNavigationMainMenuAndToolbar) {
+  // Open the main menu and toolbar, then tab navigate to the last element in
+  // the main menu.
+  CreateGameWindow(/*is_arc_window=*/false);
+  test_api_->OpenTheMainMenu();
+  test_api_->OpenTheToolbar();
+  TabNavigateForward();
+  views::Widget* main_menu_widget = test_api_->GetMainMenuWidget();
+  ASSERT_TRUE(main_menu_widget->IsActive());
+  ASSERT_TRUE(test_api_->GetMainMenuToolbarTile()->HasFocus());
+  main_menu_widget->GetFocusManager()->SetFocusedView(
+      test_api_->GetMainMenuSettingsButton());
+  ASSERT_TRUE(test_api_->GetMainMenuSettingsButton()->HasFocus());
+
+  // Tab navigate forward and verify focus is placed on the first element in the
+  // toolbar.
+  TabNavigateForward();
+  views::Widget* toolbar_widget = test_api_->GetToolbarWidget();
+  EXPECT_TRUE(toolbar_widget->IsActive());
+  EXPECT_TRUE(test_api_->GetToolbarGamepadButton()->HasFocus());
+
+  // Move focus to the last element in the toolbar, tab navigate forward, and
+  // verify focus is placed on the Game Dashboard button.
+  toolbar_widget->GetFocusManager()->SetFocusedView(
+      test_api_->GetToolbarScreenshotButton());
+  TabNavigateForward();
+  EXPECT_TRUE(test_api_->GetGameDashboardButton()->HasFocus());
+
+  // Tab navigate forward and verify focus is placed back on the main menu's
+  // first element.
+  TabNavigateForward();
+  EXPECT_TRUE(test_api_->GetMainMenuToolbarTile()->HasFocus());
+
+  // Tab navigate backwards and verify focus is placed back on the Game
+  // Dashboard button.
+  TabNavigateBackward();
+  EXPECT_TRUE(test_api_->GetGameDashboardButton()->HasFocus());
+
+  // Tab navigate backwards and verify focus is placed back on the last element
+  // in the toolbar.
+  TabNavigateBackward();
+  EXPECT_TRUE(test_api_->GetToolbarScreenshotButton()->HasFocus());
+
+  // Move focus to the first element in the toolbar, tab navigate backwards, and
+  // verify focus is placed on the last element in the main menu.
+  toolbar_widget->GetFocusManager()->SetFocusedView(
+      test_api_->GetToolbarGamepadButton());
+  TabNavigateBackward();
+  EXPECT_TRUE(test_api_->GetMainMenuSettingsButton()->HasFocus());
+}
+
+TEST_F(GameDashboardContextTest, TabNavigationToolbar) {
+  // Open the main menu and toolbar, close the main menu, then begin tab
+  // navigation.
+  CreateGameWindow(/*is_arc_window=*/false);
+  test_api_->OpenTheMainMenu();
+  test_api_->OpenTheToolbar();
+  test_api_->CloseTheMainMenu();
+  test_api_->SetFocusOnToolbar();
+  ASSERT_TRUE(test_api_->IsToolbarExpanded());
+  TabNavigateForward();
+
+  // Verify the toolbar is active and has focus.
+  views::Widget* toolbar_widget = test_api_->GetToolbarWidget();
+  EXPECT_TRUE(toolbar_widget->IsActive());
+  EXPECT_TRUE(test_api_->GetToolbarGamepadButton()->HasFocus());
+
+  // Move focus to the last element in the toolbar, tab navigate forward, and
+  // verify focus is placed on the Game Dashboard button.
+  toolbar_widget->GetFocusManager()->SetFocusedView(
+      test_api_->GetToolbarScreenshotButton());
+  ASSERT_TRUE(test_api_->GetToolbarScreenshotButton()->HasFocus());
+  TabNavigateForward();
+  EXPECT_TRUE(test_api_->GetGameDashboardButton()->HasFocus());
+
+  // Tab navigate forward and verify focus is placed back on the toolbar's
+  // first element.
+  TabNavigateForward();
+  EXPECT_TRUE(test_api_->GetToolbarGamepadButton()->HasFocus());
+
+  // Tab navigate backwards and verify focus is placed back on the Game
+  // Dashboard button.
+  TabNavigateBackward();
+  EXPECT_TRUE(test_api_->GetGameDashboardButton()->HasFocus());
+
+  // Tab navigate backwards and verify focus is placed back on the last element
+  // in the toolbar.
+  TabNavigateBackward();
+  EXPECT_TRUE(test_api_->GetToolbarScreenshotButton()->HasFocus());
+}
+
 // -----------------------------------------------------------------------------
 // GameTypeGameDashboardContextTest:
 // Test fixture to test both ARC and GeForceNow game window depending on the
@@ -2441,7 +2576,7 @@
           .append(kGameDashboardHistogramSeparator)
           .append(kGameDashboardHistogramOff);
 
-  // Toggle on/off main menu by pressing GD button.
+  // Toggle on/off main menu by pressing Game Dashboard button.
   test_api_->OpenTheMainMenu();
   VerifyToggleMainMenuHistogram(
       histograms, histogram_name_on,
diff --git a/ash/game_dashboard/game_dashboard_controller.cc b/ash/game_dashboard/game_dashboard_controller.cc
index cca81ac..275fa6d 100644
--- a/ash/game_dashboard/game_dashboard_controller.cc
+++ b/ash/game_dashboard/game_dashboard_controller.cc
@@ -239,10 +239,8 @@
       if (active_recording_context_) {
         auto* capture_mode_controller = CaptureModeController::Get();
         CHECK(capture_mode_controller->is_recording_in_progress());
-        // TODO(b/316036118): Update the end recording reason in the capture
-        // mode.
         capture_mode_controller->EndVideoRecording(
-            EndRecordingReason::kGameDashboardStopRecordingButton);
+            EndRecordingReason::kGameDashboardTabletMode);
       }
       MaybeEnableFeatures(/*enable=*/false,
                           GameDashboardMainMenuToggleMethod::kTabletMode);
diff --git a/ash/game_dashboard/game_dashboard_main_menu_view.cc b/ash/game_dashboard/game_dashboard_main_menu_view.cc
index 648d38ce..ff79ba7 100644
--- a/ash/game_dashboard/game_dashboard_main_menu_view.cc
+++ b/ash/game_dashboard/game_dashboard_main_menu_view.cc
@@ -58,12 +58,16 @@
 #include "ui/views/border.h"
 #include "ui/views/bubble/bubble_border.h"
 #include "ui/views/controls/button/button.h"
+#include "ui/views/controls/button/image_button.h"
 #include "ui/views/controls/focus_ring.h"
 #include "ui/views/controls/highlight_path_generator.h"
+#include "ui/views/controls/label.h"
 #include "ui/views/layout/box_layout.h"
 #include "ui/views/layout/box_layout_view.h"
 #include "ui/views/layout/fill_layout.h"
 #include "ui/views/layout/flex_layout_view.h"
+#include "ui/views/layout/layout_types.h"
+#include "ui/views/style/typography_provider.h"
 #include "ui/views/view.h"
 #include "ui/views/view_class_properties.h"
 #include "ui/views/widget/widget.h"
@@ -86,6 +90,10 @@
 constexpr float kDetailRowCornerRadius = 16.0f;
 // Corner radius for feature tiles.
 constexpr int kTileCornerRadius = 20;
+// Line height for feature tiles with sub-labels
+constexpr int kTileSublabelLineHeight = 16;
+// Line height for feature tiles with no sub-labels
+constexpr int kTileLabelLineHeight = 32;
 
 constexpr gfx::RoundedCornersF kGCDetailRowCorners =
     gfx::RoundedCornersF(/*upper_left=*/kDetailRowCornerRadius,
@@ -118,6 +126,7 @@
     const std::optional<std::u16string>& sub_label) {
   auto tile =
       std::make_unique<FeatureTile>(std::move(callback), is_togglable, type);
+
   tile->SetID(id);
   tile->SetVectorIcon(icon);
   tile->SetLabel(text);
@@ -140,9 +149,57 @@
   // Disabled state colors.
   tile->SetBackgroundDisabledColorId(cros_tokens::kCrosSysSystemOnBaseOpaque);
 
+  views::ImageButton* tile_icon = tile->icon_button();
+  views::FlexLayoutView* tile_label_container = tile->title_container();
+  views::Label* tile_label = tile->label();
+  views::Label* tile_sub_label = tile->sub_label();
+
+  // Readjust Compact Tiles.
+  if (type == FeatureTile::TileType::kCompact) {
+    // Adjust internal spacing.
+    tile->SetProperty(views::kInternalPaddingKey,
+                      gfx::Insets::TLBR(0, 8, 0, 8));
+    tile_icon->SetProperty(views::kMarginsKey, gfx::Insets::TLBR(12, 0, 4, 0));
+    tile_icon->SetPreferredSize(gfx::Size(20, 20));
+
+    // Adjust text and icon alignment for text wrapping.
+    tile_icon->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
+    tile_label_container->SetCrossAxisAlignment(
+        views::LayoutAlignment::kCenter);
+
+    tile_label_container->SetProperty(views::kMarginsKey,
+                                      gfx::Insets::TLBR(0, 0, 10, 0));
+
+    // Adjust line and text specifications.
+    tile_label->SetFontList(
+        TypographyProvider::Get()
+            ->ResolveTypographyToken(TypographyToken::kCrosAnnotation2)
+            .DeriveWithSizeDelta(1)
+            .DeriveWithHeightUpperBound(16));
+    tile_sub_label->SetFontList(
+        TypographyProvider::Get()->ResolveTypographyToken(
+            TypographyToken::kCrosAnnotation2));
+
+    tile_label->SetLineHeight(kTileSublabelLineHeight);
+    tile_label->SetPreferredSize(gfx::Size(80, kTileLabelLineHeight));
+
+  } else {
+    // Resize the icon and its margins.
+    tile_icon->SetPreferredSize(
+        gfx::Size(20, tile_icon->GetPreferredSize().height()));
+    tile_icon->SetProperty(views::kMarginsKey, gfx::Insets::TLBR(6, 20, 6, 16));
+
+    // Adjust line specifications and enable text wrapping.
+    tile_label->SetProperty(views::kMarginsKey, gfx::Insets::TLBR(0, 0, 0, 15));
+    tile_label->SetLineHeight(tile->sub_label() ? kTileSublabelLineHeight
+                                                : kTileLabelLineHeight);
+    tile_label->SetMultiLine(true);
+  }
+
   if (sub_label.has_value()) {
     tile->SetSubLabel(sub_label.value());
     tile->SetSubLabelVisibility(true);
+    tile_sub_label->SetLineHeight(kTileSublabelLineHeight);
   }
   // Setup focus ring.
   views::FocusRing::Get(tile.get())->SetColorId(cros_tokens::kCrosSysPrimary);
diff --git a/ash/game_dashboard/game_dashboard_toolbar_view.cc b/ash/game_dashboard/game_dashboard_toolbar_view.cc
index 5597e92..aeda597 100644
--- a/ash/game_dashboard/game_dashboard_toolbar_view.cc
+++ b/ash/game_dashboard/game_dashboard_toolbar_view.cc
@@ -33,6 +33,7 @@
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/vector2d.h"
 #include "ui/gfx/geometry/vector2d_conversions.h"
+#include "ui/views/accessibility/view_accessibility.h"
 #include "ui/views/background.h"
 #include "ui/views/border.h"
 #include "ui/views/highlight_border.h"
@@ -383,6 +384,7 @@
       l10n_util::GetStringUTF16(
           IDS_ASH_GAME_DASHBOARD_TOOLBAR_TILE_BUTTON_TITLE),
       /*is_togglable=*/false, /*icon_color=*/cros_tokens::kCrosSysPrimary));
+  gamepad_button_->GetViewAccessibility().SetRole(ax::mojom::Role::kButton);
 
   UpdateGamepadButtonTooltipText();
   MayAddGameControlsTile();
@@ -397,6 +399,8 @@
         l10n_util::GetStringUTF16(
             IDS_ASH_GAME_DASHBOARD_RECORD_GAME_TILE_BUTTON_TITLE),
         /*is_togglable=*/true));
+    record_game_button_->GetViewAccessibility().SetRole(
+        ax::mojom::Role::kButton);
     record_game_button_->SetVectorIcon(kGdRecordGameIcon);
     record_game_button_->SetBackgroundToggledColor(cros_tokens::kCrosSysError);
     record_game_button_->SetToggledVectorIcon(kCaptureModeCircleStopIcon);
@@ -432,6 +436,8 @@
       l10n_util::GetStringUTF16(
           IDS_ASH_GAME_DASHBOARD_CONTROLS_TILE_BUTTON_TITLE),
       /*is_togglable=*/true));
+  game_controls_button_->GetViewAccessibility().SetRole(
+      ax::mojom::Role::kButton);
 
   UpdateViewForGameControls(*flags);
 }
diff --git a/ash/game_dashboard/game_dashboard_utils.cc b/ash/game_dashboard/game_dashboard_utils.cc
index 611867a6..a3238ea 100644
--- a/ash/game_dashboard/game_dashboard_utils.cc
+++ b/ash/game_dashboard/game_dashboard_utils.cc
@@ -17,6 +17,7 @@
 #include "ui/aura/window.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/display/screen.h"
+#include "ui/views/accessibility/view_accessibility.h"
 #include "ui/views/controls/button/button.h"
 #include "ui/views/view_utils.h"
 
@@ -44,6 +45,26 @@
          !display::Screen::GetScreen()->InTabletMode();
 }
 
+views::Widget* GetNextWidgetToFocus(
+    const std::vector<views::Widget*> widget_list,
+    const views::Widget* focused_widget,
+    bool reverse) {
+  if (auto it =
+          std::find(widget_list.begin(), widget_list.end(), focused_widget);
+      it != widget_list.end()) {
+    const int focused_widget_index =
+        std::distance(widget_list.begin(),
+                      it);  // it - widget_list_.begin();
+    const size_t widget_list_size = widget_list.size();
+    const size_t next_focus_widget_index =
+        (reverse ? (focused_widget_index + widget_list_size - 1u)
+                 : (focused_widget_index + 1u)) %
+        widget_list_size;
+    return widget_list[next_focus_widget_index];
+  }
+  return nullptr;
+}
+
 std::optional<ArcGameControlsFlag> GetGameControlsFlag(aura::Window* window) {
   if (!IsArcWindow(window)) {
     return std::nullopt;
@@ -152,4 +173,23 @@
              : 0;
 }
 
+void UpdateAccessibilityTree(const std::vector<views::Widget*>& widget_list) {
+  const size_t widget_list_size = widget_list.size();
+  if (widget_list_size <= 1u) {
+    return;
+  }
+
+  for (size_t i = 0; i < widget_list_size; i++) {
+    auto* contents_view = widget_list[i]->GetContentsView();
+    auto& view_a11y = contents_view->GetViewAccessibility();
+    const size_t prev_index = (i + widget_list_size - 1u) % widget_list_size;
+    const size_t next_index = (i + 1u) % widget_list_size;
+
+    view_a11y.SetPreviousFocus(widget_list[prev_index]);
+    view_a11y.SetNextFocus(widget_list[next_index]);
+    contents_view->NotifyAccessibilityEvent(ax::mojom::Event::kTreeChanged,
+                                            /*send_native_event=*/true);
+  }
+}
+
 }  // namespace ash::game_dashboard_utils
diff --git a/ash/game_dashboard/game_dashboard_utils.h b/ash/game_dashboard/game_dashboard_utils.h
index 43ed949..0a0817da 100644
--- a/ash/game_dashboard/game_dashboard_utils.h
+++ b/ash/game_dashboard/game_dashboard_utils.h
@@ -6,6 +6,7 @@
 #define ASH_GAME_DASHBOARD_GAME_DASHBOARD_UTILS_H_
 
 #include <optional>
+#include <vector>
 
 #include "ash/ash_export.h"
 #include "ash/public/cpp/arc_game_controls_flag.h"
@@ -16,6 +17,7 @@
 
 namespace views {
 class Button;
+class Widget;
 }  // namespace views
 
 namespace ash::game_dashboard_utils {
@@ -40,6 +42,14 @@
 // GD features availability dependency may change.
 ASH_EXPORT bool ShouldEnableFeatures();
 
+// Returns the next `views::Widget` from the `widget_list` that should be
+// focused. This is determined by looking at the currently `focused_widget` and
+// whether or not the tab navigation is moving in `reverse`.
+ASH_EXPORT views::Widget* GetNextWidgetToFocus(
+    const std::vector<views::Widget*> widget_list,
+    const views::Widget* focused_widget,
+    bool reverse);
+
 // Returns flags value if `window` is an ARC game window. Otherwise, it returns
 // nullopt.
 std::optional<ArcGameControlsFlag> GetGameControlsFlag(aura::Window* window);
@@ -74,6 +84,12 @@
 // header is not found or when the header is invisible.
 ASH_EXPORT int GetFrameHeaderHeight(aura::Window* window);
 
+// Updates the accessibility tree to match the given `widget_list`. This ensures
+// that the order of widgets in the `widget_list` reflects accessibility
+// navigation and tab navigation.
+ASH_EXPORT void UpdateAccessibilityTree(
+    const std::vector<views::Widget*>& widget_list);
+
 }  // namespace ash::game_dashboard_utils
 
 #endif  // ASH_GAME_DASHBOARD_GAME_DASHBOARD_UTILS_H_
diff --git a/ash/shelf/hotseat_widget.cc b/ash/shelf/hotseat_widget.cc
index 3f5ecd1..571d783b 100644
--- a/ash/shelf/hotseat_widget.cc
+++ b/ash/shelf/hotseat_widget.cc
@@ -22,6 +22,7 @@
 #include "ash/shell.h"
 #include "ash/style/system_shadow.h"
 #include "ash/system/status_area_widget.h"
+#include "ash/utility/forest_util.h"
 #include "ash/wm/overview/overview_controller.h"
 #include "ash/wm/overview/overview_observer.h"
 #include "base/functional/bind.h"
@@ -32,6 +33,7 @@
 #include "chromeos/constants/chromeos_features.h"
 #include "ui/aura/scoped_window_targeter.h"
 #include "ui/aura/window_targeter.h"
+#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
 #include "ui/compositor/animation_throughput_reporter.h"
 #include "ui/compositor/layer_animation_sequence.h"
 #include "ui/compositor/scoped_layer_animation_settings.h"
@@ -440,6 +442,9 @@
   // the visibility of shadow.
   void UpdateHighlightBorder(bool update_corner_radius);
 
+  // Returns the target background color for the hotseat.
+  SkColor GetBackgroundColor();
+
   void SetTranslucentBackground(const gfx::Rect& translucent_background_bounds);
 
   // Sets whether the background should be blurred as requested by the argument,
@@ -511,8 +516,9 @@
   OverviewController* overview_controller = Shell::Get()->overview_controller();
   if (overview_controller) {
     overview_controller->AddObserver(this);
-    if (overview_controller->InOverviewSession())
+    if (overview_controller->InOverviewSession() && !IsForestFeatureEnabled()) {
       ++blur_lock_;
+    }
   }
   DCHECK(scrollable_shelf_view);
   scrollable_shelf_view_ = scrollable_shelf_view;
@@ -592,6 +598,20 @@
   translucent_background_->SetBorder(std::move(border));
 }
 
+SkColor HotseatWidget::DelegateView::GetBackgroundColor() {
+  auto* widget = GetWidget();
+  CHECK(widget);
+  aura::Window* window = widget->GetNativeWindow();
+  // A forest session uses system-on-base.
+  if (IsForestFeatureEnabled() &&
+      OverviewController::Get()->InOverviewSession() &&
+      !SplitViewController::Get(window)->InSplitViewMode()) {
+    return widget->GetColorProvider()->GetColor(
+        cros_tokens::kCrosSysSystemOnBase);
+  }
+  return ShelfConfig::Get()->GetDefaultShelfColor(widget);
+}
+
 void HotseatWidget::DelegateView::SetTranslucentBackground(
     const gfx::Rect& background_bounds) {
   DCHECK(HotseatWidget::ShouldShowHotseatBackground());
@@ -607,12 +627,11 @@
                      hotseat_widget_->GetTranslucentBackgroundReportCallback());
   }
 
-  const auto* widget = GetWidget();
-  DCHECK(widget);
-  if (ShelfConfig::Get()->GetDefaultShelfColor(widget) != target_color_) {
+  SkColor background_color = GetBackgroundColor();
+  if (background_color != target_color_) {
     ui::ScopedLayerAnimationSettings color_animation_setter(animator);
     DoScopedAnimationSetting(&color_animation_setter);
-    target_color_ = ShelfConfig::Get()->GetDefaultShelfColor(widget);
+    target_color_ = background_color;
     translucent_background_->SetBackground(
         views::CreateSolidBackground(target_color_));
   }
@@ -696,6 +715,10 @@
 }
 
 void HotseatWidget::DelegateView::OnOverviewModeWillStart() {
+  // Forest uses background blur in overview.
+  if (IsForestFeatureEnabled()) {
+    return;
+  }
   DCHECK_LE(blur_lock_, 2);
 
   SetBackgroundBlur(false);
@@ -704,6 +727,10 @@
 
 void HotseatWidget::DelegateView::OnOverviewModeEndingAnimationComplete(
     bool canceled) {
+  // Forest uses background blur in overview.
+  if (IsForestFeatureEnabled()) {
+    return;
+  }
   DCHECK_GT(blur_lock_, 0);
 
   --blur_lock_;
diff --git a/ash/shelf/hotseat_widget_unittest.cc b/ash/shelf/hotseat_widget_unittest.cc
index 427f82e..08a861a6 100644
--- a/ash/shelf/hotseat_widget_unittest.cc
+++ b/ash/shelf/hotseat_widget_unittest.cc
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "ash/shelf/hotseat_widget.h"
+
 #include <memory>
 #include <tuple>
 #include <vector>
@@ -11,13 +13,13 @@
 #include "ash/app_list/views/app_list_view.h"
 #include "ash/assistant/assistant_controller_impl.h"
 #include "ash/constants/ash_features.h"
+#include "ash/constants/ash_switches.h"
 #include "ash/focus_cycler.h"
 #include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
 #include "ash/public/cpp/test/assistant_test_api.h"
 #include "ash/public/cpp/test/shell_test_api.h"
 #include "ash/shelf/drag_window_from_shelf_controller_test_api.h"
 #include "ash/shelf/home_button.h"
-#include "ash/shelf/hotseat_widget.h"
 #include "ash/shelf/scrollable_shelf_view.h"
 #include "ash/shelf/shelf.h"
 #include "ash/shelf/shelf_app_button.h"
@@ -215,8 +217,7 @@
     GetEventGenerator()->GestureTapAt(overview_button_center);
   }
 
- private:
-  friend class StackedHotseatWidgetTest;
+ protected:
   const ShelfAutoHideBehavior shelf_auto_hide_behavior_;
   const bool is_assistant_enabled_;
   const bool navigation_buttons_shown_in_tablet_mode_;
@@ -224,6 +225,23 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
+class HotseatWidgetForestTest : public HotseatWidgetTest {
+ public:
+  HotseatWidgetForestTest() { switches::SetIgnoreForestSecretKeyForTest(true); }
+
+  ~HotseatWidgetForestTest() {
+    switches::SetIgnoreForestSecretKeyForTest(false);
+  }
+
+  // HotseatWidgetTest:
+  void SetupFeatureLists() override {
+    scoped_feature_list_.InitWithFeatureStates(
+        {{features::kHideShelfControlsInTabletMode,
+          !navigation_buttons_shown_in_tablet_mode()},
+         {features::kForestFeature, true}});
+  }
+};
+
 class StackedHotseatWidgetTest : public HotseatWidgetTest {
  public:
   void SetupFeatureLists() override {
@@ -349,6 +367,15 @@
 
 INSTANTIATE_TEST_SUITE_P(
     All,
+    HotseatWidgetForestTest,
+    testing::Combine(
+        testing::Values(ShelfAutoHideBehavior::kNever,
+                        ShelfAutoHideBehavior::kAlways),
+        /*is_assistant_enabled*/ testing::Bool(),
+        /*navigation_buttons_shown_in_tablet_mode*/ testing::Bool()));
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
     StackedHotseatWidgetTest,
     testing::Combine(
         testing::Values(ShelfAutoHideBehavior::kNever,
@@ -988,6 +1015,31 @@
       GetShelfWidget()->hotseat_widget()->GetHotseatBackgroundBlurForTest());
 }
 
+TEST_P(HotseatWidgetForestTest, EnableBlurDuringOverviewMode) {
+  TabletModeControllerTestApi().EnterTabletMode();
+
+  const int expected_blur_radius = ShelfConfig::Get()->shelf_blur_radius();
+  ASSERT_EQ(
+      GetShelfWidget()->hotseat_widget()->GetHotseatBackgroundBlurForTest(),
+      expected_blur_radius);
+
+  // Go into overview and check that at the end of the animation, background
+  // blur is still enabled.
+  StartOverview();
+  WaitForOverviewAnimation(/*enter=*/true);
+  EXPECT_EQ(
+      GetShelfWidget()->hotseat_widget()->GetHotseatBackgroundBlurForTest(),
+      expected_blur_radius);
+
+  // Exit overview and check that at the end of the animation, background
+  // blur is still enabled.
+  EndOverview();
+  WaitForOverviewAnimation(/*enter=*/false);
+  EXPECT_EQ(
+      GetShelfWidget()->hotseat_widget()->GetHotseatBackgroundBlurForTest(),
+      expected_blur_radius);
+}
+
 // Tests that releasing the hotseat gesture below the threshold results in a
 // kHidden hotseat when the shelf is shown.
 TEST_P(HotseatWidgetTest, ReleasingSlowDragBelowThreshold) {
diff --git a/ash/system/mahi/mahi_constants.h b/ash/system/mahi/mahi_constants.h
index 4df1c9b..4dd828b 100644
--- a/ash/system/mahi/mahi_constants.h
+++ b/ash/system/mahi/mahi_constants.h
@@ -86,8 +86,14 @@
 inline constexpr char kMahiFeedbackHistogramName[] = "Ash.Mahi.Feedback";
 inline constexpr char kMahiButtonClickHistogramName[] =
     "Ash.Mahi.ButtonClicked";
+inline constexpr char kAnswerLoadingTimeHistogramName[] =
+    "Ash.Mahi.QuestionAnswer.LoadingTime";
+inline constexpr char kSummaryLoadingTimeHistogramName[] =
+    "Ash.Mahi.Summary.LoadingTime";
 inline constexpr char kMahiUserJourneyTimeHistogramName[] =
     "Ash.Mahi.UserJourneyTime";
+inline constexpr char kMahiQuestionSourceHistogramName[] =
+    "Ash.Mahi.QuestionSource";
 
 }  // namespace ash::mahi_constants
 
diff --git a/ash/system/mahi/mahi_panel_view_unittest.cc b/ash/system/mahi/mahi_panel_view_unittest.cc
index b7451fd..c36a3f3 100644
--- a/ash/system/mahi/mahi_panel_view_unittest.cc
+++ b/ash/system/mahi/mahi_panel_view_unittest.cc
@@ -101,8 +101,9 @@
 void ReturnDefaultAnswerAsyncly(
     base::test::TestFuture<void>& waiter,
     MahiResponseStatus status,
-    chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
-  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+    chromeos::MahiManager::MahiAnswerQuestionCallback callback,
+    base::TimeDelta delay = base::TimeDelta()) {
+  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
       FROM_HERE,
       base::BindOnce(
           [](base::OnceClosure unblock_closure, MahiResponseStatus status,
@@ -110,7 +111,8 @@
             std::move(callback).Run(u"fake answer", status);
             std::move(unblock_closure).Run();
           },
-          waiter.GetCallback(), status, std::move(callback)));
+          waiter.GetCallback(), status, std::move(callback)),
+      delay);
 }
 
 // Returns `kFakeOutlines` syncly.
@@ -142,8 +144,9 @@
 void ReturnDefaultSummaryAsyncly(
     base::test::TestFuture<void>& waiter,
     MahiResponseStatus status,
-    chromeos::MahiManager::MahiSummaryCallback callback) {
-  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+    chromeos::MahiManager::MahiSummaryCallback callback,
+    base::TimeDelta delay = base::TimeDelta()) {
+  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
       FROM_HERE,
       base::BindOnce(
           [](base::OnceClosure unblock_closure, MahiResponseStatus status,
@@ -151,7 +154,8 @@
             std::move(callback).Run(u"fake summary", status);
             std::move(unblock_closure).Run();
           },
-          waiter.GetCallback(), status, std::move(callback)));
+          waiter.GetCallback(), status, std::move(callback)),
+      delay);
 }
 
 // Returns a long summary.
@@ -223,16 +227,20 @@
 
   // Creates a widget hosting `MahiPanelView`. Recreates if there is one.
   void CreatePanelWidget() {
-    // Avoid creating a dangling pointer.
-    panel_view_ = nullptr;
-
-    widget_.reset();
+    ResetPanelWidget();
     widget_ = CreateFramelessTestWidget();
     widget_->SetFullscreen(true);
     panel_view_ = widget_->SetContentsView(
         std::make_unique<MahiPanelView>(&ui_controller_));
   }
 
+  void ResetPanelWidget() {
+    // Avoid creating a dangling pointer.
+    panel_view_ = nullptr;
+
+    widget_.reset();
+  }
+
   // Submit a test question by setting the text in the textfield and click send
   // button.
   void SubmitTestQuestion(const std::u16string& question = u"fake question") {
@@ -613,6 +621,67 @@
   EXPECT_FALSE(outlines_container->GetVisible());
 }
 
+TEST_F(MahiPanelViewTest, SummaryLoadingAnimationsMetricsRecord) {
+  // Reset the default panel to avoid unnecessary histogram record.
+  ResetPanelWidget();
+
+  base::HistogramTester histogram_tester;
+  auto delay_time = base::Milliseconds(100);
+
+  // Config the mock mahi manager to return a summary asyncly.
+  base::test::TestFuture<void> summary_waiter;
+  ON_CALL(mock_mahi_manager(), GetSummary)
+      .WillByDefault([&summary_waiter, delay_time](
+                         chromeos::MahiManager::MahiSummaryCallback callback) {
+        ReturnDefaultSummaryAsyncly(
+            summary_waiter, MahiResponseStatus::kSuccess, std::move(callback),
+            /*delay=*/delay_time);
+      });
+
+  MahiPanelView mahi_view(ui_controller());
+
+  histogram_tester.ExpectTimeBucketCount(
+      mahi_constants::kSummaryLoadingTimeHistogramName, delay_time, 0);
+
+  // Test that loading time metrics is recorded when summary is loaded.
+  ASSERT_TRUE(summary_waiter.WaitAndClear());
+  histogram_tester.ExpectTimeBucketCount(
+      mahi_constants::kSummaryLoadingTimeHistogramName, delay_time, 1);
+
+  // Test that loading time metrics is recorded when the content is refreshed.
+  ui_controller()->RefreshContents();
+  ASSERT_TRUE(summary_waiter.Wait());
+  histogram_tester.ExpectTimeBucketCount(
+      mahi_constants::kSummaryLoadingTimeHistogramName, delay_time, 2);
+}
+
+TEST_F(MahiPanelViewTest, AnswerLoadingAnimationsMetricsRecord) {
+  base::HistogramTester histogram_tester;
+  auto delay_time = base::Milliseconds(100);
+
+  // Config the mock mahi manager to return an answer asyncly.
+  base::test::TestFuture<void> answer_waiter;
+  EXPECT_CALL(mock_mahi_manager(), AnswerQuestion)
+      .WillOnce(
+          [&answer_waiter, delay_time](
+              const std::u16string& question, bool current_panel_content,
+              chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
+            ReturnDefaultAnswerAsyncly(
+                answer_waiter, MahiResponseStatus::kSuccess,
+                std::move(callback), /*delay=*/delay_time);
+          });
+
+  SubmitTestQuestion();
+
+  histogram_tester.ExpectTimeBucketCount(
+      mahi_constants::kAnswerLoadingTimeHistogramName, delay_time, 0);
+
+  // Test that loading time metrics is recorded when a question is answered.
+  ASSERT_TRUE(answer_waiter.Wait());
+  histogram_tester.ExpectTimeBucketCount(
+      mahi_constants::kAnswerLoadingTimeHistogramName, delay_time, 1);
+}
+
 // Tests that pressing on the send button with a valid textfield takes the user
 // to the Q&A View and the back to summary outlines button that appears on top
 // takes the user back to the main view.
@@ -1123,9 +1192,12 @@
                                          std::move(callback));
             });
 
+    base::HistogramTester histogram_tester;
     const std::u16string question(u"A question that brings errors");
     SubmitTestQuestion(question);
-
+    histogram_tester.ExpectBucketCount(
+        mahi_constants::kMahiQuestionSourceHistogramName,
+        MahiUiController::QuestionSource::kPanel, 1);
     Mock::VerifyAndClearExpectations(&mock_mahi_manager());
 
     // After a question is posted and before an answer is loaded, the Q&A view
@@ -1195,7 +1267,13 @@
                                  /*callback=*/_));
       EXPECT_CALL(mock_mahi_manager(), GetOutlines).Times(0);
       EXPECT_CALL(mock_mahi_manager(), GetSummary).Times(0);
+      histogram_tester.ExpectBucketCount(
+          mahi_constants::kMahiQuestionSourceHistogramName,
+          MahiUiController::QuestionSource::kRetry, 0);
       GetEventGenerator()->ClickLeftButton();
+      histogram_tester.ExpectBucketCount(
+          mahi_constants::kMahiQuestionSourceHistogramName,
+          MahiUiController::QuestionSource::kRetry, 1);
       Mock::VerifyAndClear(&mock_mahi_manager());
     }
 
diff --git a/ash/system/mahi/mahi_question_answer_view.cc b/ash/system/mahi/mahi_question_answer_view.cc
index b562e432..21a313f 100644
--- a/ash/system/mahi/mahi_question_answer_view.cc
+++ b/ash/system/mahi/mahi_question_answer_view.cc
@@ -24,6 +24,8 @@
 #include "base/check.h"
 #include "base/functional/bind.h"
 #include "base/logging.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/time/time.h"
 #include "chromeos/components/mahi/public/cpp/mahi_manager.h"
 #include "components/vector_icons/vector_icons.h"
 #include "ui/base/l10n/l10n_util.h"
@@ -239,6 +241,10 @@
     case MahiUiUpdateType::kAnswerLoaded:
       RemoveLoadingAnimatedImage();
 
+      base::UmaHistogramTimes(
+          mahi_constants::kAnswerLoadingTimeHistogramName,
+          base::TimeTicks::Now() - answer_start_loading_time_);
+
       AddChildView(
           CreateQuestionAnswerRow(update.GetAnswer(), /*is_question=*/false));
       return;
@@ -298,6 +304,8 @@
 
       answer_loading_animated_image_.SetView(answer_loading_animated_image);
 
+      answer_start_loading_time_ = base::TimeTicks::Now();
+
       return;
     }
     case MahiUiUpdateType::kQuestionReAsked: {
diff --git a/ash/system/mahi/mahi_question_answer_view.h b/ash/system/mahi/mahi_question_answer_view.h
index 018cd7d..9da617f 100644
--- a/ash/system/mahi/mahi_question_answer_view.h
+++ b/ash/system/mahi/mahi_question_answer_view.h
@@ -7,6 +7,7 @@
 
 #include "ash/ash_export.h"
 #include "ash/system/mahi/mahi_ui_controller.h"
+#include "base/time/time.h"
 #include "chromeos/components/mahi/public/cpp/mahi_manager.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/views/layout/flex_layout_view.h"
@@ -52,6 +53,10 @@
   // an answer. The image is created when waiting and destroyed when the answer
   // is loaded.
   views::ViewTracker answer_loading_animated_image_;
+
+  // Records the time when `answer_loading_animated_image_` starts showing and
+  // playing the animation. Used for metrics collection.
+  base::TimeTicks answer_start_loading_time_;
 };
 
 BEGIN_VIEW_BUILDER(ASH_EXPORT, MahiQuestionAnswerView, views::FlexLayoutView)
diff --git a/ash/system/mahi/mahi_ui_controller.cc b/ash/system/mahi/mahi_ui_controller.cc
index 76bfdf7a..a467f9a 100644
--- a/ash/system/mahi/mahi_ui_controller.cc
+++ b/ash/system/mahi/mahi_ui_controller.cc
@@ -4,8 +4,10 @@
 
 #include "ash/system/mahi/mahi_ui_controller.h"
 
+#include "ash/system/mahi/mahi_constants.h"
 #include "ash/system/mahi/mahi_ui_update.h"
 #include "base/logging.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/notreached.h"
 #include "chromeos/components/mahi/public/cpp/mahi_manager.h"
 #include "ui/views/view.h"
@@ -91,6 +93,9 @@
 void MahiUiController::SendQuestion(const std::u16string& question,
                                     bool current_panel_content,
                                     QuestionSource source) {
+  base::UmaHistogramEnumeration(
+      mahi_constants::kMahiQuestionSourceHistogramName, source);
+
   if (source != QuestionSource::kRetry) {
     most_recent_question_params_.emplace(question, current_panel_content);
   }
diff --git a/ash/system/mahi/mahi_ui_controller.h b/ash/system/mahi/mahi_ui_controller.h
index 1061b9b9..fb76c32e 100644
--- a/ash/system/mahi/mahi_ui_controller.h
+++ b/ash/system/mahi/mahi_ui_controller.h
@@ -48,6 +48,8 @@
   };
 
   // Lists question sources.
+  // Note: this should be kept in sync with `MahiQuestionSource` enum in
+  // tools/metrics/histograms/metadata/ash/enums.xml
   enum class QuestionSource {
     // From the Mahi menu view.
     kMenuView,
@@ -57,6 +59,8 @@
 
     // From the retry button.
     kRetry,
+
+    kMaxValue = kRetry,
   };
 
   MahiUiController();
diff --git a/ash/system/mahi/summary_outlines_section.cc b/ash/system/mahi/summary_outlines_section.cc
index 9c7efe39..8b21b68 100644
--- a/ash/system/mahi/summary_outlines_section.cc
+++ b/ash/system/mahi/summary_outlines_section.cc
@@ -13,6 +13,8 @@
 #include "ash/system/mahi/resources/grit/mahi_resources.h"
 #include "base/check.h"
 #include "base/check_is_test.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/time/time.h"
 #include "chromeos/components/mahi/public/cpp/mahi_manager.h"
 #include "chromeos/strings/grit/chromeos_strings.h"
 #include "chromeos/ui/vector_icons/vector_icons.h"
@@ -191,6 +193,9 @@
   outlines_loading_animated_image_->SetVisible(false);
   // TODO(b/330643995): Show the outlines section once it is ready.
   outlines_container_->SetVisible(false);
+
+  // TODO(b/333916944): Add metrics recording the outline loading animation time
+  // here.
 }
 
 void SummaryOutlinesSection::HandleSummaryLoaded(
@@ -199,6 +204,9 @@
   summary_label_->SetText(summary_text);
   summary_loading_animated_image_->Stop();
   summary_loading_animated_image_->SetVisible(false);
+
+  base::UmaHistogramTimes(mahi_constants::kSummaryLoadingTimeHistogramName,
+                          base::Time::Now() - summary_start_loading_time_);
 }
 
 void SummaryOutlinesSection::LoadSummaryAndOutlines() {
@@ -227,6 +235,8 @@
           *outlines_loading_animated_image_->animated_image()->skottie(),
           IDR_MAHI_LOADING_OUTLINES_ANIMATION));
 
+  summary_start_loading_time_ = base::Time::Now();
+
   ui_controller_->UpdateSummaryAndOutlines();
 }
 
diff --git a/ash/system/mahi/summary_outlines_section.h b/ash/system/mahi/summary_outlines_section.h
index 97cb10a..558396bb 100644
--- a/ash/system/mahi/summary_outlines_section.h
+++ b/ash/system/mahi/summary_outlines_section.h
@@ -12,6 +12,7 @@
 #include "ash/ash_export.h"
 #include "ash/system/mahi/mahi_ui_controller.h"
 #include "base/memory/raw_ptr.h"
+#include "base/time/time.h"
 #include "chromeos/components/mahi/public/cpp/mahi_manager.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/views/layout/box_layout_view.h"
@@ -57,6 +58,8 @@
   raw_ptr<views::AnimatedImageView> outlines_loading_animated_image_ = nullptr;
   raw_ptr<views::Label> summary_label_ = nullptr;
   raw_ptr<views::View> outlines_container_ = nullptr;
+
+  base::Time summary_start_loading_time_;
 };
 
 BEGIN_VIEW_BUILDER(ASH_EXPORT, SummaryOutlinesSection, views::BoxLayoutView)
diff --git a/ash/system/phonehub/phone_hub_ui_controller.cc b/ash/system/phonehub/phone_hub_ui_controller.cc
index b7f2cffc..df68c9f 100644
--- a/ash/system/phonehub/phone_hub_ui_controller.cc
+++ b/ash/system/phonehub/phone_hub_ui_controller.cc
@@ -351,6 +351,8 @@
         phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()
             ->LogPhoneHubUiStateUpdated(
                 phonehub::PhoneHubUiState::kDisconnected);
+        phone_hub_manager_->GetPhoneHubStructuredMetricsLogger()
+            ->ResetSessionId();
       }
       break;
     case UiState::kPhoneConnecting:
diff --git a/ash/system/unified/feature_tile.h b/ash/system/unified/feature_tile.h
index ea1be931..3818d3b 100644
--- a/ash/system/unified/feature_tile.h
+++ b/ash/system/unified/feature_tile.h
@@ -196,6 +196,7 @@
   TileType tile_type() { return type_; }
   bool is_icon_clickable() const { return is_icon_clickable_; }
   views::ImageButton* icon_button() { return icon_button_; }
+  views::FlexLayoutView* title_container() const { return title_container_; }
   views::Label* label() { return label_; }
   views::Label* sub_label() { return sub_label_; }
   views::ImageView* drill_in_arrow() { return drill_in_arrow_; }
diff --git a/ash/webui/shimless_rma/resources/fake_shimless_rma_service.ts b/ash/webui/shimless_rma/resources/fake_shimless_rma_service.ts
index 52bdb6dc..7eda4bd 100644
--- a/ash/webui/shimless_rma/resources/fake_shimless_rma_service.ts
+++ b/ash/webui/shimless_rma/resources/fake_shimless_rma_service.ts
@@ -549,7 +549,8 @@
    * The fake does not use the status list parameter, the fake data is never
    * updated.
    */
-  startCalibration(): Promise<{stateResult: StateResult}> {
+  startCalibration(_components: CalibrationComponentStatus[]):
+      Promise<{stateResult: StateResult}> {
     return this.getNextStateForMethod(
         'startCalibration', State.kCheckCalibration);
   }
diff --git a/ash/webui/shimless_rma/resources/reimaging_calibration_failed_page.ts b/ash/webui/shimless_rma/resources/reimaging_calibration_failed_page.ts
index bed9cba..b2991a4e 100644
--- a/ash/webui/shimless_rma/resources/reimaging_calibration_failed_page.ts
+++ b/ash/webui/shimless_rma/resources/reimaging_calibration_failed_page.ts
@@ -369,6 +369,10 @@
       disableNextButton(this);
     }
   }
+
+  getComponentsListForTesting(): CalibrationComponentStatus[] {
+    return this.getComponentsList();
+  }
 }
 
 declare global {
diff --git a/ash/wm/overview/overview_drop_target.cc b/ash/wm/overview/overview_drop_target.cc
index fbb05108..e4873441 100644
--- a/ash/wm/overview/overview_drop_target.cc
+++ b/ash/wm/overview/overview_drop_target.cc
@@ -143,6 +143,10 @@
   return nullptr;
 }
 
+bool OverviewDropTarget::ShouldHaveShadow() const {
+  return false;
+}
+
 void OverviewDropTarget::UpdateRoundedCornersAndShadow() {}
 
 void OverviewDropTarget::SetOpacity(float opacity) {}
diff --git a/ash/wm/overview/overview_drop_target.h b/ash/wm/overview/overview_drop_target.h
index 98f278d..b28daef 100644
--- a/ash/wm/overview/overview_drop_target.h
+++ b/ash/wm/overview/overview_drop_target.h
@@ -48,6 +48,7 @@
   void EnsureVisible() override;
   std::vector<OverviewFocusableView*> GetFocusableViews() const override;
   views::View* GetBackDropView() const override;
+  bool ShouldHaveShadow() const override;
   void UpdateRoundedCornersAndShadow() override;
   void SetOpacity(float opacity) override;
   float GetOpacity() const override;
diff --git a/ash/wm/overview/overview_group_item.cc b/ash/wm/overview/overview_group_item.cc
index d3f8773..fb9bea1 100644
--- a/ash/wm/overview/overview_group_item.cc
+++ b/ash/wm/overview/overview_group_item.cc
@@ -293,6 +293,10 @@
   return overview_group_container_view_;
 }
 
+bool OverviewGroupItem::ShouldHaveShadow() const {
+  return overview_items_.size() > 1u;
+}
+
 void OverviewGroupItem::UpdateRoundedCornersAndShadow() {
   for (const auto& overview_item : overview_items_) {
     overview_item->UpdateRoundedCorners();
@@ -422,12 +426,16 @@
 
   for (const auto& item : overview_items_) {
     if (item && item.get() != overview_item) {
+      // Remove the group-level shadow and apply it on the window-level to
+      // ensure that the shadow bounds get updated properly.
+      item->set_eligible_for_shadow_config(/*eligible_for_shadow_config=*/true);
+
       OverviewItemView* item_view = item->overview_item_view();
       item_view->ResetRoundedCorners();
     }
   }
 
-  overview_grid_->PositionWindows(/*animate=*/false);
+  overview_grid_->PositionWindows(/*animate=*/true);
 }
 
 void OverviewGroupItem::HandleDragEvent(const gfx::PointF& location_in_screen) {
@@ -445,7 +453,7 @@
       desks_util::GetActiveDeskContainerForRoot(overview_grid_->root_window()),
       "OverviewGroupItemWidget", /*accept_events=*/true));
 
-  ConfigureTheShadow();
+  CreateShadow();
 
   overview_group_container_view_ = item_widget_->SetContentsView(
       std::make_unique<OverviewGroupContainerView>(this));
diff --git a/ash/wm/overview/overview_group_item.h b/ash/wm/overview/overview_group_item.h
index d23d350..16e2aa6 100644
--- a/ash/wm/overview/overview_group_item.h
+++ b/ash/wm/overview/overview_group_item.h
@@ -57,6 +57,7 @@
   void EnsureVisible() override;
   std::vector<OverviewFocusableView*> GetFocusableViews() const override;
   views::View* GetBackDropView() const override;
+  bool ShouldHaveShadow() const override;
   void UpdateRoundedCornersAndShadow() override;
   void SetOpacity(float opacity) override;
   float GetOpacity() const override;
diff --git a/ash/wm/overview/overview_item.cc b/ash/wm/overview/overview_item.cc
index b1b4a2d4..46ec340 100644
--- a/ash/wm/overview/overview_item.cc
+++ b/ash/wm/overview/overview_item.cc
@@ -577,6 +577,10 @@
   return overview_item_view_->backdrop_view();
 }
 
+bool OverviewItem::ShouldHaveShadow() const {
+  return eligible_for_shadow_config_;
+}
+
 void OverviewItem::UpdateRoundedCornersAndShadow() {
   UpdateRoundedCorners();
 
@@ -1148,7 +1152,7 @@
   wm::SetWindowVisibilityAnimationTransition(widget_window, wm::ANIMATE_NONE);
 
   if (eligible_for_shadow_config_) {
-    ConfigureTheShadow();
+    CreateShadow();
   }
 
   overview_item_view_ =
diff --git a/ash/wm/overview/overview_item.h b/ash/wm/overview/overview_item.h
index 10a1f09..2d0b142f 100644
--- a/ash/wm/overview/overview_item.h
+++ b/ash/wm/overview/overview_item.h
@@ -72,6 +72,10 @@
 
   OverviewItemView* overview_item_view() { return overview_item_view_; }
 
+  void set_eligible_for_shadow_config(bool eligible_for_shadow_config) {
+    eligible_for_shadow_config_ = eligible_for_shadow_config;
+  }
+
   // Handles events forwarded from the contents view.
   void OnFocusedViewActivated();
   void OnFocusedViewClosed();
@@ -107,6 +111,7 @@
   gfx::RectF GetTransformedBounds() const override;
   std::vector<OverviewFocusableView*> GetFocusableViews() const override;
   views::View* GetBackDropView() const override;
+  bool ShouldHaveShadow() const override;
   void UpdateRoundedCornersAndShadow() override;
   void SetOpacity(float opacity) override;
   float GetOpacity() const override;
@@ -233,8 +238,10 @@
   // If true, `shadow_` is eligible to be created, false otherwise. The shadow
   // should not be created if `this` is hosted by an `OverviewGroupItem`
   // together with another `OverviewItem` (the group-level shadow will be
-  // installed instead).
-  const bool eligible_for_shadow_config_;
+  // installed instead). However if a window inside an `OverviewGroupItem` is
+  // destroyed, `eligible_for_shadow_config_` is set to true to ensure the
+  // shadow bounds get updated correctly.
+  bool eligible_for_shadow_config_;
 
   // The view associated with |item_widget_|. Contains a title, close button and
   // maybe a backdrop. Forwards certain events to |this|.
diff --git a/ash/wm/overview/overview_item_base.cc b/ash/wm/overview/overview_item_base.cc
index ec9bdf9..5d655cf 100644
--- a/ash/wm/overview/overview_item_base.cc
+++ b/ash/wm/overview/overview_item_base.cc
@@ -68,16 +68,26 @@
 }
 
 void OverviewItemBase::RefreshShadowVisuals(bool shadow_visible) {
-  // Shadow is normally turned off during animations and reapplied when on
-  // animation complete. On destruction, `shadow_` is cleaned up before
-  // `transform_window_`, which may call this function, so early exit if
-  // `shadow_` is nullptr.
+  const bool should_have_shadow = ShouldHaveShadow();
+  if (should_have_shadow != !!shadow_) {
+    if (should_have_shadow) {
+      CreateShadow();
+    } else {
+      shadow_.reset();
+    }
+  }
+
+  // On destruction, `shadow_` is cleaned up before `transform_window_`, which
+  // may call this function, so early exit if `shadow_` is nullptr.
   if (!shadow_) {
     return;
   }
 
   const gfx::RectF shadow_bounds_in_screen = target_bounds_;
   auto* shadow_layer = shadow_->GetLayer();
+
+  // Shadow is normally turned off during animations and reapplied when on
+  // animation complete.
   if (!shadow_visible || shadow_bounds_in_screen.IsEmpty()) {
     shadow_layer->SetVisible(false);
     return;
@@ -93,7 +103,9 @@
 }
 
 void OverviewItemBase::UpdateShadowTypeForDrag(bool is_dragging) {
-  shadow_->SetType(is_dragging ? kDraggedShadowType : kDefaultShadowType);
+  if (shadow_) {
+    shadow_->SetType(is_dragging ? kDraggedShadowType : kDefaultShadowType);
+  }
 }
 
 void OverviewItemBase::HandleGestureEventForTabletModeLayout(
@@ -241,7 +253,7 @@
   return params;
 }
 
-void OverviewItemBase::ConfigureTheShadow() {
+void OverviewItemBase::CreateShadow() {
   shadow_ = SystemShadow::CreateShadowOnNinePatchLayer(
       kDefaultShadowType, SystemShadow::LayerRecreatedCallback());
   auto* shadow_layer = shadow_->GetLayer();
diff --git a/ash/wm/overview/overview_item_base.h b/ash/wm/overview/overview_item_base.h
index 62ac1b8..830e6cf 100644
--- a/ash/wm/overview/overview_item_base.h
+++ b/ash/wm/overview/overview_item_base.h
@@ -204,6 +204,9 @@
   // Returns the backdrop view of `this`.
   virtual views::View* GetBackDropView() const = 0;
 
+  // Returns true if `shadow_` should be created on the item, false otherwise.
+  virtual bool ShouldHaveShadow() const = 0;
+
   // Updates the rounded corners and shadow on `this`.
   virtual void UpdateRoundedCornersAndShadow() = 0;
 
@@ -307,8 +310,10 @@
     target_bounds_ = target_bounds;
   }
 
+  SystemShadow* shadow_for_testing() { return shadow_.get(); }
+
   gfx::Rect get_shadow_content_bounds_for_testing() const {
-    return shadow_.get()->GetContentBounds();
+    return shadow_ ? shadow_.get()->GetContentBounds() : gfx::Rect();
   }
 
   RoundedLabelWidget* get_cannot_snap_widget_for_testing() {
@@ -328,7 +333,7 @@
 
   // Creates the `shadow_` and stacks the shadow layer to be at the bottom after
   // `item_widget_` has been created.
-  void ConfigureTheShadow();
+  void CreateShadow();
 
   // Drag event can be handled differently based on the concreate instance of
   // `this`. For `OverviewItem`, the drag will be on window-level. For
diff --git a/ash/wm/snap_group/snap_group.cc b/ash/wm/snap_group/snap_group.cc
index 21994c1..d55fdd0 100644
--- a/ash/wm/snap_group/snap_group.cc
+++ b/ash/wm/snap_group/snap_group.cc
@@ -139,22 +139,25 @@
   SnapGroupController::Get()->RemoveSnapGroup(this);
 }
 
-void SnapGroup::OnWindowParentChanged(aura::Window* window,
-                                      aura::Window* parent) {
+void SnapGroup::OnWindowAddedToRootWindow(aura::Window* window) {
   DCHECK(window == window1_ || window == window2_);
   // Skip any recursive updates during the other window move.
-  if (is_moving_display_ || !parent) {
+  if (is_moving_display_) {
     return;
   }
   base::AutoReset<bool> lock(&is_moving_display_, true);
   // Hide the divider, then move the other window to the same display as the
   // moved `window`.
+  const bool old_visibility = snap_group_divider_.divider_widget()->IsVisible();
   snap_group_divider_.SetVisible(false);
   window_util::MoveWindowToDisplay(
       window == window1_ ? window2_ : window1_,
-      display::Screen::GetScreen()->GetDisplayNearestWindow(parent).id());
-  // Re-show the divider after both windows are moved to the target display.
-  snap_group_divider_.SetVisible(true);
+      display::Screen::GetScreen()
+          ->GetDisplayNearestWindow(window->GetRootWindow())
+          .id());
+  // Re-show the divider if needed after both windows are moved to the target
+  // display.
+  snap_group_divider_.SetVisible(old_visibility);
   ApplyPrimarySnapRatio(WindowState::Get(window1_)->snap_ratio().value_or(
       chromeos::kDefaultSnapRatio));
 }
diff --git a/ash/wm/snap_group/snap_group.h b/ash/wm/snap_group/snap_group.h
index fddf31c..517c5e0 100644
--- a/ash/wm/snap_group/snap_group.h
+++ b/ash/wm/snap_group/snap_group.h
@@ -63,8 +63,7 @@
 
   // aura::WindowObserver:
   void OnWindowDestroying(aura::Window* window) override;
-  void OnWindowParentChanged(aura::Window* window,
-                             aura::Window* parent) override;
+  void OnWindowAddedToRootWindow(aura::Window* window) override;
 
   // WindowStateObserver:
   void OnPreWindowStateTypeChange(WindowState* window_state,
diff --git a/ash/wm/snap_group/snap_group_unittest.cc b/ash/wm/snap_group/snap_group_unittest.cc
index c6785d3b..1d886c0 100644
--- a/ash/wm/snap_group/snap_group_unittest.cc
+++ b/ash/wm/snap_group/snap_group_unittest.cc
@@ -3299,6 +3299,18 @@
   std::unique_ptr<aura::Window> w1(CreateAppWindow());
   SnapTwoTestWindows(w0.get(), w1.get());
 
+  // Create more windows to ensure the position of the `OverviewGroupItem` needs
+  // to be updated during the Overview grid re-layout since the Overview grid
+  // layout is left-aligned.
+  std::unique_ptr<aura::Window> w2(
+      CreateAppWindow(gfx::Rect(100, 100, 200, 100)));
+  std::unique_ptr<aura::Window> w3(
+      CreateAppWindow(gfx::Rect(200, 200, 100, 200)));
+  std::unique_ptr<aura::Window> w4(
+      CreateAppWindow(gfx::Rect(100, 200, 200, 300)));
+  std::unique_ptr<aura::Window> w5(
+      CreateAppWindow(gfx::Rect(200, 100, 300, 200)));
+
   OverviewController* overview_controller = Shell::Get()->overview_controller();
   overview_controller->StartOverview(OverviewStartAction::kTests,
                                      OverviewEnterExitType::kImmediateEnter);
@@ -3307,18 +3319,28 @@
       GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
   ASSERT_TRUE(overview_grid);
   const auto& window_list = overview_grid->window_list();
-  ASSERT_EQ(window_list.size(), 1u);
+  ASSERT_EQ(window_list.size(), 5u);
+
+  OverviewGroupItem* overview_group_item =
+      static_cast<OverviewGroupItem*>(window_list[4].get());
+  const auto& overview_items =
+      overview_group_item->overview_items_for_testing();
+  ASSERT_EQ(overview_items.size(), 2u);
 
   w0.reset();
-  EXPECT_EQ(window_list.size(), 1u);
+  EXPECT_EQ(window_list.size(), 5u);
+  EXPECT_EQ(overview_items.size(), 1u);
 
-  // Verify that the shadow bounds will be refreshed to fit with the remaining
+  // Verify that the group-level shadow will be reset and the window-level
+  // shadow bounds of the remaining item is refreshed to fit with the remaining
   // item.
-  auto& overview_item = window_list[0];
-  const auto shadow_content_bounds =
-      overview_item->get_shadow_content_bounds_for_testing();
-  EXPECT_EQ(shadow_content_bounds.size(),
-            gfx::ToRoundedSize(overview_item->target_bounds().size()));
+  auto* group_shadow = overview_group_item->shadow_for_testing();
+  EXPECT_FALSE(group_shadow);
+
+  auto* window1_shadow = overview_items[0]->shadow_for_testing();
+  ASSERT_TRUE(window1_shadow);
+  EXPECT_EQ(gfx::ToRoundedSize(overview_group_item->target_bounds().size()),
+            window1_shadow->GetContentBounds().size());
 }
 
 // Tests the basic functionality of focus cycling in overview through tabbing,
diff --git a/base/message_loop/message_pump_win.cc b/base/message_loop/message_pump_win.cc
index f212eb9a..b136708 100644
--- a/base/message_loop/message_pump_win.cc
+++ b/base/message_loop/message_pump_win.cc
@@ -160,7 +160,7 @@
   // See MessageLoopTest.PostDelayedTaskFromSystemPump for an example.
   // TODO(gab): This could potentially be replaced by a ForegroundIdleProc hook
   // if Windows ends up being the only platform requiring ScheduleDelayedWork().
-  if (nested_state_ != NestedState::kNone &&
+  if (nested_state_ == NestedState::kNestedNativeLoopAnnounced &&
       !native_msg_scheduled_.load(std::memory_order_relaxed)) {
     ScheduleNativeTimer(next_work_info);
   }
diff --git a/base/message_loop/message_pump_win.h b/base/message_loop/message_pump_win.h
index 33ffd0b4..3607f768 100644
--- a/base/message_loop/message_pump_win.h
+++ b/base/message_loop/message_pump_win.h
@@ -187,8 +187,7 @@
     // There are no nested message loops running.
     kNone,
     // kMsgHaveWork was pumped from a native queue. The state will return to
-    // `kNormal` whenever DoRunLoop() regains control. In this state,
-    // ScheduleDelayedWork() will start a native timer.
+    // `kNormal` whenever DoRunLoop() regains control.
     //
     // It is reset to `kNone` when DoRunLoop() gets control back after
     // ProcessNextWindowsMessage() or DoWork().
@@ -196,9 +195,8 @@
     // HandleNestedNativeLoopWithApplicationTasks(true) was called (when a
     // `ScopedAllowApplicationTasksInNativeNestedLoop` is instantiated). When
     // running with `event_`, switches to pumping `kMsgHaveWork` MSGs when there
-    // are application tasks to be done during native runloops. Is a 'superset'
-    // of `kNestedNativeLoopDetected`, and will also start a native timer when
-    // ScheduleDelayedWork() is called.
+    // are application tasks to be done during native runloops. In this state,
+    // ScheduleDelayedWork() will start a native timer.
     //
     // It is reset to `kNone` when:
     //   - DoRunLoop() gets control back after ProcessNextWindowsMessage().
diff --git a/chrome/VERSION b/chrome/VERSION
index 80e0cc2..2d2597a 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=125
 MINOR=0
-BUILD=6416
+BUILD=6417
 PATCH=0
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/PersistedTabDataTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/PersistedTabDataTest.java
index a567a5876..570600cc 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/PersistedTabDataTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/PersistedTabDataTest.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.tab.state;
 
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -45,6 +46,8 @@
 
     @Mock private PersistedTabData.Natives mPersistedTabDataJni;
 
+    @Mock Tab mTab;
+
     @Rule public JniMocker jniMocker = new JniMocker();
 
     @Before
@@ -171,6 +174,69 @@
         verify(mShoppingPersistedTabDataMock, times(1)).disableSaving();
     }
 
+    @SmallTest
+    @Test
+    public void testUninitializedTab() throws TimeoutException {
+        doReturn(false).when(mTab).isInitialized();
+        doReturn(false).when(mTab).isDestroyed();
+        doReturn(false).when(mTab).isCustomTab();
+        CallbackHelper helper = new CallbackHelper();
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PersistedTabData.from(
+                            mTab,
+                            null,
+                            MockPersistedTabData.class,
+                            (res) -> {
+                                Assert.assertNull(res);
+                                helper.notifyCalled();
+                            });
+                });
+        helper.waitForCallback(0);
+    }
+
+    @SmallTest
+    @Test
+    public void testDestroyedTab() throws TimeoutException {
+        doReturn(true).when(mTab).isInitialized();
+        doReturn(true).when(mTab).isDestroyed();
+        doReturn(false).when(mTab).isCustomTab();
+        CallbackHelper helper = new CallbackHelper();
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PersistedTabData.from(
+                            mTab,
+                            null,
+                            MockPersistedTabData.class,
+                            (res) -> {
+                                Assert.assertNull(res);
+                                helper.notifyCalled();
+                            });
+                });
+        helper.waitForCallback(0);
+    }
+
+    @SmallTest
+    @Test
+    public void testCustomTab() throws TimeoutException {
+        doReturn(true).when(mTab).isInitialized();
+        doReturn(false).when(mTab).isDestroyed();
+        doReturn(true).when(mTab).isCustomTab();
+        CallbackHelper helper = new CallbackHelper();
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PersistedTabData.from(
+                            mTab,
+                            null,
+                            MockPersistedTabData.class,
+                            (res) -> {
+                                Assert.assertNull(res);
+                                helper.notifyCalled();
+                            });
+                });
+        helper.waitForCallback(0);
+    }
+
     static class ThreadVerifierMockPersistedTabData extends MockPersistedTabData {
         ThreadVerifierMockPersistedTabData(Tab tab) {
             super(
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index baafaa7..c232eca 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -5638,6 +5638,9 @@
           <message name="IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS" desc="Permission string for chrome.os.diagnostcs API.">
             Run ChromeOS Flex diagnostic tests
           </message>
+          <message name="IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS_NETWORK_INFO_FOR_MLAB" desc="Permission string for chrome.os.diagnostics.network_info_mlab API.">
+            Collect IP address and network measurement results for Measurement Lab, according to their privacy policy (measurementlab.net/privacy)
+          </message>
           <message name="IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_EVENTS" desc="Permission string for chrome.os.events API.">
             Subscribe to ChromeOS Flex system events
           </message>
@@ -5664,6 +5667,9 @@
           <message name="IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS" desc="Permission string for chrome.os.diagnostcs API.">
             Run ChromeOS diagnostic tests
           </message>
+          <message name="IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS_NETWORK_INFO_FOR_MLAB" desc="Permission string for chrome.os.diagnostics.network_info_mlab API.">
+            Collect IP address and network measurement results for Measurement Lab, according to their privacy policy (measurementlab.net/privacy)
+          </message>
           <message name="IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_EVENTS" desc="Permission string for chrome.os.events API.">
             Subscribe to ChromeOS system events
           </message>
diff --git a/chrome/app/generated_resources_grd/IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS_NETWORK_INFO_FOR_MLAB.png.sha1 b/chrome/app/generated_resources_grd/IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS_NETWORK_INFO_FOR_MLAB.png.sha1
new file mode 100644
index 0000000..2ad4b14d
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS_NETWORK_INFO_FOR_MLAB.png.sha1
@@ -0,0 +1 @@
+341f40a9ceb9d9b9f2d738665fe364ae136565a9
diff --git a/chrome/app/settings_chromium_strings.grdp b/chrome/app/settings_chromium_strings.grdp
index 7d8bf0b..3d155e7 100644
--- a/chrome/app/settings_chromium_strings.grdp
+++ b/chrome/app/settings_chromium_strings.grdp
@@ -265,7 +265,7 @@
     Warns you about dangerous sites, even ones Google didn't know about before, by analyzing more data from sites than standard protection. You can choose to skip Chromium warnings.
   </message>
   <message name="IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL" desc="The text for a link to a help center article that gives more information about Safe Browsing.">
-    Learn more about <ph name="BEGIN_LINK">&lt;a href="$1" target=&quot;_blank&quot;&gt;<ex>&lt;a href="$1" target=&quot;_blank&quot;&gt;</ex></ph>how Chromium keeps your data private<ph name="END_LINK">&lt;/a&gt;<ex>&lt;/a&gt;</ex></ph>
+    Learn more about <ph name="BEGIN_LINK">&lt;a href="#" id="enhancedProtectionLearnMoreLink" on-click="onEnhancedProtectionLearnMoreClick_"&gt;</ph>how Chromium keeps your data private<ph name="END_LINK">&lt;/a&gt;<ex>&lt;/a&gt;</ex></ph>
   </message>
   <message name="IDS_SETTINGS_SECURE_DNS_DESCRIPTION" desc="Secondary, continued explanation of secure DNS in Privacy options">
     Make it harder for people with access to your internet traffic to see which sites you visit. Chromium uses a secure connection to look up a site's IP address in the DNS (Domain Name System).
diff --git a/chrome/app/settings_chromium_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1 b/chrome/app/settings_chromium_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1
index b029d06..1129811 100644
--- a/chrome/app/settings_chromium_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1
+++ b/chrome/app/settings_chromium_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1
@@ -1 +1 @@
-ca5b85e661217f84b4379f7ca38c12a5cf007755
\ No newline at end of file
+a0c8c5f67627143214bfdfeb033eb56f46618d11
\ No newline at end of file
diff --git a/chrome/app/settings_google_chrome_strings.grdp b/chrome/app/settings_google_chrome_strings.grdp
index e373d7a97..7f59defb 100644
--- a/chrome/app/settings_google_chrome_strings.grdp
+++ b/chrome/app/settings_google_chrome_strings.grdp
@@ -258,7 +258,7 @@
     Warns you about dangerous sites, even ones Google didn't know about before, by analyzing more data from sites than standard protection. You can choose to skip Chrome warnings.
   </message>
   <message name="IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL" desc="The text for a link to a help center article that gives more information about Safe Browsing.">
-    Learn more about <ph name="BEGIN_LINK">&lt;a href="$1" target=&quot;_blank&quot;&gt;<ex>&lt;a href="$1" target=&quot;_blank&quot;&gt;</ex></ph>how Chrome keeps your data private<ph name="END_LINK">&lt;/a&gt;<ex>&lt;/a&gt;</ex></ph>
+    Learn more about <ph name="BEGIN_LINK">&lt;a href="#" id="enhancedProtectionLearnMoreLink" on-click="onEnhancedProtectionLearnMoreClick_"&gt;</ph>how Chrome keeps your data private<ph name="END_LINK">&lt;/a&gt;<ex>&lt;/a&gt;</ex></ph>
   </message>
   <message name="IDS_SETTINGS_SECURE_DNS_DESCRIPTION" desc="Secondary, continued explanation of secure DNS in Privacy options">
     Make it harder for people with access to your internet traffic to see which sites you visit. Chrome uses a secure connection to look up a site's IP address in the DNS (Domain Name System).
diff --git a/chrome/app/settings_google_chrome_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1 b/chrome/app/settings_google_chrome_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1
index a0b16c50..8909aa0 100644
--- a/chrome/app/settings_google_chrome_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1
+++ b/chrome/app/settings_google_chrome_strings_grdp/IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL.png.sha1
@@ -1 +1 @@
-55a272c424b34769b71a9b3604682eb8a39d3eca
\ No newline at end of file
+2d95dc82f0f93cd6a1c4b3460f3600ab3d81e6fe
\ No newline at end of file
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 6fc5c7a..ff3c5fb3 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -6139,6 +6139,10 @@
                                     kOmniboxStarterPackExpansionVariations,
                                     "StarterPackExpansion")},
 
+    {"omnibox-starter-pack-iph", flag_descriptions::kOmniboxStarterPackIPHName,
+     flag_descriptions::kOmniboxStarterPackIPHDescription, kOsDesktop,
+     FEATURE_VALUE_TYPE(omnibox::kStarterPackIPH)},
+
 #endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) ||
         // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_FUCHSIA)
 
diff --git a/chrome/browser/ash/arc/input_overlay/arc_input_overlay_manager_unittest.cc b/chrome/browser/ash/arc/input_overlay/arc_input_overlay_manager_unittest.cc
index 7cc42a0d..3be52a4 100644
--- a/chrome/browser/ash/arc/input_overlay/arc_input_overlay_manager_unittest.cc
+++ b/chrome/browser/ash/arc/input_overlay/arc_input_overlay_manager_unittest.cc
@@ -7,6 +7,7 @@
 #include <memory>
 
 #include "ash/components/arc/test/fake_compatibility_mode_instance.h"
+#include "ash/public/cpp/arc_game_controls_flag.h"
 #include "ash/shell.h"
 #include "ash/test/ash_test_base.h"
 #include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
@@ -14,6 +15,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/ash/app_list/arc/arc_app_test.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/test/arc_test_window.h"
 #include "chrome/browser/ash/arc/input_overlay/test/event_capturer.h"
@@ -46,6 +48,25 @@
 
 constexpr const gfx::Rect window_bounds = gfx::Rect(10, 10, 100, 100);
 
+// Simulates the feature (if `is_feature` is true) or hint (if `is_feature` is
+// false) toggle on `window`. When toggling the feature, it also
+// toggles the hint.
+void ToggleGameControls(aura::Window* window, bool is_feature) {
+  const bool toggle_on =
+      !IsFlagSet(window->GetProperty(ash::kArcGameControlsFlagsKey),
+                 is_feature ? ash::ArcGameControlsFlag::kEnabled
+                            : ash::ArcGameControlsFlag::kHint);
+  window->SetProperty(
+      ash::kArcGameControlsFlagsKey,
+      UpdateFlag(window->GetProperty(ash::kArcGameControlsFlagsKey),
+                 is_feature
+                     ? static_cast<ash::ArcGameControlsFlag>(
+                           /*enable_flag=*/ash::ArcGameControlsFlag::kEnabled |
+                           ash::ArcGameControlsFlag::kHint)
+                     : ash::ArcGameControlsFlag::kHint,
+                 toggle_on));
+}
+
 }  // namespace
 
 class TestArcInputOverlayManager : public ArcInputOverlayManager {
@@ -691,6 +712,174 @@
   }
 }
 
+TEST_P(VersionArcInputOverlayManagerTest, TestHistograms) {
+  if (!IsBetaVersion()) {
+    return;
+  }
+
+  base::HistogramTester histograms;
+  std::map<MappingSource, int> expected_histogram_values_for_hint_on;
+  std::map<MappingSource, int> expected_histogram_values_for_hint_off;
+  std::map<MappingSource, int> expected_histogram_values_for_feature_on;
+  std::map<MappingSource, int> expected_histogram_values_for_feature_off;
+
+  const std::string feature_on_histogram_name =
+      BuildGameControlsHistogramName(
+          base::JoinString(
+              std::vector<std::string>{kFeatureHistogramName,
+                                       kToggleWithMappingSourceHistogram},
+              ""))
+          .append(kGameControlsHistogramSeparator)
+          .append(kToggleOnHistogramName);
+
+  const std::string feature_off_histogram_name =
+      BuildGameControlsHistogramName(
+          base::JoinString(
+              std::vector<std::string>{kFeatureHistogramName,
+                                       kToggleWithMappingSourceHistogram},
+              ""))
+          .append(kGameControlsHistogramSeparator)
+          .append(kToggleOffHistogramName);
+
+  const std::string hint_on_histogram_name =
+      BuildGameControlsHistogramName(
+          base::JoinString(
+              std::vector<std::string>{kHintHistogramName,
+                                       kToggleWithMappingSourceHistogram},
+              ""))
+          .append(kGameControlsHistogramSeparator)
+          .append(kToggleOnHistogramName);
+
+  const std::string hint_off_histogram_name =
+      BuildGameControlsHistogramName(
+          base::JoinString(
+              std::vector<std::string>{kHintHistogramName,
+                                       kToggleWithMappingSourceHistogram},
+              ""))
+          .append(kGameControlsHistogramSeparator)
+          .append(kToggleOffHistogramName);
+
+  // 1. Test with the default mapping.
+  auto arc_window = CreateArcWindowSyncAndWait(
+      task_environment(), ash::Shell::GetPrimaryRootWindow(), window_bounds,
+      kEnabledPackageName);
+  // Toggle hint off.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/false);
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_off,
+                        MappingSource::kDefault);
+  VerifyHistogramValues(histograms, hint_off_histogram_name,
+                        expected_histogram_values_for_hint_off);
+  // Toggle hint on.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/false);
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_on,
+                        MappingSource::kDefault);
+  VerifyHistogramValues(histograms, hint_on_histogram_name,
+                        expected_histogram_values_for_hint_on);
+  // Toggle feature off.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/true);
+  MapIncreaseValueByOne(expected_histogram_values_for_feature_off,
+                        MappingSource::kDefault);
+  // Hint is also toggle off with feature toggle off.
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_off,
+                        MappingSource::kDefault);
+  VerifyHistogramValues(histograms, feature_off_histogram_name,
+                        expected_histogram_values_for_feature_off);
+  VerifyHistogramValues(histograms, hint_off_histogram_name,
+                        expected_histogram_values_for_hint_off);
+  // Toggle feature on.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/true);
+  MapIncreaseValueByOne(expected_histogram_values_for_feature_on,
+                        MappingSource::kDefault);
+  // Hint is also toggle on with feature toggle on.
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_on,
+                        MappingSource::kDefault);
+  VerifyHistogramValues(histograms, feature_on_histogram_name,
+                        expected_histogram_values_for_feature_on);
+  VerifyHistogramValues(histograms, hint_on_histogram_name,
+                        expected_histogram_values_for_hint_on);
+
+  // 2. Add the default mapping with extra user-added mapping.
+  auto* injector = GetTouchInjector(arc_window->GetNativeWindow());
+  injector->AddNewAction(ActionType::TAP,
+                         arc_window->GetNativeWindow()->bounds().CenterPoint());
+  // Toggle hint off.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/false);
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_off,
+                        MappingSource::kDefaultAndUserAdded);
+  VerifyHistogramValues(histograms, hint_off_histogram_name,
+                        expected_histogram_values_for_hint_off);
+  // Toggle hint on.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/false);
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_on,
+                        MappingSource::kDefaultAndUserAdded);
+  VerifyHistogramValues(histograms, hint_on_histogram_name,
+                        expected_histogram_values_for_hint_on);
+  // Toggle feature off.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/true);
+  MapIncreaseValueByOne(expected_histogram_values_for_feature_off,
+                        MappingSource::kDefaultAndUserAdded);
+  // Hint is also toggle off with feature toggle off.
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_off,
+                        MappingSource::kDefaultAndUserAdded);
+  VerifyHistogramValues(histograms, feature_off_histogram_name,
+                        expected_histogram_values_for_feature_off);
+  VerifyHistogramValues(histograms, hint_off_histogram_name,
+                        expected_histogram_values_for_hint_off);
+  // Toggle feature on.
+  ToggleGameControls(arc_window->GetNativeWindow(), /*is_feature=*/true);
+  MapIncreaseValueByOne(expected_histogram_values_for_feature_on,
+                        MappingSource::kDefaultAndUserAdded);
+  // Hint is also toggle on with feature toggle on.
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_on,
+                        MappingSource::kDefaultAndUserAdded);
+  VerifyHistogramValues(histograms, feature_on_histogram_name,
+                        expected_histogram_values_for_feature_on);
+  VerifyHistogramValues(histograms, hint_on_histogram_name,
+                        expected_histogram_values_for_hint_on);
+
+  // 3. Test with user-added mapping only.
+  auto game_window = CreateArcWindowSyncAndWait(
+      task_environment(), ash::Shell::GetPrimaryRootWindow(), window_bounds,
+      kRandomGamePackageName);
+  injector = GetTouchInjector(game_window->GetNativeWindow());
+  injector->AddNewAction(
+      ActionType::TAP, game_window->GetNativeWindow()->bounds().CenterPoint());
+  // Toggle hint off.
+  ToggleGameControls(game_window->GetNativeWindow(), /*is_feature=*/false);
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_off,
+                        MappingSource::kUserAdded);
+  VerifyHistogramValues(histograms, hint_off_histogram_name,
+                        expected_histogram_values_for_hint_off);
+  // Toggle hint on.
+  ToggleGameControls(game_window->GetNativeWindow(), /*is_feature=*/false);
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_on,
+                        MappingSource::kUserAdded);
+  VerifyHistogramValues(histograms, hint_on_histogram_name,
+                        expected_histogram_values_for_hint_on);
+  // Toggle feature off.
+  ToggleGameControls(game_window->GetNativeWindow(), /*is_feature=*/true);
+  MapIncreaseValueByOne(expected_histogram_values_for_feature_off,
+                        MappingSource::kUserAdded);
+  // Hint is also toggle off with feature toggle off.
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_off,
+                        MappingSource::kUserAdded);
+  VerifyHistogramValues(histograms, feature_off_histogram_name,
+                        expected_histogram_values_for_feature_off);
+  VerifyHistogramValues(histograms, hint_off_histogram_name,
+                        expected_histogram_values_for_hint_off);
+  // Toggle feature on.
+  ToggleGameControls(game_window->GetNativeWindow(), /*is_feature=*/true);
+  MapIncreaseValueByOne(expected_histogram_values_for_feature_on,
+                        MappingSource::kUserAdded);
+  // Hint is also toggle on with feature toggle on.
+  MapIncreaseValueByOne(expected_histogram_values_for_hint_on,
+                        MappingSource::kUserAdded);
+  VerifyHistogramValues(histograms, feature_on_histogram_name,
+                        expected_histogram_values_for_feature_on);
+  VerifyHistogramValues(histograms, hint_on_histogram_name,
+                        expected_histogram_values_for_hint_on);
+}
+
 INSTANTIATE_TEST_SUITE_P(All,
                          VersionArcInputOverlayManagerTest,
                          ::testing::Bool());
diff --git a/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.cc b/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.cc
index ad5bf5af..e302003 100644
--- a/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.cc
+++ b/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.cc
@@ -6,6 +6,7 @@
 
 #include "ash/wm/window_state.h"
 #include "base/metrics/histogram_functions.h"
+#include "base/strings/string_util.h"
 #include "components/ukm/app_source_url_recorder.h"
 #include "services/metrics/public/cpp/delegating_ukm_recorder.h"
 #include "services/metrics/public/cpp/ukm_builders.h"
@@ -13,6 +14,12 @@
 
 namespace arc::input_overlay {
 
+namespace {
+
+constexpr char kGameControlsHistogramNameRoot[] = "Arc.GameControls";
+
+}  // namespace
+
 class InputOverlayUkm {
  public:
   static void RecordInputOverlayFeatureState(std::string package_name,
@@ -81,6 +88,11 @@
   }
 };
 
+std::string BuildGameControlsHistogramName(const std::string& name) {
+  return base::JoinString({kGameControlsHistogramNameRoot, name},
+                          kGameControlsHistogramSeparator);
+}
+
 void RecordInputOverlayFeatureState(const std::string& package_name,
                                     bool enable) {
   base::UmaHistogramBoolean("Arc.InputOverlay.FeatureState", enable);
@@ -134,4 +146,38 @@
       package_name, reposition_type, state_type);
 }
 
+void RecordEditingListFunctionTriggered(EditingListFunction function) {
+  base::UmaHistogramEnumeration(
+      BuildGameControlsHistogramName(kEditingListFunctionTriggeredHistogram),
+      function);
+}
+
+void RecordButtonOptionsMenuFunctionTriggered(
+    ButtonOptionsMenuFunction function) {
+  base::UmaHistogramEnumeration(
+      BuildGameControlsHistogramName(
+          kButtonOptionsMenuFunctionTriggeredHistogram),
+      function);
+}
+
+void RecordEditDeleteMenuFunctionTriggered(EditDeleteMenuFunction function) {
+  base::UmaHistogramEnumeration(
+      BuildGameControlsHistogramName(kEditDeleteMenuFunctionTriggeredHistogram),
+      function);
+}
+
+void RecordToggleWithMappingSource(bool is_feature,
+                                   bool is_on,
+                                   MappingSource source) {
+  base::UmaHistogramEnumeration(
+      BuildGameControlsHistogramName(
+          base::JoinString(
+              {(is_feature ? kFeatureHistogramName : kHintHistogramName),
+               kToggleWithMappingSourceHistogram},
+              ""))
+          .append(kGameControlsHistogramSeparator)
+          .append(is_on ? kToggleOnHistogramName : kToggleOffHistogramName),
+      source);
+}
+
 }  // namespace arc::input_overlay
diff --git a/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h b/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h
index e4ac06e..d8c9d5e3 100644
--- a/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h
+++ b/chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h
@@ -11,13 +11,80 @@
 
 namespace arc::input_overlay {
 
+inline constexpr char kEditingListFunctionTriggeredHistogram[] =
+    "EditingListFunctionTriggered";
+inline constexpr char kButtonOptionsMenuFunctionTriggeredHistogram[] =
+    "ButtonOptionsMenuFunctionTriggered";
+inline constexpr char kEditDeleteMenuFunctionTriggeredHistogram[] =
+    "EditDeleteMenuFuctionTriggered";
+
+inline constexpr char kToggleWithMappingSourceHistogram[] =
+    "ToggleWithMappingSource";
+
+inline constexpr char kToggleOnHistogramName[] = "On";
+inline constexpr char kToggleOffHistogramName[] = "Off";
+
+inline constexpr char kFeatureHistogramName[] = "Feature";
+inline constexpr char kHintHistogramName[] = "Hint";
+
+inline constexpr char kGameControlsHistogramSeparator[] = ".";
+
+// This enum should be kept in sync with the
+// `GameControlsButtonOptionsMenuFunction` in
+// tools/metrics/histograms/enums.xml.
+enum class ButtonOptionsMenuFunction {
+  kOptionSingleButton,
+  kOptionJoystick,
+  kEditLabelFocused,
+  kKeyAssigned,
+  kDone,
+  kDelete,
+  kMaxValue = kDelete,
+};
+
+// This enum should be kept in sync with the
+// `GameControlsEditDeleteMenuFunction` in
+// tools/metrics/histograms/enums.xml.
+enum class EditDeleteMenuFunction {
+  kEdit,
+  kDelete,
+  kMaxValue = kDelete,
+};
+
+// This enum should be kept in sync with the `GameControlsEditingListFunction`
+// in tools/metrics/histograms/enums.xml.
+enum class EditingListFunction {
+  kAdd,
+  kDone,
+  kHoverListItem,
+  kPressListItem,
+  kEditLabelFocused,
+  kKeyAssigned,
+  kMaxValue = kKeyAssigned,
+};
+
+// This enum should be kept in sync with the `GameControlsMappingSource`
+// in tools/metrics/histograms/enums.xml.
+enum class MappingSource {
+  kEmpty,
+  // Only pre-defined default mapping. May include position change.
+  kDefault,
+  // Only user-added mapping.
+  kUserAdded,
+  // Includes default and user-added mapping.
+  kDefaultAndUserAdded,
+  kMaxValue = kDefaultAndUserAdded,
+};
+
+std::string BuildGameControlsHistogramName(const std::string& name);
+
 // Records whether the feature is on or off.
 void RecordInputOverlayFeatureState(const std::string& package_name,
                                     bool enable);
 
 // Records whether the mapping hint is on or off.
 void RecordInputOverlayMappingHintState(const std::string& package_name,
-                                    bool enable);
+                                        bool enable);
 
 // Records whether the overlay is customized.
 void RecordInputOverlayCustomizedUsage(const std::string& package_name);
@@ -39,6 +106,19 @@
     RepositionType reposition_type,
     InputOverlayWindowStateType state_type);
 
+void RecordEditingListFunctionTriggered(EditingListFunction function);
+
+void RecordButtonOptionsMenuFunctionTriggered(
+    ButtonOptionsMenuFunction function);
+
+void RecordEditDeleteMenuFunctionTriggered(EditDeleteMenuFunction function);
+
+// Records feature toggle data if `is_feature` is true. Otherwise, records the
+// hint toggle data.
+void RecordToggleWithMappingSource(bool is_feature,
+                                   bool is_on,
+                                   MappingSource source);
+
 }  // namespace arc::input_overlay
 
 #endif  // CHROME_BROWSER_ASH_ARC_INPUT_OVERLAY_ARC_INPUT_OVERLAY_METRICS_H_
diff --git a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
index 40a8332d..35b7739 100644
--- a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
+++ b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
@@ -9,6 +9,7 @@
 
 #include "ash/frame/non_client_frame_view_ash.h"
 #include "ash/game_dashboard/game_dashboard_controller.h"
+#include "ash/game_dashboard/game_dashboard_utils.h"
 #include "ash/public/cpp/arc_game_controls_flag.h"
 #include "ash/public/cpp/window_properties.h"
 #include "ash/shell.h"
@@ -17,6 +18,7 @@
 #include "base/functional/bind.h"
 #include "base/memory/ptr_util.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/touch_injector.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/action_highlight.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/action_view.h"
@@ -54,6 +56,10 @@
 namespace arc::input_overlay {
 
 namespace {
+
+using ash::game_dashboard_utils::GetNextWidgetToFocus;
+using ash::game_dashboard_utils::UpdateAccessibilityTree;
+
 // UI specs.
 constexpr int kMenuEntrySideMargin = 24;
 constexpr int kNudgeVerticalAlign = 8;
@@ -132,7 +138,8 @@
     }
 
     // Change focus to the next widget.
-    if (auto* next_widget = GetNextWidgetToFocus(target_widget, reverse)) {
+    if (auto* next_widget =
+            GetNextWidgetToFocus(widget_list_, target_widget, reverse)) {
       next_widget->GetFocusManager()->AdvanceFocus(reverse);
       // Change the event target.
       ui::Event::DispatcherApi(&event).set_target(
@@ -145,7 +152,7 @@
     if (auto it = std::find(widget_list_.begin(), widget_list_.end(), widget);
         it == widget_list_.end()) {
       widget_list_.emplace_back(widget);
-      OnWidgetListUpdated();
+      UpdateAccessibilityTree(widget_list_);
     }
   }
 
@@ -153,45 +160,10 @@
     if (auto it = std::find(widget_list_.begin(), widget_list_.end(), widget);
         it != widget_list_.end()) {
       widget_list_.erase(it);
-      OnWidgetListUpdated();
+      UpdateAccessibilityTree(widget_list_);
     }
   }
 
-  void OnWidgetListUpdated() {
-    const size_t widget_list_size = widget_list_.size();
-    if (widget_list_size <= 1u) {
-      return;
-    }
-
-    // Update the widget's accessibility tree.
-    for (size_t i = 0; i < widget_list_size; i++) {
-      auto* curr_view = widget_list_[i]->GetContentsView();
-      auto& curr_view_a11y = curr_view->GetViewAccessibility();
-      const size_t prev_index = (i + widget_list_size - 1u) % widget_list_size;
-      const size_t next_index = (i + 1u) % widget_list_size;
-
-      curr_view_a11y.SetPreviousFocus(widget_list_[prev_index]);
-      curr_view_a11y.SetNextFocus(widget_list_[next_index]);
-      curr_view->NotifyAccessibilityEvent(ax::mojom::Event::kTreeChanged,
-                                          /*send_native_event=*/true);
-    }
-  }
-
-  views::Widget* GetNextWidgetToFocus(views::Widget* focused_widget,
-                                      bool reverse) {
-    if (auto it =
-            std::find(widget_list_.begin(), widget_list_.end(), focused_widget);
-        it != widget_list_.end()) {
-      const int index = std::distance(widget_list_.begin(), it);
-      const size_t widget_list_size = widget_list_.size();
-      const size_t next_index =
-          reverse ? (index - 1u + widget_list_size) % widget_list_size
-                  : (index + 1u) % widget_list_size;
-      return widget_list_[next_index];
-    }
-    return nullptr;
-  }
-
   // Only contains visible and unique widgets.
   std::vector<views::Widget*> widget_list_;
 };
@@ -803,6 +775,28 @@
   return it != actions.end() && !(it->get()->IsDeleted());
 }
 
+MappingSource DisplayOverlayController::GetMappingSource() const {
+  const auto& actions = touch_injector_->actions();
+  if (actions.empty()) {
+    return MappingSource::kEmpty;
+  }
+
+  // Check if there is any default action.
+  auto default_it = std::find_if(
+      actions.begin(), actions.end(),
+      [&](const std::unique_ptr<Action>& p) { return p->IsDefaultAction(); });
+
+  // Check if there is any user added action.
+  auto user_added_it = std::find_if(
+      actions.begin(), actions.end(),
+      [&](const std::unique_ptr<Action>& p) { return !p->IsDefaultAction(); });
+
+  return default_it != actions.end() && user_added_it != actions.end()
+             ? MappingSource::kDefaultAndUserAdded
+             : (default_it != actions.end() ? MappingSource::kDefault
+                                            : MappingSource::kUserAdded);
+}
+
 void DisplayOverlayController::AddTouchInjectorObserver(
     TouchInjectorObserver* observer) {
   touch_injector_->AddObserver(observer);
@@ -1087,6 +1081,21 @@
       }
 
       UpdateEventRewriteCapability();
+
+      // Record metrics.
+      const auto mapping_source = GetMappingSource();
+      if (IsFlagChanged(flags, old_flags, ash::ArcGameControlsFlag::kEnabled)) {
+        RecordToggleWithMappingSource(
+            /*is_feature=*/true,
+            /*is_on=*/IsFlagSet(flags, ash::ArcGameControlsFlag::kEnabled),
+            mapping_source);
+      }
+      if (IsFlagChanged(flags, old_flags, ash::ArcGameControlsFlag::kHint)) {
+        RecordToggleWithMappingSource(
+            /*is_feature=*/false,
+            /*is_on=*/IsFlagSet(flags, ash::ArcGameControlsFlag::kHint),
+            mapping_source);
+      }
     }
   }
 }
diff --git a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h
index 6b64ae0..cecbfafe 100644
--- a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h
+++ b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h
@@ -13,6 +13,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/scoped_multi_source_observation.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/input_element.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "ui/aura/window_observer.h"
 #include "ui/compositor/property_change_reason.h"
 #include "ui/events/event.h"
@@ -103,6 +104,8 @@
   // Return true if action is not deleted.
   bool IsActiveAction(Action* action) const;
 
+  MappingSource GetMappingSource() const;
+
   // For menu entry hover state:
   void SetMenuEntryHoverState(bool curr_hover_state);
 
diff --git a/chrome/browser/ash/arc/input_overlay/display_overlay_controller_unittest.cc b/chrome/browser/ash/arc/input_overlay/display_overlay_controller_unittest.cc
index 1a75d31..01049fd2 100644
--- a/chrome/browser/ash/arc/input_overlay/display_overlay_controller_unittest.cc
+++ b/chrome/browser/ash/arc/input_overlay/display_overlay_controller_unittest.cc
@@ -7,8 +7,11 @@
 #include <vector>
 
 #include "ash/public/cpp/arc_game_controls_flag.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/test/game_controls_test_base.h"
 #include "chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h"
+#include "chrome/browser/ash/arc/input_overlay/test/test_utils.h"
 #include "chrome/browser/ash/arc/input_overlay/touch_injector.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/button_options_menu.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut.h"
@@ -377,4 +380,39 @@
       /*button_options_visible=*/true, /*delete_edit_menu_visible=*/false);
 }
 
+TEST_F(EditModeDisplayOverlayControllerTest, TestHistograms) {
+  base::HistogramTester histograms;
+
+  // Check button options histograms.
+  const std::string button_options_histogram_name =
+      BuildGameControlsHistogramName(
+          kButtonOptionsMenuFunctionTriggeredHistogram);
+  std::map<ButtonOptionsMenuFunction, int>
+      expected_button_options_histogram_values;
+
+  ShowButtonOptionsMenu(tap_action_);
+  PressDoneButtonOnButtonOptionsMenu();
+  MapIncreaseValueByOne(expected_button_options_histogram_values,
+                        ButtonOptionsMenuFunction::kDone);
+  VerifyHistogramValues(histograms, button_options_histogram_name,
+                        expected_button_options_histogram_values);
+
+  ShowButtonOptionsMenu(move_action_);
+  PressDeleteButtonOnButtonOptionsMenu();
+  MapIncreaseValueByOne(expected_button_options_histogram_values,
+                        ButtonOptionsMenuFunction::kDelete);
+  VerifyHistogramValues(histograms, button_options_histogram_name,
+                        expected_button_options_histogram_values);
+
+  // Check editing list histograms.
+  const std::string editing_list_histogram_name =
+      BuildGameControlsHistogramName(kEditingListFunctionTriggeredHistogram);
+  std::map<EditingListFunction, int> expected_editing_list_histogram_values;
+  PressDoneButton();
+  MapIncreaseValueByOne(expected_editing_list_histogram_values,
+                        EditingListFunction::kDone);
+  VerifyHistogramValues(histograms, editing_list_histogram_name,
+                        expected_editing_list_histogram_values);
+}
+
 }  // namespace arc::input_overlay
diff --git a/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.cc b/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.cc
index b40ce4d..107cb1c 100644
--- a/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.cc
+++ b/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.cc
@@ -51,6 +51,13 @@
   LeftClickOn(editing_list_->GetAddContainerButtonForTesting());
 }
 
+void OverlayViewTestBase::PressDoneButton() {
+  if (!editing_list_) {
+    return;
+  }
+  LeftClickOn(editing_list_->done_button_);
+}
+
 void OverlayViewTestBase::AddNewActionInCenter() {
   DCHECK(editing_list_);
 
diff --git a/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h b/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h
index b30df24..d7dbe3b 100644
--- a/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h
+++ b/chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h
@@ -33,6 +33,7 @@
   void EnableEditMode();
   void PressAddButton();
   void PressAddContainerButton();
+  void PressDoneButton();
 
   // Adds a new action in the center of the main window.
   void AddNewActionInCenter();
diff --git a/chrome/browser/ash/arc/input_overlay/test/test_utils.h b/chrome/browser/ash/arc/input_overlay/test/test_utils.h
index 353843d..cde6321 100644
--- a/chrome/browser/ash/arc/input_overlay/test/test_utils.h
+++ b/chrome/browser/ash/arc/input_overlay/test/test_utils.h
@@ -5,10 +5,12 @@
 #ifndef CHROME_BROWSER_ASH_ARC_INPUT_OVERLAY_TEST_TEST_UTILS_H_
 #define CHROME_BROWSER_ASH_ARC_INPUT_OVERLAY_TEST_TEST_UTILS_H_
 
+#include <map>
 #include <memory>
 #include <string>
 #include <vector>
 
+#include "base/test/metrics/histogram_tester.h"
 #include "base/time/time.h"
 #include "chrome/browser/ash/arc/input_overlay/db/proto/app_data.pb.h"
 #include "ui/gfx/geometry/rect.h"
@@ -69,6 +71,27 @@
 std::u16string GetControlName(ActionType action_type,
                               std::u16string key_string);
 
+// Increases the value for `key` by one. If there is no `key`, set the value
+// to 1.
+template <typename T>
+void MapIncreaseValueByOne(std::map<T, int>& map, T key) {
+  auto it = map.find(key);
+  if (it == map.end()) {
+    map[key] = 1;
+  } else {
+    map[key]++;
+  }
+}
+
+template <typename T>
+void VerifyHistogramValues(const base::HistogramTester& histograms,
+                           const std::string& histogram_name,
+                           const std::map<T, int>& histogram_values) {
+  for (const auto& value : histogram_values) {
+    histograms.ExpectBucketCount(histogram_name, value.first, value.second);
+  }
+}
+
 }  // namespace arc::input_overlay
 
 #endif  // CHROME_BROWSER_ASH_ARC_INPUT_OVERLAY_TEST_TEST_UTILS_H_
diff --git a/chrome/browser/ash/arc/input_overlay/ui/action_type_button_group.cc b/chrome/browser/ash/arc/input_overlay/ui/action_type_button_group.cc
index 57130901..f792de40 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/action_type_button_group.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/action_type_button_group.cc
@@ -9,6 +9,7 @@
 #include "base/notreached.h"
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chromeos/strings/grit/chromeos_strings.h"
 #include "ui/accessibility/ax_enums.mojom.h"
@@ -150,6 +151,8 @@
   }
   selected_action_type_ = ActionType::TAP;
   controller_->ChangeActionType(action_, ActionType::TAP);
+  RecordButtonOptionsMenuFunctionTriggered(
+      ButtonOptionsMenuFunction::kOptionSingleButton);
 }
 
 void ActionTypeButtonGroup::OnActionMoveButtonPressed() {
@@ -158,6 +161,8 @@
   }
   selected_action_type_ = ActionType::MOVE;
   controller_->ChangeActionType(action_, ActionType::MOVE);
+  RecordButtonOptionsMenuFunctionTriggered(
+      ButtonOptionsMenuFunction::kOptionJoystick);
 }
 
 BEGIN_METADATA(ActionTypeButtonGroup)
diff --git a/chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.cc b/chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.cc
index fde2730..5b55a26 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.h"
 
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/edit_labels.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/name_tag.h"
@@ -22,12 +23,14 @@
 ActionViewListItem::~ActionViewListItem() = default;
 
 void ActionViewListItem::ClickCallback() {
+  RecordEditingListFunctionTriggered(EditingListFunction::kPressListItem);
   controller_->AddButtonOptionsMenuWidget(action_);
 }
 
 void ActionViewListItem::OnMouseEntered(const ui::MouseEvent& event) {
   controller_->AddActionHighlightWidget(action_);
   controller_->AddDeleteEditShortcutWidget(this);
+  RecordEditingListFunctionTriggered(EditingListFunction::kHoverListItem);
 }
 
 void ActionViewListItem::OnMouseExited(const ui::MouseEvent& event) {
diff --git a/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc b/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc
index ef09c18..d75fa51 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc
@@ -14,6 +14,7 @@
 #include "ash/style/typography.h"
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/touch_injector.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/action_type_button_group.h"
@@ -281,11 +282,13 @@
 }
 
 void ButtonOptionsMenu::OnTrashButtonPressed() {
+  RecordButtonOptionsMenuFunctionTriggered(ButtonOptionsMenuFunction::kDelete);
   controller_->RemoveAction(action_);
 }
 
 void ButtonOptionsMenu::OnDoneButtonPressed() {
   controller_->SaveToProtoFile();
+  RecordButtonOptionsMenuFunctionTriggered(ButtonOptionsMenuFunction::kDone);
 
   controller_->SetEditingListVisibility(/*visible=*/true);
 
diff --git a/chrome/browser/ash/arc/input_overlay/ui/button_options_menu_unittest.cc b/chrome/browser/ash/arc/input_overlay/ui/button_options_menu_unittest.cc
index af12a32..6a3f8d0 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/button_options_menu_unittest.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/button_options_menu_unittest.cc
@@ -11,7 +11,9 @@
 #include "ash/root_window_controller.h"
 #include "ash/shelf/shelf.h"
 #include "ash/style/icon_button.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/db/proto/app_data.pb.h"
 #include "chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h"
 #include "chrome/browser/ash/arc/input_overlay/test/test_utils.h"
@@ -282,4 +284,22 @@
             menu->GetWidget()->GetNativeWindow()->bounds().bottom());
 }
 
+TEST_F(ButtonOptionsMenuTest, TestHistograms) {
+  base::HistogramTester histograms;
+  const std::string histogram_name = BuildGameControlsHistogramName(
+      kButtonOptionsMenuFunctionTriggeredHistogram);
+  std::map<ButtonOptionsMenuFunction, int> expected_histogram_values;
+
+  auto* menu = ShowButtonOptionsMenu(tap_action_);
+  PressActionMoveButton(menu);
+  MapIncreaseValueByOne(expected_histogram_values,
+                        ButtonOptionsMenuFunction::kOptionJoystick);
+  VerifyHistogramValues(histograms, histogram_name, expected_histogram_values);
+
+  PressTapButton(menu);
+  MapIncreaseValueByOne(expected_histogram_values,
+                        ButtonOptionsMenuFunction::kOptionSingleButton);
+  VerifyHistogramValues(histograms, histogram_name, expected_histogram_values);
+}
+
 }  // namespace arc::input_overlay
diff --git a/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut.cc b/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut.cc
index 24c826825..fb0f5bb 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut.cc
@@ -10,6 +10,7 @@
 #include "ash/style/icon_button.h"
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/constants.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.h"
@@ -119,6 +120,7 @@
 }
 
 void DeleteEditShortcut::OnEditButtonPressed() {
+  RecordEditDeleteMenuFunctionTriggered(EditDeleteMenuFunction::kEdit);
   if (auto* anchor_view =
           views::AsViewClass<ActionViewListItem>(GetAnchorView())) {
     controller_->AddButtonOptionsMenuWidget(anchor_view->action());
@@ -126,6 +128,7 @@
 }
 
 void DeleteEditShortcut::OnDeleteButtonPressed() {
+  RecordEditDeleteMenuFunctionTriggered(EditDeleteMenuFunction::kDelete);
   if (auto* anchor_view =
           views::AsViewClass<ActionViewListItem>(GetAnchorView())) {
     controller_->RemoveAction(anchor_view->action());
diff --git a/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut_unittest.cc b/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut_unittest.cc
index 92899f5f..1752b506 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut_unittest.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut_unittest.cc
@@ -4,9 +4,12 @@
 
 #include "chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut.h"
 
+#include "base/test/metrics/histogram_tester.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h"
+#include "chrome/browser/ash/arc/input_overlay/test/test_utils.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.h"
 #include "ui/views/view_utils.h"
 
@@ -109,4 +112,24 @@
   EXPECT_EQ(original_size - 1, GetActionViewSize());
 }
 
+TEST_F(DeleteEditShortcutTest, TestHistograms) {
+  base::HistogramTester histograms;
+  const std::string histogram_name =
+      BuildGameControlsHistogramName(kEditDeleteMenuFunctionTriggeredHistogram);
+  std::map<EditDeleteMenuFunction, int> expected_histogram_values;
+
+  HoverAtActionViewListItem(/*index=*/0u);
+  PressEditButton();
+  MapIncreaseValueByOne(expected_histogram_values,
+                        EditDeleteMenuFunction::kEdit);
+  VerifyHistogramValues(histograms, histogram_name, expected_histogram_values);
+
+  PressDoneButtonOnButtonOptionsMenu();
+  HoverAtActionViewListItem(/*index=*/1u);
+  PressDeleteButton();
+  MapIncreaseValueByOne(expected_histogram_values,
+                        EditDeleteMenuFunction::kDelete);
+  VerifyHistogramValues(histograms, histogram_name, expected_histogram_values);
+}
+
 }  // namespace arc::input_overlay
diff --git a/chrome/browser/ash/arc/input_overlay/ui/edit_label.cc b/chrome/browser/ash/arc/input_overlay/ui/edit_label.cc
index 12e7d97d..c712b08 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/edit_label.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/edit_label.cc
@@ -14,6 +14,7 @@
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/input_element.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/constants.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.h"
@@ -276,6 +277,10 @@
   SetToFocused();
   if (for_editing_list_) {
     controller_->AddActionHighlightWidget(action_);
+    RecordEditingListFunctionTriggered(EditingListFunction::kEditLabelFocused);
+  } else {
+    RecordButtonOptionsMenuFunctionTriggered(
+        ButtonOptionsMenuFunction::kEditLabelFocused);
   }
 }
 
@@ -333,6 +338,12 @@
   }
 
   SetTextLabel(new_bind);
+  if (for_editing_list_) {
+    RecordEditingListFunctionTriggered(EditingListFunction::kKeyAssigned);
+  } else {
+    RecordButtonOptionsMenuFunctionTriggered(
+        ButtonOptionsMenuFunction::kKeyAssigned);
+  }
 
   std::unique_ptr<InputElement> input;
   switch (action_->GetType()) {
diff --git a/chrome/browser/ash/arc/input_overlay/ui/edit_label_unittest.cc b/chrome/browser/ash/arc/input_overlay/ui/edit_label_unittest.cc
index d44b5aa4..b69d8281 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/edit_label_unittest.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/edit_label_unittest.cc
@@ -9,6 +9,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/constants.h"
 #include "chrome/browser/ash/arc/input_overlay/db/proto/app_data.pb.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
@@ -308,4 +309,48 @@
               {u"", u"a", u"", u""}, GetControlName(ActionType::MOVE, u"a"));
 }
 
+TEST_F(EditLabelTest, TestHistograms) {
+  widget_->GetNativeWindow()->SetBounds(gfx::Rect(310, 10, 300, 500));
+  base::HistogramTester histograms;
+
+  // Check histograms for editing list.
+  const std::string editing_list_histogram_name =
+      BuildGameControlsHistogramName(kEditingListFunctionTriggeredHistogram);
+  std::map<EditingListFunction, int> expected_editing_list_histogram_values;
+  LeftClickOn(GetEditLabel(tap_action_list_item_, /*index=*/0));
+  MapIncreaseValueByOne(expected_editing_list_histogram_values,
+                        EditingListFunction::kEditLabelFocused);
+  VerifyHistogramValues(histograms, editing_list_histogram_name,
+                        expected_editing_list_histogram_values);
+
+  auto* event_generator = GetEventGenerator();
+  event_generator->PressAndReleaseKey(ui::VKEY_M, ui::EF_NONE);
+  MapIncreaseValueByOne(expected_editing_list_histogram_values,
+                        EditingListFunction::kKeyAssigned);
+  VerifyHistogramValues(histograms, editing_list_histogram_name,
+                        expected_editing_list_histogram_values);
+
+  // Check histograms for button options menu.
+  const std::string button_options_histogram_name =
+      BuildGameControlsHistogramName(
+          kButtonOptionsMenuFunctionTriggeredHistogram);
+  std::map<ButtonOptionsMenuFunction, int>
+      expected_button_options_histogram_values;
+  auto* menu = ShowButtonOptionsMenu(move_action_);
+  LeftClickOn(GetEditLabel(menu, /*index=*/1));
+  MapIncreaseValueByOne(expected_button_options_histogram_values,
+                        ButtonOptionsMenuFunction::kEditLabelFocused);
+  VerifyHistogramValues(histograms, button_options_histogram_name,
+                        expected_button_options_histogram_values);
+
+  event_generator->PressAndReleaseKey(ui::VKEY_N, ui::EF_NONE);
+  // After assign a key, the focus is automatically moved to the next one.
+  MapIncreaseValueByOne(expected_button_options_histogram_values,
+                        ButtonOptionsMenuFunction::kEditLabelFocused);
+  MapIncreaseValueByOne(expected_button_options_histogram_values,
+                        ButtonOptionsMenuFunction::kKeyAssigned);
+  VerifyHistogramValues(histograms, button_options_histogram_name,
+                        expected_button_options_histogram_values);
+}
+
 }  // namespace arc::input_overlay
diff --git a/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc b/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc
index 44fdbe92..b4c1b21 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc
@@ -21,6 +21,7 @@
 #include "base/notreached.h"
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/constants.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/touch_injector.h"
@@ -318,14 +319,14 @@
   help_button->SetProperty(views::kMarginsKey, gfx::Insets::TLBR(0, 0, 0, 8));
 
   // Add done button.
-  auto* done_button =
+  done_button_ =
       header_container->AddChildView(std::make_unique<ash::PillButton>(
           base::BindRepeating(&EditingList::OnDoneButtonPressed,
                               base::Unretained(this)),
           l10n_util::GetStringUTF16(
               IDS_INPUT_OVERLAY_EDITING_DONE_BUTTON_LABEL),
           ash::PillButton::Type::kSecondaryWithoutIcon));
-  done_button->SetAccessibleName(l10n_util::GetStringUTF16(
+  done_button_->SetAccessibleName(l10n_util::GetStringUTF16(
       IDS_INPUT_OVERLAY_EDITING_LIST_DONE_BUTTON_A11Y_LABEL));
 }
 
@@ -421,10 +422,12 @@
     ash::Shell::Get()->anchored_nudge_manager()->Cancel(kKeyEditNudgeID);
   }
   controller_->EnterButtonPlaceMode(ActionType::TAP);
+  RecordEditingListFunctionTriggered(EditingListFunction::kAdd);
 }
 
 void EditingList::OnDoneButtonPressed() {
   DCHECK(controller_);
+  RecordEditingListFunctionTriggered(EditingListFunction::kDone);
   controller_->OnCustomizeSave();
 }
 
diff --git a/chrome/browser/ash/arc/input_overlay/ui/editing_list.h b/chrome/browser/ash/arc/input_overlay/ui/editing_list.h
index 544807e..bf93cc2 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/editing_list.h
+++ b/chrome/browser/ash/arc/input_overlay/ui/editing_list.h
@@ -7,6 +7,7 @@
 
 #include <memory>
 
+#include "ash/style/pill_button.h"
 #include "base/callback_list.h"
 #include "base/memory/raw_ptr.h"
 #include "chrome/browser/ash/arc/input_overlay/touch_injector_observer.h"
@@ -15,6 +16,7 @@
 
 namespace ash {
 class AnchoredNudge;
+class PillButton;
 class SystemShadow;
 }  // namespace ash
 
@@ -140,6 +142,7 @@
   raw_ptr<views::Label> editing_header_label_;
 
   raw_ptr<AddContainerButton> add_container_;
+  raw_ptr<ash::PillButton> done_button_;
 
   // Owned by this view.
   std::unique_ptr<ash::SystemShadow> shadow_;
diff --git a/chrome/browser/ash/arc/input_overlay/ui/editing_list_unittest.cc b/chrome/browser/ash/arc/input_overlay/ui/editing_list_unittest.cc
index cdcc2701..ebbdcc3 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/editing_list_unittest.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/editing_list_unittest.cc
@@ -10,7 +10,9 @@
 #include "ash/system/toast/anchored_nudge.h"
 #include "ash/system/toast/anchored_nudge_manager_impl.h"
 #include "base/check.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "chrome/browser/ash/arc/input_overlay/actions/action.h"
+#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
 #include "chrome/browser/ash/arc/input_overlay/constants.h"
 #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
 #include "chrome/browser/ash/arc/input_overlay/test/overlay_view_test_base.h"
@@ -527,4 +529,26 @@
   EXPECT_FALSE(GetTargetView());
 }
 
+TEST_F(EditingListTest, TestHistograms) {
+  base::HistogramTester histograms;
+  const std::string histogram_name =
+      BuildGameControlsHistogramName(kEditingListFunctionTriggeredHistogram);
+  std::map<EditingListFunction, int> expected_histogram_values;
+
+  PressAddButton();
+  MapIncreaseValueByOne(expected_histogram_values, EditingListFunction::kAdd);
+  VerifyHistogramValues(histograms, histogram_name, expected_histogram_values);
+
+  GetEventGenerator()->PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
+  HoverAtActionViewListItem(/*index=*/0u);
+  MapIncreaseValueByOne(expected_histogram_values,
+                        EditingListFunction::kHoverListItem);
+  VerifyHistogramValues(histograms, histogram_name, expected_histogram_values);
+
+  LeftClickAtActionViewListItem(/*index=*/0);
+  MapIncreaseValueByOne(expected_histogram_values,
+                        EditingListFunction::kPressListItem);
+  VerifyHistogramValues(histograms, histogram_name, expected_histogram_values);
+}
+
 }  // namespace arc::input_overlay
diff --git a/chrome/browser/ash/phonehub/phone_hub_manager_factory.cc b/chrome/browser/ash/phonehub/phone_hub_manager_factory.cc
index 98dc0359..6654dcc 100644
--- a/chrome/browser/ash/phonehub/phone_hub_manager_factory.cc
+++ b/chrome/browser/ash/phonehub/phone_hub_manager_factory.cc
@@ -32,6 +32,7 @@
 #include "chromeos/ash/components/phonehub/onboarding_ui_tracker_impl.h"
 #include "chromeos/ash/components/phonehub/phone_hub_manager.h"
 #include "chromeos/ash/components/phonehub/phone_hub_manager_impl.h"
+#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/components/phonehub/recent_apps_interaction_handler_impl.h"
 #include "chromeos/ash/components/phonehub/screen_lock_manager_impl.h"
 #include "chromeos/ash/components/phonehub/user_action_recorder_impl.h"
@@ -223,6 +224,7 @@
   OnboardingUiTrackerImpl::RegisterPrefs(registry);
   ScreenLockManagerImpl::RegisterPrefs(registry);
   RecentAppsInteractionHandlerImpl::RegisterPrefs(registry);
+  PhoneHubStructuredMetricsLogger::RegisterPrefs(registry);
 }
 
 }  // namespace ash::phonehub
diff --git a/chrome/browser/chromeos/extensions/telemetry/api/common/base_telemetry_extension_browser_test.cc b/chrome/browser/chromeos/extensions/telemetry/api/common/base_telemetry_extension_browser_test.cc
index 367b43e..023c5a7 100644
--- a/chrome/browser/chromeos/extensions/telemetry/api/common/base_telemetry_extension_browser_test.cc
+++ b/chrome/browser/chromeos/extensions/telemetry/api/common/base_telemetry_extension_browser_test.cc
@@ -90,6 +90,7 @@
           "os.attached_device_info",
           "os.bluetooth_peripherals_info",
           "os.diagnostics",
+          "os.diagnostics.network_info_mlab",
           "os.events",
           "os.management.audio",
           "os.telemetry",
diff --git a/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api.cc b/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api.cc
index eb8ae9f..d16e8c7 100644
--- a/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api.cc
+++ b/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api.cc
@@ -65,7 +65,7 @@
 
 bool IsPendingApprovalRoutine(
     const crosapi::mojom::TelemetryDiagnosticRoutineArgumentPtr& arg) {
-  return arg->is_network_bandwidth();
+  return false;
 }
 
 }  // namespace
@@ -526,6 +526,18 @@
         NewUnrecognizedArgument(false);
   }
 
+  // Network bandwidth routine is guarded by `os.diagnostics.network_info_mlab`
+  // permission.
+  if (mojo_arg.value()->is_network_bandwidth() &&
+      !extension()->permissions_data()->HasAPIPermission(
+          extensions::mojom::APIPermissionID::
+              kChromeOSDiagnosticsNetworkInfoForMlab)) {
+    RespondWithError(
+        "Unauthorized access to chrome.os.diagnostics.CreateRoutine with "
+        "networkBandwidth argument. Extension doesn't have the permission.");
+    return;
+  }
+
   auto* routines_manager = DiagnosticRoutineManager::Get(browser_context());
   auto result = routines_manager->CreateRoutine(extension_id(),
                                                 std::move(mojo_arg.value()));
@@ -763,6 +775,19 @@
         NewUnrecognizedArgument(false);
   }
 
+  // Network bandwidth routine is guarded by `os.diagnostics.network_info_mlab`
+  // permission.
+  if (mojo_arg.value()->is_network_bandwidth() &&
+      !extension()->permissions_data()->HasAPIPermission(
+          extensions::mojom::APIPermissionID::
+              kChromeOSDiagnosticsNetworkInfoForMlab)) {
+    RespondWithError(
+        "Unauthorized access to "
+        "chrome.os.diagnostics.isRoutineArgumentSupported with "
+        "networkBandwidth argument. Extension doesn't have the permission.");
+    return;
+  }
+
   auto* routines_manager = DiagnosticRoutineManager::Get(browser_context());
   routines_manager->IsRoutineArgumentSupported(
       std::move(mojo_arg.value()),
diff --git a/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api_v2_browsertest.cc b/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api_v2_browsertest.cc
index 1d70b018..b9c4b72 100644
--- a/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api_v2_browsertest.cc
+++ b/chrome/browser/chromeos/extensions/telemetry/api/diagnostics/diagnostics_api_v2_browsertest.cc
@@ -1130,32 +1130,6 @@
   )");
 }
 
-IN_PROC_BROWSER_TEST_F(
-    TelemetryExtensionDiagnosticsApiV2BrowserTest,
-    CreateNetworkBandwidthRoutineWithoutFeatureFlagUnrecognized) {
-  fake_service().SetOnCreateRoutineCalled(base::BindLambdaForTesting([this]() {
-    auto* control = fake_service().GetCreatedRoutineControlForRoutineType(
-        crosapi::TelemetryDiagnosticRoutineArgument::Tag::
-            kUnrecognizedArgument);
-    ASSERT_TRUE(control);
-  }));
-
-  OpenAppUiAndMakeItSecure();
-
-  CreateExtensionAndRunServiceWorker(R"(
-    chrome.test.runTests([
-      async function createNetworkBandwidthRoutineUnrecognized() {
-        const result = await chrome.os.diagnostics.createRoutine({
-          networkBandwidth: {},
-        });
-
-        chrome.test.assertTrue(result !== undefined);
-        chrome.test.succeed();
-      }
-    ]);
-  )");
-}
-
 IN_PROC_BROWSER_TEST_F(TelemetryExtensionDiagnosticsApiV2BrowserTest,
                        ReplyToRoutineInquiryUnknownUuidError) {
   OpenAppUiAndMakeItSecure();
@@ -1289,21 +1263,8 @@
   EXPECT_TRUE(led_routine_created.Wait());
 }
 
-class PendingApprovalTelemetryExtensionDiagnosticsApiV2BrowserTest
-    : public TelemetryExtensionDiagnosticsApiV2BrowserTest {
- public:
-  PendingApprovalTelemetryExtensionDiagnosticsApiV2BrowserTest() {
-    feature_list_.InitAndEnableFeature(
-        extensions_features::kTelemetryExtensionPendingApprovalApi);
-  }
-
- private:
-  base::test::ScopedFeatureList feature_list_;
-};
-
-IN_PROC_BROWSER_TEST_F(
-    PendingApprovalTelemetryExtensionDiagnosticsApiV2BrowserTest,
-    CreateNetworkBandwidthRoutineWithFeatureFlagSuccess) {
+IN_PROC_BROWSER_TEST_F(TelemetryExtensionDiagnosticsApiV2BrowserTest,
+                       CreateNetworkBandwidthRoutineSuccess) {
   fake_service().SetOnCreateRoutineCalled(base::BindLambdaForTesting([this]() {
     auto* control = fake_service().GetCreatedRoutineControlForRoutineType(
         crosapi::TelemetryDiagnosticRoutineArgument::Tag::kNetworkBandwidth);
@@ -1374,4 +1335,55 @@
   )");
 }
 
+class NoExtraPermissionTelemetryExtensionDiagnosticsApiV2BrowserTest
+    : public TelemetryExtensionDiagnosticsApiV2BrowserTest {
+ public:
+  NoExtraPermissionTelemetryExtensionDiagnosticsApiV2BrowserTest() = default;
+
+ protected:
+  std::string GetManifestFile(const std::string& manifest_key,
+                              const std::string& matches_origin) override {
+    return base::StringPrintf(R"(
+      {
+        "key": "%s",
+        "name": "Test Telemetry Extension",
+        "version": "1",
+        "manifest_version": 3,
+        "chromeos_system_extension": {},
+        "background": {
+          "service_worker": "sw.js"
+        },
+        "permissions": [ "os.diagnostics" ],
+        "externally_connectable": {
+          "matches": [
+            "%s"
+          ]
+        },
+        "options_page": "options.html"
+      }
+    )",
+                              manifest_key.c_str(), matches_origin.c_str());
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(
+    NoExtraPermissionTelemetryExtensionDiagnosticsApiV2BrowserTest,
+    NetworkBandwidthRoutineNoPermissionFail) {
+  CreateExtensionAndRunServiceWorker(R"(
+    chrome.test.runTests([
+      async function createNetworkBandwidthRoutineNoPermission() {
+        await chrome.test.assertPromiseRejects(
+          chrome.os.diagnostics.createRoutine({
+            networkBandwidth: {},
+          }),
+          'Error: Unauthorized access to ' +
+          'chrome.os.diagnostics.CreateRoutine with networkBandwidth ' +
+          'argument. Extension doesn\'t have the permission.'
+        );
+        chrome.test.succeed();
+      }
+    ]);
+  )");
+}
+
 }  // namespace chromeos
diff --git a/chrome/browser/chromeos/extensions/telemetry/chromeos_permission_messages_unittest.cc b/chrome/browser/chromeos/extensions/telemetry/chromeos_permission_messages_unittest.cc
index 7fe4fd0..166f20ad1 100644
--- a/chrome/browser/chromeos/extensions/telemetry/chromeos_permission_messages_unittest.cc
+++ b/chrome/browser/chromeos/extensions/telemetry/chromeos_permission_messages_unittest.cc
@@ -50,6 +50,9 @@
 const std::u16string kBluetoothPeripheralsInfo =
     u"Read Bluetooth peripherals information and data";
 const std::u16string kManagementAudio = u"Manage ChromeOS audio settings";
+const std::u16string kDiagnosticsNetworkInfoForMlab =
+    u"Collect IP address and network measurement results for Measurement Lab, "
+    u"according to their privacy policy (measurementlab.net/privacy)";
 }  // namespace
 
 // Tests that ChromePermissionMessageProvider provides not only correct, but
@@ -188,6 +191,26 @@
   EXPECT_EQ(kDiagnosticsPermissionMessage, active_permissions()[0]);
 }
 
+TEST_F(ChromeOSPermissionMessageUnittest, OsDiagnosticsNetworkInfoForMlab) {
+  CreateAndInstallExtensionWithPermissions(
+      base::Value::List(),
+      base::Value::List().Append("os.diagnostics.network_info_mlab"));
+
+  ASSERT_EQ(1U, optional_permissions().size());
+  EXPECT_EQ(kDiagnosticsNetworkInfoForMlab, optional_permissions()[0]);
+  ASSERT_EQ(1U, GetInactiveOptionalPermissionMessages().size());
+  EXPECT_EQ(kDiagnosticsNetworkInfoForMlab,
+            GetInactiveOptionalPermissionMessages()[0]);
+  EXPECT_EQ(0U, required_permissions().size());
+  EXPECT_EQ(0U, active_permissions().size());
+
+  GrantOptionalPermissions();
+
+  EXPECT_EQ(0U, GetInactiveOptionalPermissionMessages().size());
+  ASSERT_EQ(1U, active_permissions().size());
+  EXPECT_EQ(kDiagnosticsNetworkInfoForMlab, active_permissions()[0]);
+}
+
 TEST_F(ChromeOSPermissionMessageUnittest, OsTelemetryMessage) {
   CreateAndInstallExtensionWithPermissions(
       base::Value::List().Append("os.telemetry"), base::Value::List());
diff --git a/chrome/browser/download/download_core_service_impl.cc b/chrome/browser/download/download_core_service_impl.cc
index 01dd185..5005b4f2 100644
--- a/chrome/browser/download/download_core_service_impl.cc
+++ b/chrome/browser/download/download_core_service_impl.cc
@@ -130,9 +130,11 @@
   DownloadManager* download_manager = profile_->GetDownloadManager();
   DownloadManager::DownloadVector downloads;
   download_manager->GetAllDownloads(&downloads);
-  for (auto it = downloads.begin(); it != downloads.end(); ++it) {
-    if ((*it)->GetState() == download::DownloadItem::IN_PROGRESS)
-      (*it)->Cancel(false);
+  for (auto& download : downloads) {
+    if (download->GetState() == download::DownloadItem::IN_PROGRESS) {
+      download->Cancel(/*user_cancel=*/false);
+      manager_delegate_->OnDownloadCanceledAtShutdown(download);
+    }
   }
 }
 
diff --git a/chrome/browser/download/download_core_service_impl_unittest.cc b/chrome/browser/download/download_core_service_impl_unittest.cc
new file mode 100644
index 0000000..5409877
--- /dev/null
+++ b/chrome/browser/download/download_core_service_impl_unittest.cc
@@ -0,0 +1,104 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/download/download_core_service_impl.h"
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/download/chrome_download_manager_delegate.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/download/public/common/download_item.h"
+#include "components/download/public/common/mock_download_item.h"
+#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/mock_download_manager.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+using DownloadState = download::DownloadItem::DownloadState;
+using ::testing::_;
+using ::testing::NiceMock;
+using ::testing::Return;
+using ::testing::SetArgPointee;
+
+class TestDownloadManagerDelegate : public ChromeDownloadManagerDelegate {
+ public:
+  explicit TestDownloadManagerDelegate(Profile* profile)
+      : ChromeDownloadManagerDelegate(profile) {}
+  ~TestDownloadManagerDelegate() override = default;
+
+  void OnDownloadCanceledAtShutdown(download::DownloadItem* item) override {
+    canceled_at_shutdown_called_count_++;
+  }
+
+  int OnDownloadCanceledAtShutdownCalledCount() {
+    return canceled_at_shutdown_called_count_;
+  }
+
+ private:
+  int canceled_at_shutdown_called_count_ = 0;
+};
+
+class DownloadCoreServiceImplTest : public testing::Test {
+ public:
+  void SetUp() override {
+    profile_ = std::make_unique<TestingProfile>();
+    auto download_manager =
+        std::make_unique<NiceMock<content::MockDownloadManager>>();
+    download_manager_ = download_manager.get();
+    profile_->SetDownloadManagerForTesting(std::move(download_manager));
+    download_core_service_ =
+        std::make_unique<DownloadCoreServiceImpl>(profile_.get());
+    auto delegate =
+        std::make_unique<TestDownloadManagerDelegate>(profile_.get());
+    delegate_ = delegate.get();
+    download_core_service_->SetDownloadManagerDelegateForTesting(
+        std::move(delegate));
+  }
+
+  void TearDown() override {
+    download_core_service_->GetDownloadManagerDelegate()->Shutdown();
+    delegate_ = nullptr;
+    download_manager_ = nullptr;
+    download_core_service_ = nullptr;
+    profile_ = nullptr;
+  }
+
+ protected:
+  std::unique_ptr<download::MockDownloadItem> CreateDownloadItem(
+      DownloadState state) {
+    auto item = std::make_unique<NiceMock<download::MockDownloadItem>>();
+    EXPECT_CALL(*item, GetState()).WillRepeatedly(Return(state));
+    return item;
+  }
+
+  content::BrowserTaskEnvironment task_environment_;
+  raw_ptr<NiceMock<content::MockDownloadManager>> download_manager_;
+  std::unique_ptr<TestingProfile> profile_;
+  raw_ptr<TestDownloadManagerDelegate> delegate_;
+  std::unique_ptr<DownloadCoreServiceImpl> download_core_service_;
+};
+
+TEST_F(DownloadCoreServiceImplTest, CancelDownloads) {
+  auto completed_item = CreateDownloadItem(DownloadState::COMPLETE);
+  auto in_progress_item1 = CreateDownloadItem(DownloadState::IN_PROGRESS);
+  auto in_progress_item2 = CreateDownloadItem(DownloadState::IN_PROGRESS);
+  std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> items;
+  items.push_back(completed_item.get());
+  items.push_back(in_progress_item1.get());
+  items.push_back(in_progress_item2.get());
+  EXPECT_CALL(*download_manager_, GetAllDownloads)
+      .WillRepeatedly(SetArgPointee<0>(items));
+
+  // Only in progress items should be canceled.
+  EXPECT_CALL(*completed_item, Cancel(_)).Times(0);
+  EXPECT_CALL(*in_progress_item1, Cancel(/*user_cancel=*/false)).Times(1);
+  EXPECT_CALL(*in_progress_item2, Cancel(/*user_cancel=*/false)).Times(1);
+
+  download_core_service_->CancelDownloads();
+
+  EXPECT_EQ(delegate_->OnDownloadCanceledAtShutdownCalledCount(), 2);
+}
+
+}  // namespace
diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc
index 0871c48..4b31aed1 100644
--- a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc
+++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc
@@ -1168,7 +1168,7 @@
   EXPECT_CALL(*mock_adapter_, GetDevice(kTestLeDeviceAddress1))
       .WillRepeatedly(Return(device1_.get()));
   static_assert(
-      BluetoothDevice::NUM_CONNECT_ERROR_CODES == 14,
+      BluetoothDevice::NUM_CONNECT_ERROR_CODES == 15,
       "Update required if the number of BluetoothDevice enums changes.");
   EXPECT_CALL(*device0_, CreateGattConnection(_, _))
       .Times(9)
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 62b1b188..e01b7f0 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -6384,6 +6384,14 @@
     "expiry_milestone": 130
   },
   {
+    "name": "omnibox-starter-pack-iph",
+    "owners": [
+      "yoangela@chromium.org",
+      "chrome-omnibox-team@google.com"
+    ],
+    "expiry_milestone": 130
+  },
+  {
     "name": "omnibox-suggestion-answer-migration",
     "owners": ["jennserrano@google.com", "chrome-omnibox-team@google.com"],
     "expiry_milestone": 130
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index d44be8f..152c5bb 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2561,6 +2561,12 @@
 const char kOmniboxStarterPackExpansionDescription[] =
     "Enables additional providers for the Site search starter pack feature";
 
+const char kOmniboxStarterPackIPHName[] =
+    "IPH message for the Site search starter pack";
+const char kOmniboxStarterPackIPHDescription[] =
+    "Enables an informational IPH message for the  Site search starter pack "
+    "feature";
+
 #if BUILDFLAG(IS_ANDROID)
 const char kOmnibox2023RefreshConnectionSecurityIndicatorsName[] =
     "Omnibox 2023 refresh connection security indicators";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 2312e94e..47e3ec83 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1489,6 +1489,9 @@
 extern const char kOmniboxStarterPackExpansionName[];
 extern const char kOmniboxStarterPackExpansionDescription[];
 
+extern const char kOmniboxStarterPackIPHName[];
+extern const char kOmniboxStarterPackIPHDescription[];
+
 extern const char kOmniboxZeroSuggestPrefetchingName[];
 extern const char kOmniboxZeroSuggestPrefetchingDescription[];
 
diff --git a/chrome/browser/platform_experience/win b/chrome/browser/platform_experience/win
index 065a550..e226150 160000
--- a/chrome/browser/platform_experience/win
+++ b/chrome/browser/platform_experience/win
@@ -1 +1 @@
-Subproject commit 065a550502a2de7532a2b3d8fb926af407611ab4
+Subproject commit e2261501b3a54bb01cd879dfc0d9297a9a26aa0e
diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc
index 1c1ffa5..14abe72 100644
--- a/chrome/browser/printing/print_browsertest.cc
+++ b/chrome/browser/printing/print_browsertest.cc
@@ -391,7 +391,12 @@
 
 PrintBrowserTest::KillPrintRenderFrame::KillPrintRenderFrame(
     content::RenderProcessHost* rph)
-    : rph_(rph) {}
+    : rph_(rph), print_render_frame_(nullptr) {}
+
+PrintBrowserTest::KillPrintRenderFrame::KillPrintRenderFrame(
+    content::RenderProcessHost* rph,
+    mojom::PrintRenderFrame* print_render_frame)
+    : rph_(rph), print_render_frame_(print_render_frame) {}
 
 PrintBrowserTest::KillPrintRenderFrame::~KillPrintRenderFrame() = default;
 
@@ -418,8 +423,9 @@
 
 mojom::PrintRenderFrame*
 PrintBrowserTest::KillPrintRenderFrame::GetForwardingInterface() {
-  NOTREACHED();
-  return nullptr;
+  CHECK(print_render_frame_);
+  rph_->Shutdown(0);
+  return print_render_frame_;
 }
 
 void PrintBrowserTest::KillPrintRenderFrame::PrintFrameContent(
diff --git a/chrome/browser/printing/print_browsertest.h b/chrome/browser/printing/print_browsertest.h
index 07f9282..912d095 100644
--- a/chrome/browser/printing/print_browsertest.h
+++ b/chrome/browser/printing/print_browsertest.h
@@ -96,6 +96,8 @@
       : public mojom::PrintRenderFrameInterceptorForTesting {
    public:
     explicit KillPrintRenderFrame(content::RenderProcessHost* rph);
+    KillPrintRenderFrame(content::RenderProcessHost* rph,
+                         mojom::PrintRenderFrame* print_render_frame);
     ~KillPrintRenderFrame() override;
 
     void OverrideBinderForTesting(content::RenderFrameHost* render_frame_host);
@@ -113,6 +115,7 @@
 
    private:
     const raw_ptr<content::RenderProcessHost> rph_;
+    const raw_ptr<mojom::PrintRenderFrame> print_render_frame_;
     mojo::AssociatedReceiver<mojom::PrintRenderFrame> receiver_{this};
   };
 
diff --git a/chrome/browser/printing/print_view_manager_base.cc b/chrome/browser/printing/print_view_manager_base.cc
index 261742ba..6b1bc5d 100644
--- a/chrome/browser/printing/print_view_manager_base.cc
+++ b/chrome/browser/printing/print_view_manager_base.cc
@@ -897,10 +897,19 @@
   if (render_frame_host != printing_rfh_)
     return;
 
+  for (auto& observer : GetTestObservers()) {
+    observer.OnRenderFrameDeleted();
+  }
+
   printing_rfh_ = nullptr;
 
   PrintManager::PrintingRenderFrameDeleted();
   ReleasePrinterQuery();
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  if (ShouldPrintJobOop()) {
+    UnregisterSystemPrintClient();
+  }
+#endif
 
   if (!print_job_)
     return;
diff --git a/chrome/browser/printing/print_view_manager_base.h b/chrome/browser/printing/print_view_manager_base.h
index 43e3536..eda41e7 100644
--- a/chrome/browser/printing/print_view_manager_base.h
+++ b/chrome/browser/printing/print_view_manager_base.h
@@ -69,6 +69,8 @@
     virtual void OnRegisterSystemPrintClient(bool succeeded) {}
 
     virtual void OnDidPrintDocument() {}
+
+    virtual void OnRenderFrameDeleted() {}
   };
 
   PrintViewManagerBase(const PrintViewManagerBase&) = delete;
diff --git a/chrome/browser/printing/system_access_process_print_browsertest.cc b/chrome/browser/printing/system_access_process_print_browsertest.cc
index 58fe98f..72af5a8d 100644
--- a/chrome/browser/printing/system_access_process_print_browsertest.cc
+++ b/chrome/browser/printing/system_access_process_print_browsertest.cc
@@ -30,6 +30,7 @@
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
+#include "content/public/test/no_renderer_crashes_assertion.h"
 #include "mojo/public/cpp/bindings/pending_associated_remote.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "printing/buildflags/buildflags.h"
@@ -793,6 +794,12 @@
     CheckForQuit();
   }
 
+  void OnRenderFrameDeleted() override {
+    if (check_for_render_frame_deleted_) {
+      CheckForQuit();
+    }
+  }
+
   // PrintJob::Observer:
   void OnDestruction() override {
     ++print_job_destruction_count_;
@@ -1055,6 +1062,10 @@
   }
 #endif
 
+  void SetCheckForRenderFrameDeleted(bool check) {
+    check_for_render_frame_deleted_ = check;
+  }
+
   const std::optional<bool> system_print_registration_succeeded() const {
     return system_print_registration_succeeded_;
   }
@@ -1255,6 +1266,7 @@
 #endif
 #if BUILDFLAG(ENABLE_OOP_PRINTING)
   bool check_for_print_preview_done_ = false;
+  bool check_for_render_frame_deleted_ = false;
   TestPrintJobWorker::PrintCallbacks test_print_job_worker_callbacks_;
   TestPrintJobWorkerOop::PrintCallbacks test_print_job_worker_oop_callbacks_;
   CreatePrinterQueryCallback test_create_printer_query_callback_;
@@ -2439,6 +2451,78 @@
 #endif  // BUILDFLAG(IS_WIN)
 
 IN_PROC_BROWSER_TEST_P(SystemAccessProcessSandboxedServicePrintBrowserTest,
+                       PrintPreviewPrintAfterSystemPrintRendererCrash) {
+  AddPrinter("printer1");
+  SetPrinterNameForSubsequentContexts("printer1");
+
+  ASSERT_TRUE(embedded_test_server()->Started());
+  GURL url(embedded_test_server()->GetURL("/printing/test3.html"));
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(web_contents);
+  SetUpPrintViewManager(web_contents);
+
+  content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
+  content::RenderProcessHost* frame_rph = frame->GetProcess();
+
+  KillPrintRenderFrame frame_content(frame_rph,
+                                     GetPrintRenderFrame(frame).get());
+  frame_content.OverrideBinderForTesting(frame);
+
+  // With the renderer being prepared to fake a crash, the test needs to watch
+  // for it being deleted.
+  SetCheckForRenderFrameDeleted(/*check=*/true);
+  content::ScopedAllowRendererCrashes allow_renderer_crash;
+
+  // First invoke system print directly.
+
+  // The expected events for this are:
+  // 1.  Printing is attempted, but quickly get notified that the render frame
+  //     has been deleted because the renderer "crashed".
+  SetNumExpectedMessages(/*num=*/1);
+
+  StartBasicPrint(web_contents);
+
+  WaitUntilCallbackReceived();
+
+  // After renderer crash, reload the page again in the same tab.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  // Now try to initiate print from a Print Preview.
+  PrepareRunloop();
+  ResetNumReceivedMessages();
+
+  // No longer interested in when the renderer is deleted.
+  SetCheckForRenderFrameDeleted(/*check=*/false);
+
+  // The expected events for this are:
+  // 1.  Update print settings.
+  // 2.  A print job is started.
+  // 3.  Rendering for 1 page of document of content.
+  // 4.  Completes with document done.
+  // 5.  Wait for the one print job to be destroyed, to ensure printing
+  //     finished cleanly before completing the test.
+  SetNumExpectedMessages(/*num=*/5);
+
+  PrintAfterPreviewIsReadyAndLoaded();
+
+  EXPECT_EQ(start_printing_result(), mojom::ResultCode::kSuccess);
+#if BUILDFLAG(IS_WIN)
+  // TODO(crbug.com/40100562)  Include Windows coverage of
+  // RenderPrintedDocument() once XPS print pipeline is added.
+  EXPECT_EQ(render_printed_page_result(), mojom::ResultCode::kSuccess);
+  EXPECT_EQ(render_printed_page_count(), 1);
+#else
+  EXPECT_EQ(render_printed_document_result(), mojom::ResultCode::kSuccess);
+#endif
+  EXPECT_EQ(document_done_result(), mojom::ResultCode::kSuccess);
+  EXPECT_EQ(error_dialog_shown_count(), 0u);
+  EXPECT_EQ(print_job_destruction_count(), 1);
+}
+
+IN_PROC_BROWSER_TEST_P(SystemAccessProcessSandboxedServicePrintBrowserTest,
                        StartBasicPrint) {
   AddPrinter("printer1");
   SetPrinterNameForSubsequentContexts("printer1");
diff --git a/chrome/browser/resources/lens/overlay/lens_overlay_app.html b/chrome/browser/resources/lens/overlay/lens_overlay_app.html
index 8561e85..4677e1e3 100644
--- a/chrome/browser/resources/lens/overlay/lens_overlay_app.html
+++ b/chrome/browser/resources/lens/overlay/lens_overlay_app.html
@@ -6,20 +6,10 @@
     background-color: rgba(60, 60, 60, 0.5);
   }
 
-  .action-button {
-    position: absolute;
-    left: 0;
-  }
-
   #closeButton {
-    top: 25px;
-  }
-
-  lens-selection-overlay {
     position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
+    inset-block-start: 24px;
+    inset-inline-end: 24px;
   }
 </style>
 <div class="app-container">
diff --git a/chrome/browser/resources/lens/overlay/region_selection.html b/chrome/browser/resources/lens/overlay/region_selection.html
index 93f484b..c4ba3cae5 100644
--- a/chrome/browser/resources/lens/overlay/region_selection.html
+++ b/chrome/browser/resources/lens/overlay/region_selection.html
@@ -13,5 +13,5 @@
   width="[[canvasWidth]]"></canvas>
 <!-- Provides image source for canvas; element is not displayed. -->
 <div id="highlightImgContainer">
-  <img id="highlightImgSrc" src="screenshot.jpeg">
+  <img id="highlightImg" src="screenshot.jpeg">
 </div>
diff --git a/chrome/browser/resources/lens/overlay/region_selection.ts b/chrome/browser/resources/lens/overlay/region_selection.ts
index e49da09..c425a7ef 100644
--- a/chrome/browser/resources/lens/overlay/region_selection.ts
+++ b/chrome/browser/resources/lens/overlay/region_selection.ts
@@ -13,7 +13,10 @@
 import type {GestureEvent} from './selection_utils.js';
 
 export interface RegionSelectionElement {
-  $: {regionSelectionCanvas: HTMLCanvasElement};
+  $: {
+    highlightImg: HTMLImageElement,
+    regionSelectionCanvas: HTMLCanvasElement,
+  };
 }
 
 /*
@@ -141,9 +144,8 @@
     // Draw the highlight image clipped to the path.
     this.context.save();
     this.context.clip();
-    const image = this.shadowRoot!.querySelector('#highlightImgSrc');
     this.context.drawImage(
-        image as HTMLImageElement, 0, 0, this.canvasWidth, this.canvasHeight);
+        this.$.highlightImg, 0, 0, this.canvasWidth, this.canvasHeight);
     this.context.restore();
 
     // Stroke the path on top of the image.
diff --git a/chrome/browser/resources/lens/overlay/selection_overlay.html b/chrome/browser/resources/lens/overlay/selection_overlay.html
index 7533fd01f..9c169d26 100644
--- a/chrome/browser/resources/lens/overlay/selection_overlay.html
+++ b/chrome/browser/resources/lens/overlay/selection_overlay.html
@@ -1,20 +1,40 @@
 <style>
+  :host {
+    align-items: center;
+    display: flex;
+    height: 100%;
+    justify-content: center;
+    width: 100%;
+  }
+
   #selectionOverlay {
     display: grid;
+    position: relative;
+  }
+
+  :host([is-resized]) #selectionOverlay {
+    border-radius: 28px;
+    overflow: hidden;
   }
 
   /* Force all child elements to share the same grid cell so they overlap. */
   #selectionOverlay > * {
     grid-column: 1;
     grid-row: 1;
+
+    user-select: none;
+    -webkit-user-drag: none;
+    -webkit-user-select: none;
   }
 
   #backgroundImage {
-    pointer-events: none;
     max-width: 100vw;
     max-height: 100vh;
-    pointer-events: none;
-    user-select: none;
+  }
+
+  :host([is-resized]) #backgroundImage{
+    max-width: calc(100vw - 24px);
+    max-height: calc(100vh - 24px);
   }
 
   #selectionElements > * {
@@ -23,7 +43,7 @@
   }
   /* Temporary red scrim to make it evident the overlay is opened. */
   #scrim {
-    position: absolute;
+    position: fixed;
     inset: 0;
     background-color: rgba(255, 0, 0, 0.15);
     pointer-events: none;
diff --git a/chrome/browser/resources/lens/overlay/selection_overlay.ts b/chrome/browser/resources/lens/overlay/selection_overlay.ts
index e06aae4..58b0537e 100644
--- a/chrome/browser/resources/lens/overlay/selection_overlay.ts
+++ b/chrome/browser/resources/lens/overlay/selection_overlay.ts
@@ -16,8 +16,11 @@
 import {DRAG_THRESHOLD, DragFeature, emptyGestureEvent, type GestureEvent, GestureState} from './selection_utils.js';
 import type {TextLayerElement} from './text_layer.js';
 
+const RESIZE_THRESHOLD = 8;
+
 export interface SelectionOverlayElement {
   $: {
+    backgroundImage: HTMLImageElement,
     objectSelectionLayer: ObjectLayerElement,
     postSelectionRenderer: PostSelectionRendererElement,
     regionSelectionLayer: RegionSelectionElement,
@@ -42,6 +45,16 @@
     return getTemplate();
   }
 
+  static get properties() {
+    return {
+      isResized: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+    };
+  }
+
   // The current gesture event. The coordinate values are only accurate if a
   // gesture has started.
   private currentGesture: GestureEvent = emptyGestureEvent();
@@ -51,15 +64,28 @@
   private resizeObserver: ResizeObserver = new ResizeObserver(() => {
     this.handleResize();
   });
+  // We need to listen to resizes on the selectionElements separately, since
+  // resizeObserver will trigger before the selectionElements have a chance to
+  // resize.
+  private selectionElementsResizeObserver: ResizeObserver =
+      new ResizeObserver(() => {
+        this.handleSelectionElementsResize();
+      });
+  private initialWidth: number = 0;
+  private initialHeight: number = 0;
+  // Whether the selection overlay is its initial size, or has changed size.
+  private isResized: boolean;
 
   override connectedCallback() {
     super.connectedCallback();
     this.resizeObserver.observe(this);
+    this.selectionElementsResizeObserver.observe(this.$.selectionOverlay);
   }
 
   override disconnectedCallback() {
     super.disconnectedCallback();
     this.resizeObserver.unobserve(this);
+    this.selectionElementsResizeObserver.unobserve(this.$.selectionOverlay);
   }
 
   override ready() {
@@ -167,7 +193,24 @@
 
   private handleResize() {
     const newRect = this.getBoundingClientRect();
-    this.$.regionSelectionLayer.setCanvasSizeTo(newRect.width, newRect.height);
+
+    if (this.initialHeight === 0 || this.initialWidth === 0) {
+      this.initialWidth = newRect.width;
+      this.initialHeight = newRect.height;
+    }
+    // We allow a buffer threshold when determining if the page has been
+    // resized so that subtle one pixel adjustments don't trigger an entire
+    // page reflow.
+    this.isResized =
+        Math.abs(newRect.height - this.initialHeight) >= RESIZE_THRESHOLD ||
+        Math.abs(newRect.width - this.initialWidth) >= RESIZE_THRESHOLD;
+  }
+
+  handleSelectionElementsResize() {
+    const selectionOverlayBounds =
+        this.$.selectionOverlay.getBoundingClientRect();
+    this.$.regionSelectionLayer.setCanvasSizeTo(
+        selectionOverlayBounds.width, selectionOverlayBounds.height);
   }
 
   // Updates the currentGesture to correspond with the given PointerEvent.
diff --git a/chrome/browser/resources/settings/privacy_page/security_page.html b/chrome/browser/resources/settings/privacy_page/security_page.html
index 37cbcacd1..8398d22b 100644
--- a/chrome/browser/resources/settings/privacy_page/security_page.html
+++ b/chrome/browser/resources/settings/privacy_page/security_page.html
@@ -59,6 +59,10 @@
         padding-block-start: var(--cr-section-vertical-padding);
         padding-block-end: var(--cr-section-vertical-padding);
       }
+
+      #learnMoreLabelContainer {
+        pointer-events: auto;
+      }
     </style>
     <picture>
       <source
@@ -187,7 +191,7 @@
                   </li>
                 </ul>
                 <div id="learnMoreLabelContainer" class="cr-secondary-text">
-                  $i18nRaw{enhancedProtectionLearnMoreLabel}
+                  $i18nRaw{safeBrowsingEnhancedLearnMoreLabel}
                 </div>
               </div>
             </div>
diff --git a/chrome/browser/resources/settings/privacy_page/security_page.ts b/chrome/browser/resources/settings/privacy_page/security_page.ts
index cbc1613..8d0bcae 100644
--- a/chrome/browser/resources/settings/privacy_page/security_page.ts
+++ b/chrome/browser/resources/settings/privacy_page/security_page.ts
@@ -562,6 +562,12 @@
   }
   // </if>
 
+  private onEnhancedProtectionLearnMoreClick_(e: Event) {
+    OpenWindowProxyImpl.getInstance().openUrl(
+        loadTimeData.getString('enhancedProtectionHelpCenterURL'));
+    e.preventDefault();
+  }
+
   private onSafeBrowsingExtendedReportingChange_() {
     this.metricsBrowserProxy_.recordSettingsPageHistogram(
         PrivacyElementInteractions.IMPROVE_SECURITY);
diff --git a/chrome/browser/resources/welcome/BUILD.gn b/chrome/browser/resources/welcome/BUILD.gn
index 2fd06475..89b854c 100644
--- a/chrome/browser/resources/welcome/BUILD.gn
+++ b/chrome/browser/resources/welcome/BUILD.gn
@@ -74,7 +74,6 @@
     "google_apps/google_app_proxy.ts",
     "google_apps/google_apps_metrics_proxy.ts",
     "landing_view_proxy.ts",
-    "navigation_mixin.ts",
     "navigation_mixin_lit.ts",
     "ntp_background/ntp_background_metrics_proxy.ts",
     "ntp_background/ntp_background_proxy.ts",
@@ -89,6 +88,7 @@
 
   # Files that are passed as input to css_to_wrapper().
   css_files = [
+    "google_apps/nux_google_apps.css",
     "landing_view.css",
     "ntp_background/nux_ntp_background.css",
     "set_as_default/nux_set_as_default.css",
@@ -105,6 +105,7 @@
     "shared/splash_pages_shared_lit.css",
     "shared/step_indicator.css",
     "signin_view.css",
+    "welcome_app.css",
   ]
 
   icons_html_files = [ "shared/icons.html" ]
diff --git a/chrome/browser/resources/welcome/google_apps/nux_google_apps.css b/chrome/browser/resources/welcome/google_apps/nux_google_apps.css
new file mode 100644
index 0000000..a8b57c2
--- /dev/null
+++ b/chrome/browser/resources/welcome/google_apps/nux_google_apps.css
@@ -0,0 +1,164 @@
+/* Copyright 2024 The Chromium Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+
+/* #css_wrapper_metadata_start
+ * #type=style-lit
+ * import=chrome://resources/cr_elements/cr_shared_vars.css.js
+ * #import=../shared/animations_lit.css.js
+ * #import=../shared/chooser_shared_lit.css.js
+ * #include=animations-lit chooser-shared-lit
+ * #css_wrapper_metadata_end */
+
+.apps-ask {
+  text-align: center;
+}
+
+.chrome-logo {
+  background-image: url(../images/module_icons/add_bookmarks.svg);
+  background-position: center bottom;
+  background-size: 170px 170px;
+  height: 146px;
+  margin: auto;
+  margin-bottom: 32px;
+  width: 170px;
+}
+
+h1 {
+  color: var(--cr-primary-text-color);
+  font-size: 1.5rem;
+  font-weight: 500;
+  margin: 0;
+  margin-bottom: 48px;
+  outline: none;
+}
+
+#appChooser {
+  display: block;
+  white-space: nowrap;
+}
+
+.button-bar {
+  margin-top: 4rem;
+}
+
+.option {
+  -webkit-appearance: none;
+  align-items: center;
+  border-radius: 8px;
+  box-sizing: border-box;
+  display: inline-flex;
+  font-family: inherit;
+  height: 7.5rem;
+  justify-content: center;
+  outline: 0;
+  position: relative;
+  transition-duration: 500ms;
+  transition-property: box-shadow;
+  vertical-align: bottom;
+  width: 6.25rem;
+}
+
+.option:not(:first-of-type) {
+  margin-inline-start: 1.5rem;
+}
+
+.option[active] {
+  border: 1px solid var(--cr-checked-color);
+  color: var(--cr-checked-color);
+  font-weight: 500;
+}
+
+.option.keyboard-focused:focus {
+  outline: var(--navi-keyboard-focus-color) solid 3px;
+}
+
+.option-name {
+  flex-grow: 0;
+  line-height: 1.25rem;
+  text-align: center;
+  white-space: normal;
+}
+
+.option-icon {
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: contain;
+  height: 2rem;
+  margin: auto;
+  width: 2rem;
+}
+
+.option-icon-shadow {
+  background-color: var(--navi-option-icon-shadow-color);
+  border-radius: 50%;
+  display: flex;
+  height: 3rem;
+  margin-bottom: .25rem;
+  width: 3rem;
+}
+
+.option iron-icon {
+  --iron-icon-fill-color: var(--cr-card-background-color);
+  background: var(--navi-check-icon-color);
+  border-radius: 50%;
+  display: none;
+  height: .75rem;
+  margin: 0;
+  position: absolute;
+  right: .375rem;
+  top: .375rem;
+  width: .75rem;
+}
+
+:host-context([dir=rtl]) .option iron-icon {
+  left: .375rem;
+  right: unset;
+}
+
+.option.keyboard-focused:focus iron-icon[icon='cr:check'],
+.option:hover iron-icon[icon='cr:check'],
+.option[active] iron-icon[icon='cr:check'] {
+  display: block;
+}
+
+.option[active] iron-icon[icon='cr:check'] {
+  background: var(--cr-checked-color);
+}
+
+/* App Icons */
+.gmail {
+  content: image-set(
+      url(chrome://theme/IDS_WELCOME_GMAIL@1x) 1x,
+      url(chrome://theme/IDS_WELCOME_GMAIL@2x) 2x);
+}
+
+.youtube {
+  content: image-set(
+      url(chrome://theme/IDS_WELCOME_YOUTUBE@1x) 1x,
+      url(chrome://theme/IDS_WELCOME_YOUTUBE@2x) 2x);
+}
+
+.maps {
+  content: image-set(
+      url(chrome://theme/IDS_WELCOME_MAPS@1x) 1x,
+      url(chrome://theme/IDS_WELCOME_MAPS@2x) 2x);
+}
+
+.translate {
+  content: image-set(
+      url(chrome://theme/IDS_WELCOME_TRANSLATE@1x) 1x,
+      url(chrome://theme/IDS_WELCOME_TRANSLATE@2x) 2x);
+}
+
+.news {
+  content: image-set(
+      url(chrome://theme/IDS_WELCOME_NEWS@1x) 1x,
+      url(chrome://theme/IDS_WELCOME_NEWS@2x) 2x);
+}
+
+.search {
+  content: image-set(
+      url(chrome://theme/IDS_WELCOME_SEARCH@1x) 1x,
+      url(chrome://theme/IDS_WELCOME_SEARCH@2x) 2x);
+}
diff --git a/chrome/browser/resources/welcome/google_apps/nux_google_apps.html b/chrome/browser/resources/welcome/google_apps/nux_google_apps.html
index 51287fd..863abcb 100644
--- a/chrome/browser/resources/welcome/google_apps/nux_google_apps.html
+++ b/chrome/browser/resources/welcome/google_apps/nux_google_apps.html
@@ -1,183 +1,30 @@
-<style include="animations chooser-shared">
-  .apps-ask {
-    text-align: center;
-  }
-
-  .chrome-logo {
-    background-image: url(../images/module_icons/add_bookmarks.svg);
-    background-position: center bottom;
-    background-size: 170px 170px;
-    height: 146px;
-    margin: auto;
-    margin-bottom: 32px;
-    width: 170px;
-  }
-
-  h1 {
-    color: var(--cr-primary-text-color);
-    font-size: 1.5rem;
-    font-weight: 500;
-    margin: 0;
-    margin-bottom: 48px;
-    outline: none;
-  }
-
-  #appChooser {
-    display: block;
-    white-space: nowrap;
-  }
-
-  .button-bar {
-    margin-top: 4rem;
-  }
-
-  .option {
-    -webkit-appearance: none;
-    align-items: center;
-    border-radius: 8px;
-    box-sizing: border-box;
-    display: inline-flex;
-    font-family: inherit;
-    height: 7.5rem;
-    justify-content: center;
-    outline: 0;
-    position: relative;
-    transition-duration: 500ms;
-    transition-property: box-shadow;
-    vertical-align: bottom;
-    width: 6.25rem;
-  }
-
-  .option:not(:first-of-type) {
-    margin-inline-start: 1.5rem;
-  }
-
-  .option[active] {
-    border: 1px solid var(--cr-checked-color);
-    color: var(--cr-checked-color);
-    font-weight: 500;
-  }
-
-  .option.keyboard-focused:focus {
-    outline: var(--navi-keyboard-focus-color) solid 3px;
-  }
-
-  .option-name {
-    flex-grow: 0;
-    line-height: 1.25rem;
-    text-align: center;
-    white-space: normal;
-  }
-
-  .option-icon {
-    background-position: center;
-    background-repeat: no-repeat;
-    background-size: contain;
-    height: 2rem;
-    margin: auto;
-    width: 2rem;
-  }
-
-  .option-icon-shadow {
-    background-color: var(--navi-option-icon-shadow-color);
-    border-radius: 50%;
-    display: flex;
-    height: 3rem;
-    margin-bottom: .25rem;
-    width: 3rem;
-  }
-
-  .option iron-icon {
-    --iron-icon-fill-color: var(--cr-card-background-color);
-    background: var(--navi-check-icon-color);
-    border-radius: 50%;
-    display: none;
-    height: .75rem;
-    margin: 0;
-    position: absolute;
-    right: .375rem;
-    top: .375rem;
-    width: .75rem;
-  }
-
-  :host-context([dir=rtl]) .option iron-icon {
-    left: .375rem;
-    right: unset;
-  }
-
-  .option.keyboard-focused:focus iron-icon[icon='cr:check'],
-  .option:hover iron-icon[icon='cr:check'],
-  .option[active] iron-icon[icon='cr:check'] {
-    display: block;
-  }
-
-  .option[active] iron-icon[icon='cr:check'] {
-    background: var(--cr-checked-color);
-  }
-
-  /* App Icons */
-  .gmail {
-    content: image-set(
-        url(chrome://theme/IDS_WELCOME_GMAIL@1x) 1x,
-        url(chrome://theme/IDS_WELCOME_GMAIL@2x) 2x);
-  }
-
-  .youtube {
-    content: image-set(
-        url(chrome://theme/IDS_WELCOME_YOUTUBE@1x) 1x,
-        url(chrome://theme/IDS_WELCOME_YOUTUBE@2x) 2x);
-  }
-
-  .maps {
-    content: image-set(
-        url(chrome://theme/IDS_WELCOME_MAPS@1x) 1x,
-        url(chrome://theme/IDS_WELCOME_MAPS@2x) 2x);
-  }
-
-  .translate {
-    content: image-set(
-        url(chrome://theme/IDS_WELCOME_TRANSLATE@1x) 1x,
-        url(chrome://theme/IDS_WELCOME_TRANSLATE@2x) 2x);
-  }
-
-  .news {
-    content: image-set(
-        url(chrome://theme/IDS_WELCOME_NEWS@1x) 1x,
-        url(chrome://theme/IDS_WELCOME_NEWS@2x) 2x);
-  }
-
-  .search {
-    content: image-set(
-        url(chrome://theme/IDS_WELCOME_SEARCH@1x) 1x,
-        url(chrome://theme/IDS_WELCOME_SEARCH@2x) 2x);
-  }
-</style>
 <div class="apps-ask">
   <div class="chrome-logo" aria-hidden="true"></div>
-  <h1 tabindex="-1">[[subtitle]]</h1>
+  <h1 tabindex="-1">${this.subtitle}</h1>
   <div id="appChooser">
     <div class="slide-in">
-      <template is="dom-repeat" items="[[appList_]]">
-        <button active$="[[item.selected]]"
-            aria-pressed$="[[getAriaPressed_(item.selected)]]"
-            on-click="onAppClick_" on-pointerdown="onAppPointerDown_"
-            on-keyup="onAppKeyUp_" class="option">
+      ${this.appList_.map((item, index) => html`
+        <button ?active="${item.selected}"
+            aria-pressed="${item.selected}"
+            data-index="${index}" @click="${this.onAppClick_}"
+            @pointerdown="${this.onAppPointerDown_}"
+            @keyup="${this.onAppKeyUp_}" class="option">
           <div class="option-icon-shadow">
-            <div class$="[[item.icon]] option-icon"></div>
+            <div class="${item.icon} option-icon"></div>
           </div>
-          <div class="option-name">[[item.name]]</div>
+          <div class="option-name">${item.name}</div>
           <iron-icon icon="cr:check"></iron-icon>
         </button>
-      </template>
+      `)}
     </div>
 
     <div class="button-bar">
-      <cr-button id="noThanksButton" on-click="onNoThanksClicked_">
+      <cr-button id="noThanksButton" @click="${this.onNoThanksClicked_}">
         $i18n{skip}
       </cr-button>
-      <step-indicator model="[[indicatorModel]]"></step-indicator>
-      <cr-button class="action-button" disabled$="[[!hasAppsSelected_]]"
-          on-click="onNextClicked_">
+      <step-indicator .model="${this.indicatorModel}"></step-indicator>
+      <cr-button class="action-button" ?disabled="${!this.hasAppsSelected_}"
+          @click="${this.onNextClicked_}">
         $i18n{next}
         <iron-icon icon="cr:chevron-right"></iron-icon>
       </cr-button>
diff --git a/chrome/browser/resources/welcome/google_apps/nux_google_apps.ts b/chrome/browser/resources/welcome/google_apps/nux_google_apps.ts
index 8389e918..e2ec9e7f 100644
--- a/chrome/browser/resources/welcome/google_apps/nux_google_apps.ts
+++ b/chrome/browser/resources/welcome/google_apps/nux_google_apps.ts
@@ -4,20 +4,17 @@
 
 import 'chrome://resources/cr_elements/cr_button/cr_button.js';
 import 'chrome://resources/cr_elements/icons.html.js';
-import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
 import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
-import '../shared/animations.css.js';
-import '../shared/chooser_shared.css.js';
 import '../shared/step_indicator.js';
 import '../strings.m.js';
 
 import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {isRTL} from 'chrome://resources/js/util.js';
-import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
 
-import {NavigationMixin} from '../navigation_mixin.js';
+import {NavigationMixinLit} from '../navigation_mixin_lit.js';
 import {navigateToNextStep} from '../router.js';
 import type {BookmarkProxy} from '../shared/bookmark_proxy.js';
 import {BookmarkBarManager, BookmarkProxyImpl} from '../shared/bookmark_proxy.js';
@@ -27,7 +24,8 @@
 import type {GoogleAppProxy} from './google_app_proxy.js';
 import {GoogleAppProxyImpl} from './google_app_proxy.js';
 import {GoogleAppsMetricsProxyImpl} from './google_apps_metrics_proxy.js';
-import {getTemplate} from './nux_google_apps.html.js';
+import {getCss} from './nux_google_apps.css.js';
+import {getHtml} from './nux_google_apps.html.js';
 
 interface AppItem {
   id: number;
@@ -38,11 +36,6 @@
   selected: boolean;
 }
 
-interface AppItemModel {
-  item: AppItem;
-  set: (p1: string, p2: boolean) => void;
-}
-
 const KEYBOARD_FOCUSED = 'keyboard-focused';
 
 export interface NuxGoogleAppsElement {
@@ -51,33 +44,26 @@
   };
 }
 
-const NuxGoogleAppsElementBase = I18nMixin(NavigationMixin(PolymerElement));
+const NuxGoogleAppsElementBase = I18nMixinLit(NavigationMixinLit(CrLitElement));
 
-/** @polymer */
 export class NuxGoogleAppsElement extends NuxGoogleAppsElementBase {
   static get is() {
     return 'nux-google-apps';
   }
 
-  static get template() {
-    return getTemplate();
+  static override get styles() {
+    return getCss();
   }
 
-  static get properties() {
+  override render() {
+    return getHtml.bind(this)();
+  }
+
+  static override get properties() {
     return {
-      indicatorModel: Object,
-
-      appList_: Array,
-
-      hasAppsSelected_: {
-        type: Boolean,
-        notify: true,
-      },
-
-      subtitle: {
-        type: String,
-        value: loadTimeData.getString('googleAppsDescription'),
-      },
+      indicatorModel: {type: Object},
+      appList_: {type: Array},
+      hasAppsSelected_: {type: Boolean},
     };
   }
 
@@ -87,13 +73,14 @@
   private bookmarkProxy_: BookmarkProxy;
   private bookmarkBarManager_: BookmarkBarManager;
   private wasBookmarkBarShownOnInit_: boolean = false;
-  private appList_: AppItem[]|null = null;
-  private hasAppsSelected_: boolean = true;
+  protected appList_: AppItem[] = [];
+  protected hasAppsSelected_: boolean = true;
   indicatorModel?: StepIndicatorModel;
 
   constructor() {
     super();
 
+    this.subtitle = loadTimeData.getString('googleAppsDescription');
     this.appProxy_ = GoogleAppProxyImpl.getInstance();
     this.metricsManager_ =
         new ModuleMetricsManager(GoogleAppsMetricsProxyImpl.getInstance());
@@ -158,7 +145,7 @@
   private cleanUp_() {
     this.finalized_ = true;
 
-    if (!this.appList_) {
+    if (this.appList_.length === 0) {
       return;
     }  // No apps to remove.
 
@@ -182,10 +169,12 @@
   /**
    * Handle toggling the apps selected.
    */
-  private onAppClick_(e: {model: AppItemModel}) {
-    const item = e.model.item;
+  protected onAppClick_(e: Event) {
+    const index = Number((e.currentTarget as HTMLElement).dataset['index']);
+    const item = this.appList_[index];
 
-    e.model.set('item.selected', !item.selected);
+    item.selected = !item.selected;
+    this.requestUpdate();
 
     this.updateBookmark_(item);
     this.updateHasAppsSelected_();
@@ -198,7 +187,7 @@
     this.announceA11y_(this.i18n(i18nKey));
   }
 
-  private onAppKeyUp_(e: KeyboardEvent) {
+  protected onAppKeyUp_(e: KeyboardEvent) {
     if (e.key === 'ArrowRight') {
       this.changeFocus_(e.currentTarget!, 1);
     } else if (e.key === 'ArrowLeft') {
@@ -208,13 +197,13 @@
     }
   }
 
-  private onAppPointerDown_(e: Event) {
+  protected onAppPointerDown_(e: Event) {
     (e.currentTarget as HTMLElement).classList.remove(KEYBOARD_FOCUSED);
   }
 
-  private onNextClicked_() {
+  protected onNextClicked_() {
     this.finalized_ = true;
-    this.appList_!.forEach(app => {
+    this.appList_.forEach(app => {
       if (app.selected) {
         this.appProxy_.recordProviderSelected(app.id);
       }
@@ -223,7 +212,7 @@
     navigateToNextStep();
   }
 
-  private onNoThanksClicked_() {
+  protected onNoThanksClicked_() {
     this.cleanUp_();
     this.metricsManager_.recordNoThanks();
     navigateToNextStep();
@@ -235,7 +224,7 @@
   private populateAllBookmarks_() {
     this.wasBookmarkBarShownOnInit_ = this.bookmarkBarManager_.getShown();
 
-    if (this.appList_) {
+    if (this.appList_.length > 0) {
       this.appList_.forEach(app => this.updateBookmark_(app));
     } else {
       this.appProxy_.getAppList().then(list => {
@@ -275,19 +264,11 @@
    * Updates the value of hasAppsSelected_.
    */
   private updateHasAppsSelected_() {
-    this.hasAppsSelected_ =
-        !!this.appList_ && this.appList_.some(a => a.selected);
+    this.hasAppsSelected_ = this.appList_.some(a => a.selected);
     if (!this.hasAppsSelected_) {
       this.bookmarkBarManager_.setShown(this.wasBookmarkBarShownOnInit_);
     }
   }
-
-  /**
-   * Converts a boolean to a string because aria-pressed needs a string value.
-   */
-  private getAriaPressed_(value: boolean): string {
-    return value ? 'true' : 'false';
-  }
 }
 
 declare global {
diff --git a/chrome/browser/resources/welcome/landing_view.css b/chrome/browser/resources/welcome/landing_view.css
index 8cd5e48..2dfa7eaf 100644
--- a/chrome/browser/resources/welcome/landing_view.css
+++ b/chrome/browser/resources/welcome/landing_view.css
@@ -7,7 +7,6 @@
  * #import=./shared/animations_lit.css.js
  * #import=./shared/splash_pages_shared_lit.css.js
  * #import=./shared/action_link_style_lit.css.js
- * #scheme=relative
  * #include=animations-lit action-link-style-lit splash-pages-shared-lit
  * #css_wrapper_metadata_end */
 
diff --git a/chrome/browser/resources/welcome/navigation_mixin.ts b/chrome/browser/resources/welcome/navigation_mixin.ts
deleted file mode 100644
index c4534e3..0000000
--- a/chrome/browser/resources/welcome/navigation_mixin.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright 2018 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/**
- * @fileoverview The NavigationMixin is in charge of manipulating and
- *     watching window.history.state changes. The page is using the history
- *     state object to remember state instead of changing the URL directly,
- *     because the flow requires that users can use browser-back/forward to
- *     navigate between steps, without being able to go directly or copy an URL
- *     that points at a specific step. Using history.state object allows adding
- *     or popping history state without actually changing the path.
- */
-
-import '../strings.m.js';
-
-import {assert} from 'chrome://resources/js/assert.js';
-import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
-import type {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
-import {afterNextRender, dedupingMixin} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
-
-import {routeObservers, setCurrentRouteElement} from './router.js';
-import type {Routes} from './router.js';
-
-type Constructor<T> = new (...args: any[]) => T;
-
-/**
- * Elements can override onRoute(Change|Enter|Exit) to handle route changes.
- * Order of hooks being called:
- *   1) onRouteExit() on the old route
- *   2) onRouteChange() on all subscribed routes
- *   3) onRouteEnter() on the new route
- */
-export const NavigationMixin = dedupingMixin(
-    <T extends Constructor<PolymerElement>>(superClass: T): T&
-    Constructor<NavigationMixinInterface> => {
-      class NavigationMixin extends superClass {
-        static get properties() {
-          return {
-            subtitle: String,
-          };
-        }
-
-        subtitle?: string;
-
-        override connectedCallback() {
-          super.connectedCallback();
-
-          assert(!routeObservers.has(this));
-          routeObservers.add(this);
-          const route = (history.state.route as Routes);
-          const step = history.state.step;
-
-          // history state was set when page loaded, so when the element first
-          // attaches, call the route-change handler to initialize first.
-          this.onRouteChange(route, step);
-
-          // Modules are only attached to DOM if they're for the current route,
-          // so as long as the id of an element matches up to the current step,
-          // it means that element is for the current route.
-          if (this.id === `step-${step}`) {
-            setCurrentRouteElement(this);
-            this.notifyRouteEnter();
-          }
-        }
-
-        /**
-         * Notifies elements that route was entered and updates the state of the
-         * app based on the new route.
-         */
-        notifyRouteEnter(): void {
-          this.onRouteEnter();
-          this.updateFocusForA11y();
-          this.updateTitle();
-        }
-
-        /** Called to update focus when progressing through the modules. */
-        updateFocusForA11y(): void {
-          const header = this.shadowRoot!.querySelector('h1');
-          if (header) {
-            afterNextRender(this, () => header.focus());
-          }
-        }
-
-        updateTitle(): void {
-          let title = loadTimeData.getString('headerText');
-          if (this.subtitle) {
-            title += ' - ' + this.subtitle;
-          }
-          document.title = title;
-        }
-
-        override disconnectedCallback() {
-          super.disconnectedCallback();
-          assert(routeObservers.delete(this));
-        }
-
-        onRouteChange(_route: Routes, _step: number): void {}
-        onRouteEnter(): void {}
-        onRouteExit(): void {}
-        onRouteUnload(): void {}
-      }
-
-      return NavigationMixin;
-    });
-
-export interface NavigationMixinInterface {
-  subtitle?: string;
-  notifyRouteEnter(): void;
-  updateFocusForA11y(): void;
-  updateTitle(): void;
-  onRouteChange(route: Routes, step: number): void;
-  onRouteEnter(): void;
-  onRouteExit(): void;
-  onRouteUnload(): void;
-}
diff --git a/chrome/browser/resources/welcome/router.ts b/chrome/browser/resources/welcome/router.ts
index 3f249bb..171cbde 100644
--- a/chrome/browser/resources/welcome/router.ts
+++ b/chrome/browser/resources/welcome/router.ts
@@ -4,7 +4,7 @@
 
 import {assert} from 'chrome://resources/js/assert.js';
 
-import type {NavigationMixinInterface} from './navigation_mixin.js';
+import type {NavigationMixinLitInterface} from './navigation_mixin_lit.js';
 
 /**
  * Valid route pathnames.
@@ -15,11 +15,11 @@
   RETURNING_USER = 'returning-user'
 }
 
-export const routeObservers: Set<NavigationMixinInterface> = new Set();
+export const routeObservers: Set<NavigationMixinLitInterface> = new Set();
 
-let currentRouteElement: NavigationMixinInterface|null;
+let currentRouteElement: NavigationMixinLitInterface|null;
 
-export function setCurrentRouteElement(element: NavigationMixinInterface) {
+export function setCurrentRouteElement(element: NavigationMixinLitInterface) {
   currentRouteElement = element;
 }
 
@@ -44,7 +44,7 @@
 
   // If currentRouteElement is not null, it means there was a new route.
   if (currentRouteElement) {
-    (currentRouteElement as NavigationMixinInterface).notifyRouteEnter();
+    (currentRouteElement as NavigationMixinLitInterface).notifyRouteEnter();
   }
 }
 
diff --git a/chrome/browser/resources/welcome/shared/onboarding_background.css b/chrome/browser/resources/welcome/shared/onboarding_background.css
index 68b1e67..47f95954 100644
--- a/chrome/browser/resources/welcome/shared/onboarding_background.css
+++ b/chrome/browser/resources/welcome/shared/onboarding_background.css
@@ -4,7 +4,6 @@
 
 /* #css_wrapper_metadata_start
  * #type=style-lit
- * #scheme=relative
  * #css_wrapper_metadata_end */
 
 :host {
diff --git a/chrome/browser/resources/welcome/shared/step_indicator.css b/chrome/browser/resources/welcome/shared/step_indicator.css
index 03098537..806f523 100644
--- a/chrome/browser/resources/welcome/shared/step_indicator.css
+++ b/chrome/browser/resources/welcome/shared/step_indicator.css
@@ -5,7 +5,6 @@
 /* #css_wrapper_metadata_start
  * #type=style-lit
  * #import=./navi_colors_lit.css.js
- * #scheme=relative
  * #include=navi-colors-lit
  * #css_wrapper_metadata_end */
 
diff --git a/chrome/browser/resources/welcome/welcome_app.css b/chrome/browser/resources/welcome/welcome_app.css
new file mode 100644
index 0000000..509a821
--- /dev/null
+++ b/chrome/browser/resources/welcome/welcome_app.css
@@ -0,0 +1,31 @@
+/* Copyright 2024 The Chromium Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+
+/* #css_wrapper_metadata_start
+ * #type=style-lit
+ * #import=chrome://resources/cr_elements/cr_hidden_style_lit.css.js
+ * #include=cr-hidden-style-lit
+ * #css_wrapper_metadata_end */
+
+#viewManager {
+  display: flex;
+  font-size: 100%;
+  margin: 0;
+  min-height: 100vh;
+}
+
+#viewManager :-webkit-any(nux-google-apps, nux-ntp-background,
+    nux-set-as-default) {
+  /* Override cr-view-manager's default styling for view. */
+  bottom: initial;
+  left: initial;
+  margin: auto;
+  position: unset;
+  right: initial;
+  top: initial;
+}
+
+cr-toast {
+  min-width: initial;
+}
diff --git a/chrome/browser/resources/welcome/welcome_app.html b/chrome/browser/resources/welcome/welcome_app.html
index 21e0477..48383bf 100644
--- a/chrome/browser/resources/welcome/welcome_app.html
+++ b/chrome/browser/resources/welcome/welcome_app.html
@@ -1,27 +1,5 @@
-<style include="cr-hidden-style">
-  #viewManager {
-    display: flex;
-    font-size: 100%;
-    margin: 0;
-    min-height: 100vh;
-  }
 
-  #viewManager :-webkit-any(nux-google-apps, nux-ntp-background,
-      nux-set-as-default) {
-    /* Override cr-view-manager's default styling for view. */
-    bottom: initial;
-    left: initial;
-    margin: auto;
-    position: unset;
-    right: initial;
-    top: initial;
-  }
-
-  cr-toast {
-    min-width: initial;
-  }
-</style>
-<cr-view-manager id="viewManager" hidden="[[!modulesInitialized_]]">
+<cr-view-manager id="viewManager" ?hidden="${!this.modulesInitialized_}">
   <landing-view id="step-landing" slot="view" class="active"></landing-view>
 </cr-view-manager>
 <cr-toast duration="3000">
diff --git a/chrome/browser/resources/welcome/welcome_app.ts b/chrome/browser/resources/welcome/welcome_app.ts
index 6a0d4f6..4bfbb561 100644
--- a/chrome/browser/resources/welcome/welcome_app.ts
+++ b/chrome/browser/resources/welcome/welcome_app.ts
@@ -4,7 +4,6 @@
 
 import 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
 import 'chrome://resources/cr_elements/cr_view_manager/cr_view_manager.js';
-import 'chrome://resources/cr_elements/cr_hidden_style.css.js';
 import './google_apps/nux_google_apps.js';
 import './landing_view.js';
 import './ntp_background/nux_ntp_background.js';
@@ -16,16 +15,17 @@
 import {assert} from 'chrome://resources/js/assert.js';
 import {FocusOutlineManager} from 'chrome://resources/js/focus_outline_manager.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
-import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
 
 import type {NuxGoogleAppsElement} from './google_apps/nux_google_apps.js';
-import {NavigationMixin} from './navigation_mixin.js';
+import {NavigationMixinLit} from './navigation_mixin_lit.js';
 import type {NuxNtpBackgroundElement} from './ntp_background/nux_ntp_background.js';
 import {Routes} from './router.js';
 import type {NuxSetAsDefaultElement} from './set_as_default/nux_set_as_default.js';
 import {NuxSetAsDefaultProxyImpl} from './set_as_default/nux_set_as_default_proxy.js';
 import {BookmarkBarManager} from './shared/bookmark_proxy.js';
-import {getTemplate} from './welcome_app.html.js';
+import {getCss} from './welcome_app.css.js';
+import {getHtml} from './welcome_app.html.js';
 import {WelcomeBrowserProxyImpl} from './welcome_browser_proxy.js';
 
 /**
@@ -60,7 +60,7 @@
   };
 }
 
-const WelcomeAppElementBase = NavigationMixin(PolymerElement);
+const WelcomeAppElementBase = NavigationMixinLit(CrLitElement);
 
 /** @polymer */
 export class WelcomeAppElement extends WelcomeAppElementBase {
@@ -68,13 +68,17 @@
     return 'welcome-app';
   }
 
-  static get template() {
-    return getTemplate();
+  static override get styles() {
+    return getCss();
   }
 
-  static get properties() {
+  override render() {
+    return getHtml.bind(this)();
+  }
+
+  static override get properties() {
     return {
-      modulesInitialized_: Boolean,
+      modulesInitialized_: {type: Boolean},
     };
   }
   private currentRoute_: Routes|null = null;
@@ -82,7 +86,7 @@
 
   // Default to false so view-manager is hidden until views are
   // initialized.
-  private modulesInitialized_: boolean = false;
+  protected modulesInitialized_: boolean = false;
 
   constructor() {
     super();
@@ -93,8 +97,7 @@
     };
   }
 
-  override ready() {
-    super.ready();
+  override firstUpdated() {
     this.setAttribute('role', 'main');
     this.addEventListener(
         'default-browser-change', () => this.onDefaultBrowserChange_());
@@ -187,14 +190,13 @@
 
           let indicatorActiveCount = 0;
           modules.forEach((elementTagName, index) => {
-            const element =
-                document.createElement(elementTagName) as PolymerElement;
+            const element = document.createElement(elementTagName) as (
+                                NuxGoogleAppsElement | NuxNtpBackgroundElement |
+                                NuxSetAsDefaultElement);
             element.id = 'step-' + (index + 1);
             element.setAttribute('slot', 'view');
             if (MODULES_NEEDING_INDICATOR.has(elementTagName)) {
-              (element as NuxGoogleAppsElement | NuxNtpBackgroundElement |
-               NuxSetAsDefaultElement)
-                  .indicatorModel = {
+              element.indicatorModel = {
                 total: indicatorElementCount,
                 active: indicatorActiveCount++,
               };
diff --git a/chrome/browser/safe_browsing/chrome_ping_manager_factory.cc b/chrome/browser/safe_browsing/chrome_ping_manager_factory.cc
index 03bc86f..284c296b 100644
--- a/chrome/browser/safe_browsing/chrome_ping_manager_factory.cc
+++ b/chrome/browser/safe_browsing/chrome_ping_manager_factory.cc
@@ -19,11 +19,16 @@
 #include "components/safe_browsing/core/browser/ping_manager.h"
 #include "components/safe_browsing/core/browser/sync/safe_browsing_primary_account_token_fetcher.h"
 #include "components/safe_browsing/core/browser/sync/sync_utils.h"
+#include "components/safe_browsing/core/common/features.h"
 #include "components/safe_browsing/core/common/safe_browsing_prefs.h"
 #include "content/public/browser/browser_thread.h"
 
 namespace safe_browsing {
 
+namespace {
+bool kAllowPingManagerInTests = false;
+}  // namespace
+
 // static
 ChromePingManagerFactory* ChromePingManagerFactory::GetInstance() {
   static base::NoDestructor<ChromePingManagerFactory> instance;
@@ -74,7 +79,9 @@
       content::GetUIThreadTaskRunner({}),
       base::BindRepeating(&safe_browsing::GetUserPopulationForProfile, profile),
       base::BindRepeating(&safe_browsing::GetPageLoadTokenForURL, profile),
-      std::move(hats_delegate), /*persister_root_path=*/profile->GetPath());
+      std::move(hats_delegate), /*persister_root_path=*/profile->GetPath(),
+      base::BindRepeating(&ChromePingManagerFactory::ShouldSendPersistedReport,
+                          profile));
 }
 
 // static
@@ -87,4 +94,29 @@
          safe_browsing::SyncUtils::IsPrimaryAccountSignedIn(identity_manager);
 }
 
+// static
+bool ChromePingManagerFactory::ShouldSendPersistedReport(Profile* profile) {
+  return !profile->IsOffTheRecord() &&
+         IsExtendedReportingEnabled(*profile->GetPrefs()) &&
+         base::FeatureList::IsEnabled(kDownloadReportWithoutUserDecision);
+}
+
+bool ChromePingManagerFactory::ServiceIsCreatedWithBrowserContext() const {
+  // When kDownloadReportWithoutUserDecision is enabled, PingManager is created
+  // at startup to send persisted reports.
+  return base::FeatureList::IsEnabled(kDownloadReportWithoutUserDecision);
+}
+
+bool ChromePingManagerFactory::ServiceIsNULLWhileTesting() const {
+  return !kAllowPingManagerInTests;
+}
+
+ChromePingManagerAllowerForTesting::ChromePingManagerAllowerForTesting() {
+  kAllowPingManagerInTests = true;
+}
+
+ChromePingManagerAllowerForTesting::~ChromePingManagerAllowerForTesting() {
+  kAllowPingManagerInTests = false;
+}
+
 }  // namespace safe_browsing
diff --git a/chrome/browser/safe_browsing/chrome_ping_manager_factory.h b/chrome/browser/safe_browsing/chrome_ping_manager_factory.h
index fb09ea7..e9a62bd 100644
--- a/chrome/browser/safe_browsing/chrome_ping_manager_factory.h
+++ b/chrome/browser/safe_browsing/chrome_ping_manager_factory.h
@@ -29,8 +29,25 @@
   // BrowserContextKeyedServiceFactory override:
   std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
       content::BrowserContext* context) const override;
+  bool ServiceIsCreatedWithBrowserContext() const override;
+  bool ServiceIsNULLWhileTesting() const override;
 
   static bool ShouldFetchAccessTokenForReport(Profile* profile);
+  static bool ShouldSendPersistedReport(Profile* profile);
+};
+
+// Used only for tests. By default, the factory returns null for tests . To
+// override it, create an object of this type and keep it in scope for as long
+// as the override should exist. The constructor will set the override, and the
+// destructor will clear it.
+class ChromePingManagerAllowerForTesting {
+ public:
+  ChromePingManagerAllowerForTesting();
+  ChromePingManagerAllowerForTesting(
+      const ChromePingManagerAllowerForTesting&) = delete;
+  ChromePingManagerAllowerForTesting& operator=(
+      const ChromePingManagerAllowerForTesting&) = delete;
+  ~ChromePingManagerAllowerForTesting();
 };
 
 }  // namespace safe_browsing
diff --git a/chrome/browser/safe_browsing/chrome_ping_manager_factory_unittest.cc b/chrome/browser/safe_browsing/chrome_ping_manager_factory_unittest.cc
index bf3d295..a9070619 100644
--- a/chrome/browser/safe_browsing/chrome_ping_manager_factory_unittest.cc
+++ b/chrome/browser/safe_browsing/chrome_ping_manager_factory_unittest.cc
@@ -43,13 +43,15 @@
                                               bool is_signed_in,
                                               bool expect_should_fetch);
   TestingProfile* SetUpProfile(bool is_enhanced_protection, bool is_signed_in);
+  bool ShouldSendPersistedReport(Profile* profile);
 
   content::BrowserTaskEnvironment task_environment_;
   std::unique_ptr<TestingProfileManager> profile_manager_;
+  base::test::ScopedFeatureList feature_list_;
 
  private:
   scoped_refptr<safe_browsing::SafeBrowsingService> sb_service_;
-  base::test::ScopedFeatureList feature_list_;
+  ChromePingManagerAllowerForTesting allow_ping_manager_;
 };
 
 void ChromePingManagerFactoryTest::SetUp() {
@@ -136,6 +138,10 @@
             PingManager::ReportThreatDetailsResult::SUCCESS);
 }
 
+bool ChromePingManagerFactoryTest::ShouldSendPersistedReport(Profile* profile) {
+  return ChromePingManagerFactory::ShouldSendPersistedReport(profile);
+}
+
 TEST_F(ChromePingManagerFactoryTest, ReportThreatDetails) {
   RunReportThreatDetailsTest();
 }
@@ -156,6 +162,39 @@
                                          /*is_signed_in=*/true,
                                          /*expect_should_fetch=*/false);
 }
+
+TEST_F(ChromePingManagerFactoryTest, ShouldSendPersistedReport_Yes) {
+  feature_list_.InitAndEnableFeature(kDownloadReportWithoutUserDecision);
+  TestingProfile* profile =
+      SetUpProfile(/*is_enhanced_protection=*/true, /*is_signed_in=*/false);
+  EXPECT_EQ(ShouldSendPersistedReport(profile), true);
+}
+
+TEST_F(ChromePingManagerFactoryTest,
+       ShouldSendPersistedReport_NotEnhancedProtection) {
+  feature_list_.InitAndEnableFeature(kDownloadReportWithoutUserDecision);
+  TestingProfile* profile =
+      SetUpProfile(/*is_enhanced_protection=*/false, /*is_signed_in=*/false);
+  EXPECT_EQ(ShouldSendPersistedReport(profile), false);
+}
+
+TEST_F(ChromePingManagerFactoryTest, ShouldSendPersistedReport_Incognito) {
+  feature_list_.InitAndEnableFeature(kDownloadReportWithoutUserDecision);
+  TestingProfile* profile =
+      SetUpProfile(/*is_enhanced_protection=*/true, /*is_signed_in=*/false);
+  EXPECT_EQ(ShouldSendPersistedReport(
+                TestingProfile::Builder().BuildIncognito(profile)),
+            false);
+}
+
+TEST_F(ChromePingManagerFactoryTest,
+       ShouldSendPersistedReport_FeatureDisabled) {
+  feature_list_.InitAndDisableFeature(kDownloadReportWithoutUserDecision);
+  TestingProfile* profile =
+      SetUpProfile(/*is_enhanced_protection=*/true, /*is_signed_in=*/false);
+  EXPECT_EQ(ShouldSendPersistedReport(profile), false);
+}
+
 TEST_F(ChromePingManagerFactoryTest, NoPingManagerForIncognito) {
   TestingProfile* profile = TestingProfile::Builder().BuildIncognito(
       profile_manager_->CreateTestingProfile("testing_profile"));
diff --git a/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc b/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
index 9dd8648a..f256ca4d 100644
--- a/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
+++ b/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
@@ -214,6 +214,7 @@
   scoped_refptr<safe_browsing::SafeBrowsingService> sb_service_;
   std::unique_ptr<PhishyInteractionTracker> phishy_interaction_tracker_;
   scoped_refptr<MockSafeBrowsingUIManager> ui_manager_;
+  safe_browsing::ChromePingManagerAllowerForTesting allow_ping_manager_;
 };
 
 TEST_F(PhishyInteractionTrackerTest, CheckHistogramCountsOnPhishyUserEvents) {
diff --git a/chrome/browser/safe_browsing/safe_browsing_service.cc b/chrome/browser/safe_browsing/safe_browsing_service.cc
index 4ba282c..149f3398 100644
--- a/chrome/browser/safe_browsing/safe_browsing_service.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_service.cc
@@ -277,6 +277,10 @@
 SafeBrowsingService::GetURLLoaderFactory(
     content::BrowserContext* browser_context) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  if (url_loader_factory_for_testing_) {
+    return url_loader_factory_for_testing_;
+  }
+
   NetworkContextService* service =
       NetworkContextServiceFactory::GetForBrowserContext(browser_context);
   if (!service) {
@@ -551,9 +555,13 @@
                                      show_download_in_folder);
   Profile* profile = Profile::FromBrowserContext(
       content::DownloadItemUtils::GetBrowserContext(download));
-  return ChromePingManagerFactory::GetForBrowserContext(profile)
-             ->PersistThreatDetailsAndReportOnNextStartup(std::move(report)) ==
-         PingManager::PersistThreatDetailsResult::kPersistTaskPosted;
+  PingManager::PersistThreatDetailsResult result =
+      ChromePingManagerFactory::GetForBrowserContext(profile)
+          ->PersistThreatDetailsAndReportOnNextStartup(std::move(report));
+  base::UmaHistogramEnumeration(
+      "SafeBrowsing.ClientSafeBrowsingReport.PersistDownloadReportResult",
+      result);
+  return result == PingManager::PersistThreatDetailsResult::kPersistTaskPosted;
 }
 
 bool SafeBrowsingService::SendPhishyInteractionsReport(
diff --git a/chrome/browser/safe_browsing/safe_browsing_service.h b/chrome/browser/safe_browsing/safe_browsing_service.h
index 76ffb70..3a91b47 100644
--- a/chrome/browser/safe_browsing/safe_browsing_service.h
+++ b/chrome/browser/safe_browsing/safe_browsing_service.h
@@ -145,6 +145,11 @@
   void FlushNetworkInterfaceForTesting(
       content::BrowserContext* browser_context);
 
+  void SetURLLoaderFactoryForTesting(
+      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
+    url_loader_factory_for_testing_ = url_loader_factory;
+  }
+
   const scoped_refptr<SafeBrowsingUIManager>& ui_manager() const;
 
   virtual const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager()
@@ -361,6 +366,9 @@
   std::unique_ptr<TriggerManager> trigger_manager_;
 
   bool url_is_allowlisted_for_testing_ = false;
+
+  scoped_refptr<network::SharedURLLoaderFactory>
+      url_loader_factory_for_testing_;
 };
 
 SafeBrowsingServiceFactory* GetSafeBrowsingServiceFactory();
diff --git a/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc b/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
index 4eb901e..f7ce20f 100644
--- a/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "chrome/browser/safe_browsing/safe_browsing_service.h"
+
 #include <memory>
 
 #include "base/test/bind.h"
@@ -59,6 +60,10 @@
     // the interface in components/safe_browsing, and remove this cast.
     sb_service_ = static_cast<SafeBrowsingService*>(
         safe_browsing::SafeBrowsingService::CreateSafeBrowsingService());
+    auto ref_counted_url_loader_factory =
+        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+            &test_url_loader_factory_);
+    sb_service_->SetURLLoaderFactoryForTesting(ref_counted_url_loader_factory);
     browser_process_->SetSafeBrowsingService(sb_service_.get());
     sb_service_->Initialize();
     base::RunLoop().RunUntilIdle();
@@ -72,6 +77,7 @@
   }
 
   void TearDown() override {
+    sb_service_->SetURLLoaderFactoryForTesting(nullptr);
     browser_process_->safe_browsing_service()->ShutDown();
     browser_process_->SetSafeBrowsingService(nullptr);
     safe_browsing::SafeBrowsingServiceInterface::RegisterFactory(nullptr);
@@ -194,7 +200,9 @@
   GURL download_url_ = GURL(kTestDownloadUrl);
 
  private:
+  network::TestURLLoaderFactory test_url_loader_factory_;
   base::test::ScopedFeatureList feature_list_;
+  ChromePingManagerAllowerForTesting allow_ping_manager_;
 };
 
 TEST_F(SafeBrowsingServiceTest, SendDownloadReport_Success) {
diff --git a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java
index 8b84967..2e54b11 100644
--- a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java
+++ b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java
@@ -187,6 +187,10 @@
     protected static <T extends PersistedTabData> void from(
             Tab tab, Supplier<T> supplier, Class<T> clazz, Callback<T> callback) {
         ThreadUtils.assertOnUiThread();
+        if (!tab.isInitialized() || tab.isDestroyed() || tab.isCustomTab()) {
+            onInvalidTab(callback);
+            return;
+        }
         T userData = getUserData(tab, clazz);
         // {@link PersistedTabData} already attached to {@link Tab}
         if (userData != null) {
@@ -209,6 +213,10 @@
                         tab.getId(),
                         config.getId(),
                         (data) -> {
+                            if (tab.isDestroyed()) {
+                                onInvalidTab(callback);
+                                return;
+                            }
                             // No stored {@link PersistedTabData} found, return null.
                             if (data == null || data.limit() == 0) {
                                 PostTask.postTask(
@@ -224,6 +232,10 @@
                                 PostTask.postTask(
                                         TaskTraits.USER_BLOCKING_MAY_BLOCK,
                                         () -> {
+                                            if (tab.isDestroyed()) {
+                                                onInvalidTab(callback);
+                                                return;
+                                            }
                                             persistedTabData.deserializeAndLog(data);
                                             // Post result back to UI thread.
                                             PostTask.postTask(
@@ -237,6 +249,14 @@
                         });
     }
 
+    private static <T extends PersistedTabData> void onInvalidTab(Callback<T> callback) {
+        PostTask.postTask(
+                TaskTraits.UI_DEFAULT,
+                () -> {
+                    callback.onResult(null);
+                });
+    }
+
     private static <T extends PersistedTabData> void onPersistedTabDataRetrieved(
             ByteBuffer data,
             PersistedTabDataConfiguration config,
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index dda5bce..f6c9c27d 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -2807,6 +2807,8 @@
       "ash/network/network_state_notifier.h",
       "ash/network/tether_notification_presenter.cc",
       "ash/network/tether_notification_presenter.h",
+      "ash/picker/picker_lacros_omnibox_search_provider.cc",
+      "ash/picker/picker_lacros_omnibox_search_provider.h",
       "ash/picker/picker_client_impl.cc",
       "ash/picker/picker_client_impl.h",
       "ash/picker/picker_file_suggester.cc",
diff --git a/chrome/browser/ui/ash/picker/picker_client_impl.cc b/chrome/browser/ui/ash/picker/picker_client_impl.cc
index d7c8bfa..b44ef7bf 100644
--- a/chrome/browser/ui/ash/picker/picker_client_impl.cc
+++ b/chrome/browser/ui/ash/picker/picker_client_impl.cc
@@ -34,12 +34,12 @@
 #include "chrome/browser/ash/app_list/search/omnibox/omnibox_provider.h"
 #include "chrome/browser/ash/app_list/search/search_engine.h"
 #include "chrome/browser/ash/crosapi/browser_util.h"
-#include "chrome/browser/ash/crosapi/crosapi_manager.h"
 #include "chrome/browser/ash/file_manager/fileapi_util.h"
 #include "chrome/browser/ash/input_method/editor_mediator_factory.h"
 #include "chrome/browser/chromeos/launcher_search/search_util.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/picker/picker_file_suggester.h"
+#include "chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h"
 #include "chrome/browser/ui/webui/ash/emoji/emoji_picker.mojom-forward.h"
 #include "chrome/browser/ui/webui/ash/emoji/emoji_picker.mojom-shared.h"
 #include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
@@ -378,11 +378,10 @@
                                         bool history,
                                         bool open_tabs) {
   if (crosapi::browser_util::IsLacrosEnabled()) {
-    // TODO: b/326147929 - Add autocomplete provider types for the Lacros
-    // provider.
     return std::make_unique<app_list::OmniboxLacrosProvider>(
         profile_, &app_list_controller_delegate_,
-        app_list::OmniboxLacrosProvider::GetSingletonControllerCallback());
+        PickerLacrosOmniboxSearchProvider::CreateControllerCallback(
+            bookmarks, history, open_tabs));
   } else {
     return std::make_unique<app_list::OmniboxProvider>(
         profile_, &app_list_controller_delegate_,
diff --git a/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.cc b/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.cc
new file mode 100644
index 0000000..77c5990
--- /dev/null
+++ b/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.cc
@@ -0,0 +1,65 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/check_deref.h"
+#include "base/functional/bind.h"
+#include "chrome/browser/ash/app_list/search/omnibox/omnibox_lacros_provider.h"
+#include "chrome/browser/ash/crosapi/crosapi_ash.h"
+#include "chrome/browser/ash/crosapi/crosapi_manager.h"
+#include "chrome/browser/ash/crosapi/search_controller_ash.h"
+#include "chrome/browser/ash/crosapi/search_controller_factory_ash.h"
+
+PickerLacrosOmniboxSearchProvider::PickerLacrosOmniboxSearchProvider(
+    crosapi::SearchControllerFactoryAsh* factory,
+    bool bookmarks,
+    bool history,
+    bool open_tabs)
+    : bookmarks_(bookmarks),
+      history_(history),
+      open_tabs_(open_tabs),
+      factory_(CHECK_DEREF(factory)) {
+  if (factory_->IsBound()) {
+    controller_ = factory_->CreateSearchControllerPicker(bookmarks_, history_,
+                                                         open_tabs_);
+  }
+}
+
+PickerLacrosOmniboxSearchProvider::~PickerLacrosOmniboxSearchProvider() =
+    default;
+
+crosapi::SearchControllerAsh*
+PickerLacrosOmniboxSearchProvider::GetController() {
+  if (!controller_ && factory_->IsBound()) {
+    controller_ = factory_->CreateSearchControllerPicker(bookmarks_, history_,
+                                                         open_tabs_);
+  }
+  return controller_.get();
+}
+
+app_list::OmniboxLacrosProvider::SearchControllerCallback
+PickerLacrosOmniboxSearchProvider::CreateControllerCallback(bool bookmarks,
+                                                            bool history,
+                                                            bool open_tabs) {
+  // The following dereferences are safe, because `CrosapiManager::Get()`
+  // `DCHECK`s the returned pointer, and both `CrosapiManager::crosapi_ash()`
+  // and `CrosapiAsh::search_controller_factory_ash()` return a pointer to a
+  // `std::unique_ptr`, which are initialised when the classes are constructed
+  // and never reset.
+  crosapi::SearchControllerFactoryAsh* factory =
+      crosapi::CrosapiManager::Get()
+          ->crosapi_ash()
+          ->search_controller_factory_ash();
+  auto provider = std::make_unique<PickerLacrosOmniboxSearchProvider>(
+      factory, bookmarks, history, open_tabs);
+  return base::BindRepeating(
+      [](const std::unique_ptr<PickerLacrosOmniboxSearchProvider>& provider) {
+        return provider->GetController();
+      },
+      std::move(provider));
+}
diff --git a/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h b/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h
new file mode 100644
index 0000000..74d6ddc9
--- /dev/null
+++ b/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h
@@ -0,0 +1,54 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_ASH_PICKER_PICKER_LACROS_OMNIBOX_SEARCH_PROVIDER_H_
+#define CHROME_BROWSER_UI_ASH_PICKER_PICKER_LACROS_OMNIBOX_SEARCH_PROVIDER_H_
+
+#include <memory>
+
+#include "base/memory/raw_ref.h"
+#include "chrome/browser/ash/app_list/search/omnibox/omnibox_lacros_provider.h"
+
+namespace crosapi {
+class SearchControllerAsh;
+class SearchControllerFactoryAsh;
+}
+
+// Manages a dedicated Picker `crosapi::SearchControllerAsh` obtained from a
+// given `crosapi::SearchControllerFactoryAsh`.
+// Intended to be used to construct a `app_list::OmniboxLacrosProvider` - see
+// the `CreateControllerCallback` static method below.
+class PickerLacrosOmniboxSearchProvider {
+ public:
+  explicit PickerLacrosOmniboxSearchProvider(
+      crosapi::SearchControllerFactoryAsh* factory,
+      bool bookmarks,
+      bool history,
+      bool open_tabs);
+  PickerLacrosOmniboxSearchProvider(const PickerLacrosOmniboxSearchProvider&) =
+      delete;
+  PickerLacrosOmniboxSearchProvider& operator=(
+      const PickerLacrosOmniboxSearchProvider&) = delete;
+  ~PickerLacrosOmniboxSearchProvider();
+
+  crosapi::SearchControllerAsh* GetController();
+
+  // Returns a `SearchControllerCallback` for use with
+  // `app_list::OmniboxLacrosProvider` which uses the singleton
+  // `crosapi::SearchControllerFactoryAsh` to create a dedicated search
+  // controller for Picker.
+  static app_list::OmniboxLacrosProvider::SearchControllerCallback
+  CreateControllerCallback(bool bookmarks, bool history, bool open_tabs);
+
+ private:
+  bool bookmarks_;
+  bool history_;
+  bool open_tabs_;
+
+  std::unique_ptr<crosapi::SearchControllerAsh> controller_;
+
+  base::raw_ref<crosapi::SearchControllerFactoryAsh> factory_;
+};
+
+#endif  // CHROME_BROWSER_UI_ASH_PICKER_PICKER_LACROS_OMNIBOX_SEARCH_PROVIDER_H_
diff --git a/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider_unittest.cc b/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider_unittest.cc
new file mode 100644
index 0000000..41459b4b
--- /dev/null
+++ b/chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider_unittest.cc
@@ -0,0 +1,298 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/auto_reset.h"
+#include "base/check.h"
+#include "base/functional/callback_forward.h"
+#include "base/functional/callback_helpers.h"
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "chrome/browser/ash/crosapi/search_controller_ash.h"
+#include "chrome/browser/ash/crosapi/search_controller_factory_ash.h"
+#include "chromeos/crosapi/mojom/launcher_search.mojom-forward.h"
+#include "chromeos/crosapi/mojom/launcher_search.mojom.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+// TODO: b/326147929 - Share this code with `crosapi::SearchControllerAsh` and
+// `crosapi::SearchControllerFactoryAsh` unit tests.
+class TestMojomSearchController : public crosapi::mojom::SearchController {
+ public:
+  explicit TestMojomSearchController(bool bookmarks,
+                                     bool history,
+                                     bool open_tabs)
+      : bookmarks_(bookmarks), history_(history), open_tabs_(open_tabs) {}
+  ~TestMojomSearchController() override = default;
+
+  void Bind(mojo::PendingReceiver<crosapi::mojom::SearchController> receiver) {
+    receiver_.Bind(std::move(receiver));
+  }
+
+  void RunUntilSearch() {
+    base::RunLoop loop;
+    base::AutoReset<base::RepeatingClosure> quit_loop(&search_callback_,
+                                                      loop.QuitClosure());
+    loop.Run();
+  }
+
+  const std::u16string& last_query() { return last_query_; }
+  bool bookmarks() const { return bookmarks_; }
+  bool history() const { return history_; }
+  bool open_tabs() const { return open_tabs_; }
+
+ private:
+  void Search(const std::u16string& query, SearchCallback callback) override {
+    last_query_ = query;
+    // We are not interested in the search callback in the class under test, but
+    // we still need to run the callback with a valid receiver.
+    std::move(callback).Run(
+        mojo::AssociatedRemote<crosapi::mojom::SearchResultsPublisher>()
+            .BindNewEndpointAndPassReceiver());
+
+    search_callback_.Run();
+  }
+
+  base::RepeatingClosure search_callback_ = base::DoNothing();
+
+  mojo::Receiver<crosapi::mojom::SearchController> receiver_{this};
+  bool bookmarks_;
+  bool history_;
+  bool open_tabs_;
+  std::u16string last_query_;
+};
+
+class TestMojomSearchControllerFactory
+    : public crosapi::mojom::SearchControllerFactory {
+ public:
+  TestMojomSearchControllerFactory() = default;
+  ~TestMojomSearchControllerFactory() override = default;
+
+  mojo::PendingRemote<crosapi::mojom::SearchControllerFactory> BindToRemote() {
+    return receiver_.BindNewPipeAndPassRemote();
+  }
+
+  void RunUntilCreateSearchController() {
+    base::RunLoop loop;
+    base::AutoReset<base::RepeatingClosure> quit_loop(
+        &create_search_controller_callback_, loop.QuitClosure());
+    loop.Run();
+  }
+
+  std::unique_ptr<TestMojomSearchController> TakeLastTestController() {
+    return std::move(last_test_controller_);
+  }
+
+  // cam::SearchControllerFactory overrides:
+  void CreateSearchControllerPicker(
+      mojo::PendingReceiver<crosapi::mojom::SearchController> controller,
+      bool bookmark,
+      bool history,
+      bool open_tab) override {
+    CHECK(!last_test_controller_);
+    last_test_controller_ = std::make_unique<TestMojomSearchController>(
+        bookmark, history, open_tab);
+    last_test_controller_->Bind(std::move(controller));
+
+    create_search_controller_callback_.Run();
+  }
+
+ private:
+  base::RepeatingClosure create_search_controller_callback_ = base::DoNothing();
+
+  std::unique_ptr<TestMojomSearchController> last_test_controller_;
+  mojo::Receiver<crosapi::mojom::SearchControllerFactory> receiver_{this};
+};
+
+using PickerLacrosOmniboxSearchProviderTest = ::testing::Test;
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       ControllerIsNullptrWhenNotBoundOnConstruction) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  ASSERT_FALSE(factory.IsBound());
+
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  crosapi::SearchControllerAsh* controller = provider.GetController();
+
+  EXPECT_FALSE(controller);
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       ControllerIsNonNullWhenBoundOnConstruction) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+  ASSERT_TRUE(factory.IsBound());
+
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  crosapi::SearchControllerAsh* controller = provider.GetController();
+
+  EXPECT_TRUE(controller);
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       ControllerIsNonNullWhenBoundAfterConstruction) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  ASSERT_FALSE(factory.IsBound());
+
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+  ASSERT_TRUE(factory.IsBound());
+  crosapi::SearchControllerAsh* controller = provider.GetController();
+
+  EXPECT_TRUE(controller);
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       CallsFactoryOnConstructionIfBound) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+  ASSERT_TRUE(factory.IsBound());
+
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  mojom_factory.RunUntilCreateSearchController();
+
+  EXPECT_TRUE(mojom_factory.TakeLastTestController());
+}
+
+// The three tests below show the UNWANTED behaviour of the provider.
+// The provider SHOULD re-call the factory when it is bound, not when
+// `GetController` is called.
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       CallsFactoryAfterConstructionOnGetController) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  ASSERT_FALSE(factory.IsBound());
+
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+  ASSERT_TRUE(factory.IsBound());
+  (void)provider.GetController();
+  mojom_factory.RunUntilCreateSearchController();
+
+  EXPECT_TRUE(mojom_factory.TakeLastTestController());
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       DoesNotCallFactoryAfterConstructionWhenBound) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  ASSERT_FALSE(factory.IsBound());
+
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+  ASSERT_TRUE(factory.IsBound());
+  // Ensure that the factory is NOT called.
+  // There is no way to have a callback for "function is not called", so we need
+  // to use `RunUntilIdle` here.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_FALSE(mojom_factory.TakeLastTestController());
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest, DoesNotCallFactoryWhenRebound) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  {
+    // Connect a remote factory...
+    TestMojomSearchControllerFactory mojom_factory;
+    factory.BindRemote(mojom_factory.BindToRemote());
+    (void)provider.GetController();
+    mojom_factory.RunUntilCreateSearchController();
+    ASSERT_TRUE(mojom_factory.TakeLastTestController());
+    // ...then disconnect it.
+  }
+  // Ensure that the factory receives the disconnection so it can be rebound.
+  // TODO: b/326147929 - Use a `QuitClosure` for this.
+  base::RunLoop().RunUntilIdle();
+
+  // Rebind another remote factory.
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+  (void)provider.GetController();
+  // Ensure that the factory is NOT called.
+  // There is no way to have a callback for "function is not called", so we need
+  // to use `RunUntilIdle` here.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_FALSE(mojom_factory.TakeLastTestController());
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest, DoesNotCallFactoryMultipleTimes) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  mojom_factory.RunUntilCreateSearchController();
+  ASSERT_TRUE(mojom_factory.TakeLastTestController());
+  (void)provider.GetController();
+  (void)provider.GetController();
+  // Ensure that the factory is NOT called.
+  // There is no way to have a callback for "function is not called", so we need
+  // to use `RunUntilIdle` here.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_FALSE(mojom_factory.TakeLastTestController());
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       ControllerSendsToRemoteControllerFromFactory) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+  PickerLacrosOmniboxSearchProvider provider(&factory, false, false, false);
+  crosapi::SearchControllerAsh* controller = provider.GetController();
+  ASSERT_TRUE(controller);
+  mojom_factory.RunUntilCreateSearchController();
+  std::unique_ptr<TestMojomSearchController> mojom_controller =
+      mojom_factory.TakeLastTestController();
+  ASSERT_TRUE(mojom_controller);
+
+  controller->Search(u"cat", base::DoNothing());
+  mojom_controller->RunUntilSearch();
+
+  EXPECT_EQ(mojom_controller->last_query(), u"cat");
+}
+
+TEST_F(PickerLacrosOmniboxSearchProviderTest,
+       RemoteControllerHasCorrectProviderTypes) {
+  base::test::SingleThreadTaskEnvironment environment;
+  crosapi::SearchControllerFactoryAsh factory;
+  TestMojomSearchControllerFactory mojom_factory;
+  factory.BindRemote(mojom_factory.BindToRemote());
+
+  PickerLacrosOmniboxSearchProvider provider(
+      &factory, /*bookmarks=*/true, /*history=*/false, /*open_tabs=*/true);
+  mojom_factory.RunUntilCreateSearchController();
+
+  std::unique_ptr<TestMojomSearchController> mojom_controller =
+      mojom_factory.TakeLastTestController();
+  ASSERT_TRUE(mojom_controller);
+  EXPECT_TRUE(mojom_controller->bookmarks());
+  EXPECT_FALSE(mojom_controller->history());
+  EXPECT_TRUE(mojom_controller->open_tabs());
+}
+
+}  // namespace
diff --git a/chrome/browser/ui/omnibox/omnibox_theme.h b/chrome/browser/ui/omnibox/omnibox_theme.h
index dd3babf2..8791f8c 100644
--- a/chrome/browser/ui/omnibox/omnibox_theme.h
+++ b/chrome/browser/ui/omnibox/omnibox_theme.h
@@ -7,18 +7,17 @@
 
 #include "chrome/browser/ui/color/chrome_color_id.h"
 
-enum class OmniboxPartState {
-  NORMAL,
-  HOVERED,
-  SELECTED,
-};
+enum class OmniboxPartState { NORMAL, HOVERED, SELECTED, IPH };
 
 constexpr float kOmniboxOpacityHovered = 0.10f;
 constexpr float kOmniboxOpacitySelected = 0.16f;
 
 inline ui::ColorId GetOmniboxBackgroundColorId(OmniboxPartState state) {
+  // TODO(crbug.com/333762301): Update the background color for the IPH
+  // suggestion.
   constexpr ui::ColorId kIds[] = {kColorOmniboxResultsBackground,
                                   kColorOmniboxResultsBackgroundHovered,
+                                  kColorOmniboxResultsBackgroundSelected,
                                   kColorOmniboxResultsBackgroundSelected};
   return kIds[static_cast<size_t>(state)];
 }
diff --git a/chrome/browser/ui/views/omnibox/omnibox_result_view.cc b/chrome/browser/ui/views/omnibox/omnibox_result_view.cc
index 1b51d17..1e439fb7 100644
--- a/chrome/browser/ui/views/omnibox/omnibox_result_view.cc
+++ b/chrome/browser/ui/views/omnibox/omnibox_result_view.cc
@@ -318,6 +318,13 @@
   if (part_state == OmniboxPartState::NORMAL && !prefers_contrast)
     return nullptr;
 
+  if (OmniboxFieldTrial::IsStarterPackIPHEnabled() &&
+      part_state == OmniboxPartState::IPH) {
+    return views::CreateThemedRoundedRectBackground(
+        GetOmniboxBackgroundColorId(part_state), /*radius=*/8,
+        /*for_border_thickness=*/0);
+  }
+
   if (OmniboxFieldTrial::IsChromeRefreshSuggestHoverFillShapeEnabled()) {
     gfx::RoundedCornersF radii = {0, static_cast<float>(view->height()),
                                   static_cast<float>(view->height()), 0};
@@ -482,8 +489,18 @@
   // NULL_RESULT_MESSAGE matches are no-op suggestions that only deliver a
   // message. The selected and hovered states imply an action can be taken from
   // that suggestion, so do not allow those states for this result.
-  if (match_.type == AutocompleteMatchType::NULL_RESULT_MESSAGE)
-    return OmniboxPartState::NORMAL;
+  //
+  // IPH messages originate from the Featured Search Provider and show a
+  // different theme state (colored background).
+  // TODO(crbug.com/333762301): Probably makes sense to find a more sustainable
+  // way to differentiate IPH from the "No Results Found" suggestion. Maybe a
+  // different autocomplete match type.
+  if (match_.type == AutocompleteMatchType::NULL_RESULT_MESSAGE) {
+    bool is_iph = OmniboxFieldTrial::IsStarterPackIPHEnabled() &&
+                  match_.provider->type() ==
+                      AutocompleteProvider::Type::TYPE_FEATURED_SEARCH;
+    return is_iph ? OmniboxPartState::IPH : OmniboxPartState::NORMAL;
+  }
 
   if (GetMatchSelected())
     return OmniboxPartState::SELECTED;
diff --git a/chrome/browser/ui/views/omnibox/omnibox_row_view.cc b/chrome/browser/ui/views/omnibox/omnibox_row_view.cc
index c77357a..84a3fea 100644
--- a/chrome/browser/ui/views/omnibox/omnibox_row_view.cc
+++ b/chrome/browser/ui/views/omnibox/omnibox_row_view.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/ui/views/omnibox/omnibox_row_view.h"
 
 #include "chrome/browser/ui/color/chrome_color_id.h"
+#include "chrome/browser/ui/omnibox/omnibox_theme.h"
 #include "chrome/browser/ui/views/omnibox/omnibox_header_view.h"
 #include "chrome/browser/ui/views/omnibox/omnibox_popup_view_views.h"
 #include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
@@ -116,6 +117,10 @@
       !OmniboxFieldTrial::IsChromeRefreshSuggestIconsEnabled()) {
     return gfx::Insets::TLBR(4, 0, 0, right_inset);
   }
+  if (OmniboxFieldTrial::IsStarterPackIPHEnabled() &&
+      result_view_->GetThemeState() == OmniboxPartState::IPH) {
+    return gfx::Insets::TLBR(8, 8, 8, 16);
+  }
 
   return gfx::Insets::TLBR(0, 0, 0, right_inset);
 }
diff --git a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
index ae89b76..b0e1af6 100644
--- a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
@@ -1832,7 +1832,8 @@
       {"chromeCertificatesDescription",
        IDS_SETTINGS_CHROME_CERTIFICATES_DESCRIPTION},
 #endif
-  };
+      {"safeBrowsingEnhancedLearnMoreLabel",
+       IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL}};
   html_source->AddLocalizedStrings(kLocalizedStrings);
 
   html_source->AddString("cookiesSettingsHelpCenterURL",
@@ -1857,12 +1858,6 @@
           ? chrome::kSafeBrowsingHelpCenterUpdatedURL
           : chrome::kSafeBrowsingHelpCenterURL);
 
-  html_source->AddString(
-      "enhancedProtectionLearnMoreLabel",
-      l10n_util::GetStringFUTF16(
-          IDS_SETTINGS_SAFEBROWSING_ENHANCED_LEARN_MORE_LABEL,
-          chrome::kSafeBrowsingInChromeHelpCenterURL));
-
   html_source->AddString("syncAndGoogleServicesLearnMoreURL",
                          chrome::kSyncAndGoogleServicesLearnMoreURL);
 
@@ -1879,6 +1874,8 @@
   html_source->AddBoolean("driveSuggestNoSyncRequirement",
                           base::FeatureList::IsEnabled(
                               omnibox::kDocumentProviderNoSyncRequirement));
+  html_source->AddString("enhancedProtectionHelpCenterURL",
+                         chrome::kSafeBrowsingInChromeHelpCenterURL);
 
   bool show_secure_dns = IsSecureDnsAvailable();
   bool link_secure_dns = ShouldLinkSecureDnsOsSettings();
diff --git a/chrome/build/android-arm32.pgo.txt b/chrome/build/android-arm32.pgo.txt
index 41e8864d..64cb2a65 100644
--- a/chrome/build/android-arm32.pgo.txt
+++ b/chrome/build/android-arm32.pgo.txt
@@ -1 +1 @@
-chrome-android32-main-1712944793-6c2b3c82ec4dda250ae5820cf42a6c07a16bef25-bf0f3b907a9a0e3acc1e4d0ef924d586e4a2f9c5.profdata
+chrome-android32-main-1712987449-7dd5b46742c0695a911e8e4512443f1f540ca842-6f3514f173e1d1e27a5fef4995511f7f41516460.profdata
diff --git a/chrome/build/android-arm64.pgo.txt b/chrome/build/android-arm64.pgo.txt
index b201ffa..630fd040 100644
--- a/chrome/build/android-arm64.pgo.txt
+++ b/chrome/build/android-arm64.pgo.txt
@@ -1 +1 @@
-chrome-android64-main-1712944793-32d6b78f973749f8ef53f71e0e6b7653cb6a73a9-bf0f3b907a9a0e3acc1e4d0ef924d586e4a2f9c5.profdata
+chrome-android64-main-1712972859-5d48efe501c3566fba5dd99b73d8d5f0e21ab716-d93ead5578559e1260a4457b13176e1b2c632f88.profdata
diff --git a/chrome/build/lacros64.pgo.txt b/chrome/build/lacros64.pgo.txt
index 0c3bf1f..c761a90caa 100644
--- a/chrome/build/lacros64.pgo.txt
+++ b/chrome/build/lacros64.pgo.txt
@@ -1 +1 @@
-chrome-chromeos-amd64-generic-main-1712923045-5b33955d2f6555f4386bb10e322b3d9658eda86c-78fcd7bd9bbf8d0b9b0f5bd7c7348461ea0362fe.profdata
+chrome-chromeos-amd64-generic-main-1712966818-62e4d31fe423af8c1e7aba25312838795d06b02f-e081eabaf23f8bc38c820e8bf452b35ee71e0c51.profdata
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 54edf91..d440de2 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1712944793-2483263ad14e801b606f823fa79298cf632f9f78-bf0f3b907a9a0e3acc1e4d0ef924d586e4a2f9c5.profdata
+chrome-linux-main-1712987449-4ce950049b56e22fced0548fa25f8bb726ce273b-6f3514f173e1d1e27a5fef4995511f7f41516460.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 77b22a5..e947ef57 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1712959143-b058c2d51303c73da8b4e47020bfa3699b92bb82-941c34d78fc4a31fb6a5d87aa661115dc9ea640c.profdata
+chrome-mac-arm-main-1713001580-586b194c248fe14c5880691a09d53aa2bcc40f27-87853405a6894e2d3eca1e678a07e60d3becfdcb.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 10452f0..343278a 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1712944793-e0a9b989e7df4252f86291566b45c6cb090d0f5e-bf0f3b907a9a0e3acc1e4d0ef924d586e4a2f9c5.profdata
+chrome-mac-main-1712966302-c77b4f8ebb0229604d6a3498094f297b82b6a3b0-df16b1e5e37e908a9e59e72f15f7a5ae2c12ac71.profdata
diff --git a/chrome/build/win-arm64.pgo.txt b/chrome/build/win-arm64.pgo.txt
index 806a8a3..a78faa6 100644
--- a/chrome/build/win-arm64.pgo.txt
+++ b/chrome/build/win-arm64.pgo.txt
@@ -1 +1 @@
-chrome-win-arm64-main-1712944793-2f7fbc6d118513c0a169e7b4ed549939068ce790-bf0f3b907a9a0e3acc1e4d0ef924d586e4a2f9c5.profdata
+chrome-win-arm64-main-1712987449-75ba781fabbf1f127475d9b5f171cbd91146ad72-6f3514f173e1d1e27a5fef4995511f7f41516460.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index ba0cc469..27087979 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1712944793-be04f890082e93ea0a1bc0a74b914213b2c947d6-bf0f3b907a9a0e3acc1e4d0ef924d586e4a2f9c5.profdata
+chrome-win32-main-1712987449-8188c22e8e406da9ddf98a1f67b118b95bc5420e-6f3514f173e1d1e27a5fef4995511f7f41516460.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 2c71afa..5989e33 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1712944793-410e7e14f7a3cccaa5c0fb3a1c6510528eb59950-bf0f3b907a9a0e3acc1e4d0ef924d586e4a2f9c5.profdata
+chrome-win64-main-1712987449-ac9bddae1b4ca71a0d7253d4a7d7f47411119f53-6f3514f173e1d1e27a5fef4995511f7f41516460.profdata
diff --git a/chrome/common/chromeos/extensions/api/_permission_features.json b/chrome/common/chromeos/extensions/api/_permission_features.json
index bdd1a21..3f4821a 100644
--- a/chrome/common/chromeos/extensions/api/_permission_features.json
+++ b/chrome/common/chromeos/extensions/api/_permission_features.json
@@ -29,6 +29,13 @@
     ],
     "dependencies": [ "manifest:chromeos_system_extension" ]
   },
+  "os.diagnostics.network_info_mlab": {
+    "channel": "stable",
+    "extension_types": [
+      "chromeos_system_extension"
+    ],
+    "dependencies": [ "manifest:chromeos_system_extension" ]
+  },
   "os.events": {
     "channel": "stable",
     "extension_types": [
diff --git a/chrome/common/chromeos/extensions/chromeos_system_extensions_api_permissions.cc b/chrome/common/chromeos/extensions/chromeos_system_extensions_api_permissions.cc
index cf16517..eaede715e 100644
--- a/chrome/common/chromeos/extensions/chromeos_system_extensions_api_permissions.cc
+++ b/chrome/common/chromeos/extensions/chromeos_system_extensions_api_permissions.cc
@@ -26,6 +26,8 @@
     {APIPermissionID::kChromeOSBluetoothPeripheralsInfo,
      "os.bluetooth_peripherals_info"},
     {APIPermissionID::kChromeOSDiagnostics, "os.diagnostics"},
+    {APIPermissionID::kChromeOSDiagnosticsNetworkInfoForMlab,
+     "os.diagnostics.network_info_mlab"},
     {APIPermissionID::kChromeOSEvents, "os.events"},
     {APIPermissionID::kChromeOSManagementAudio, "os.management.audio"},
     {APIPermissionID::kChromeOSTelemetry, "os.telemetry"},
diff --git a/chrome/common/extensions/permissions/chrome_permission_message_rules.cc b/chrome/common/extensions/permissions/chrome_permission_message_rules.cc
index ede23107..7b47aa1 100644
--- a/chrome/common/extensions/permissions/chrome_permission_message_rules.cc
+++ b/chrome/common/extensions/permissions/chrome_permission_message_rules.cc
@@ -752,6 +752,9 @@
       {IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS,
        {APIPermissionID::kChromeOSDiagnostics},
        {}},
+      {IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_DIAGNOSTICS_NETWORK_INFO_FOR_MLAB,
+       {APIPermissionID::kChromeOSDiagnosticsNetworkInfoForMlab},
+       {}},
       {IDS_EXTENSION_PROMPT_WARNING_CHROMEOS_EVENTS,
        {APIPermissionID::kChromeOSEvents},
        {}},
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index e8b749e..6478728 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -6163,6 +6163,7 @@
     "../browser/data_sharing/data_sharing_service_factory_unittest.cc",
     "../browser/download/chrome_download_manager_delegate_unittest.cc",
     "../browser/download/deferred_client_wrapper_unittest.cc",
+    "../browser/download/download_core_service_impl_unittest.cc",
     "../browser/download/download_history_unittest.cc",
     "../browser/download/download_item_model_unittest.cc",
     "../browser/download/download_item_warning_data_unittest.cc",
@@ -8579,6 +8580,7 @@
       "../browser/ui/ash/network/network_state_notifier_unittest.cc",
       "../browser/ui/ash/network/tether_notification_presenter_unittest.cc",
       "../browser/ui/ash/picker/picker_client_impl_unittest.cc",
+      "../browser/ui/ash/picker/picker_lacros_omnibox_search_provider_unittest.cc",
       "../browser/ui/ash/projector/projector_client_impl_unittest.cc",
       "../browser/ui/ash/projector/projector_soda_installation_controller_unittest.cc",
       "../browser/ui/ash/projector/projector_utils_unittest.cc",
diff --git a/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_asus.json b/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_asus.json
index ee289cb6..12cb2cd 100644
--- a/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_asus.json
+++ b/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_asus.json
@@ -13,6 +13,7 @@
   ],
   "optional_permissions": [
     "os.bluetooth_peripherals_info",
+    "os.diagnostics.network_info_mlab",
     "os.management.audio"
   ],
   "chromeos_system_extension": {},
diff --git a/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_google.json b/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_google.json
index 6f2e872..62c728ad 100644
--- a/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_google.json
+++ b/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_google.json
@@ -16,6 +16,7 @@
       "os.telemetry.serial_number",
       "os.telemetry.network_info",
       "os.bluetooth_peripherals_info",
+      "os.diagnostics.network_info_mlab",
       "os.management.audio"
     ],
     "chromeos_system_extension": {},
diff --git a/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_hp.json b/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_hp.json
index 297e796..886a6c3 100644
--- a/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_hp.json
+++ b/chrome/test/data/extensions/manifest_tests/chromeos_system_extension_hp.json
@@ -13,6 +13,7 @@
     ],
     "optional_permissions": [
       "os.bluetooth_peripherals_info",
+      "os.diagnostics.network_info_mlab",
       "os.management.audio"
     ],
     "chromeos_system_extension": {},
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/BUILD.gn b/chrome/test/data/webui/chromeos/shimless_rma/BUILD.gn
index d4bf5ce..95c5041 100644
--- a/chrome/test/data/webui/chromeos/shimless_rma/BUILD.gn
+++ b/chrome/test/data/webui/chromeos/shimless_rma/BUILD.gn
@@ -30,8 +30,8 @@
     "onboarding_wait_for_manual_wp_disable_page_test.ts",
     "onboarding_wp_disable_complete_page_test.ts",
     "reboot_page_test.ts",
-    "reimaging_calibration_failed_page_test.js",
-    "reimaging_calibration_run_page_test.js",
+    "reimaging_calibration_failed_page_test.ts",
+    "reimaging_calibration_run_page_test.ts",
     "reimaging_calibration_setup_page_test.js",
     "reimaging_device_information_page_test.js",
     "reimaging_firmware_update_page_test.js",
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/fake_shimless_rma_service_test.ts b/chrome/test/data/webui/chromeos/shimless_rma/fake_shimless_rma_service_test.ts
index b5f70087..65953f5 100644
--- a/chrome/test/data/webui/chromeos/shimless_rma/fake_shimless_rma_service_test.ts
+++ b/chrome/test/data/webui/chromeos/shimless_rma/fake_shimless_rma_service_test.ts
@@ -787,7 +787,7 @@
     assert(service);
     service.setStates(states);
 
-    const result = await service.startCalibration();
+    const result = await service.startCalibration(/* components= */[]);
     assertEquals(State.kChooseDestination, result.stateResult.state);
     assertEquals(RmadErrorCode.kOk, result.stateResult.error);
   });
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_failed_page_test.js b/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_failed_page_test.js
deleted file mode 100644
index eb1cba6..0000000
--- a/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_failed_page_test.js
+++ /dev/null
@@ -1,322 +0,0 @@
-// Copyright 2021 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import {PromiseResolver} from 'chrome://resources/ash/common/promise_resolver.js';
-import {getDeepActiveElement} from 'chrome://resources/ash/common/util.js';
-import {fakeCalibrationComponentsWithFails, fakeCalibrationComponentsWithoutFails} from 'chrome://shimless-rma/fake_data.js';
-import {FakeShimlessRmaService} from 'chrome://shimless-rma/fake_shimless_rma_service.js';
-import {setShimlessRmaServiceForTesting} from 'chrome://shimless-rma/mojo_interface_provider.js';
-import {ReimagingCalibrationFailedPage} from 'chrome://shimless-rma/reimaging_calibration_failed_page.js';
-import {ShimlessRma} from 'chrome://shimless-rma/shimless_rma.js';
-import {CalibrationComponentStatus, CalibrationStatus, ComponentType} from 'chrome://shimless-rma/shimless_rma.mojom-webui.js';
-import {assertDeepEquals, assertEquals, assertFalse, assertNotEquals, assertNotReached, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
-import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
-
-// TODO(crbug/1296829): Add a non-flaky test for keyboard navigation.
-suite('reimagingCalibrationFailedPageTest', function() {
-  /**
-   * ShimlessRma is needed to handle the 'transition-state' event used
-   * when handling calibration overall progress signals.
-   * @type {?ShimlessRma}
-   */
-  let shimlessRmaComponent = null;
-
-  /** @type {?ReimagingCalibrationFailedPage} */
-  let component = null;
-
-  /** @type {?FakeShimlessRmaService} */
-  let service = null;
-
-  setup(() => {
-    document.body.innerHTML = trustedTypes.emptyHTML;
-    service = new FakeShimlessRmaService();
-    setShimlessRmaServiceForTesting(service);
-  });
-
-  teardown(() => {
-    component.remove();
-    component = null;
-    shimlessRmaComponent.remove();
-    shimlessRmaComponent = null;
-    service.reset();
-  });
-
-  /**
-   * @return {!Promise}
-   * @param {!Array<!CalibrationComponentStatus>} calibrationComponents
-   */
-  function initializeCalibrationPage(calibrationComponents) {
-    assertFalse(!!component);
-
-    shimlessRmaComponent =
-        /** @type {!ShimlessRma} */ (document.createElement('shimless-rma'));
-    assertTrue(!!shimlessRmaComponent);
-    document.body.appendChild(shimlessRmaComponent);
-
-    // Initialize the fake data.
-    service.setGetCalibrationComponentListResult(calibrationComponents);
-
-    component = /** @type {!ReimagingCalibrationFailedPage} */ (
-        document.createElement('reimaging-calibration-failed-page'));
-    assertTrue(!!component);
-    document.body.appendChild(component);
-
-    return flushTasks();
-  }
-
-  /**
-   * @return {!Promise}
-   */
-  function clickComponentCameraToggle() {
-    const cameraComponent =
-        component.shadowRoot.querySelector('#componentCamera');
-    cameraComponent.click();
-    return flushTasks();
-  }
-
-  /**
-   * Get getComponentsList private member for testing.
-   * @suppress {visibility} // access private member
-   * @return {!Array<!CalibrationComponentStatus>}
-   */
-  function getComponentsList() {
-    return component.getComponentsList();
-  }
-
-
-  test('Initializes', async () => {
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-
-    const cameraComponent =
-        component.shadowRoot.querySelector('#componentCamera');
-    const batteryComponent =
-        component.shadowRoot.querySelector('#componentBattery');
-    const baseAccelerometerComponent =
-        component.shadowRoot.querySelector('#componentBaseAccelerometer');
-    const lidAccelerometerComponent =
-        component.shadowRoot.querySelector('#componentLidAccelerometer');
-    const touchpadComponent =
-        component.shadowRoot.querySelector('#componentTouchpad');
-    assertEquals('Camera', cameraComponent.componentName);
-    assertFalse(cameraComponent.checked);
-    assertTrue(cameraComponent.failed);
-    assertFalse(cameraComponent.disabled);
-    assertEquals('Battery', batteryComponent.componentName);
-    assertFalse(batteryComponent.checked);
-    assertFalse(batteryComponent.failed);
-    assertTrue(batteryComponent.disabled);
-    assertEquals(
-        'Base Accelerometer', baseAccelerometerComponent.componentName);
-    assertFalse(baseAccelerometerComponent.checked);
-    assertFalse(baseAccelerometerComponent.failed);
-    assertTrue(baseAccelerometerComponent.disabled);
-    assertEquals('Lid Accelerometer', lidAccelerometerComponent.componentName);
-    assertFalse(lidAccelerometerComponent.checked);
-    assertFalse(lidAccelerometerComponent.failed);
-    assertTrue(lidAccelerometerComponent.disabled);
-    assertEquals('Touchpad', touchpadComponent.componentName);
-    assertFalse(touchpadComponent.checked);
-    assertFalse(touchpadComponent.failed);
-    assertTrue(touchpadComponent.disabled);
-  });
-
-  test('ToggleComponent', async () => {
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-    const componentList = getComponentsList();
-    assertEquals(
-        3,
-        componentList
-            .filter(
-                component =>
-                    component.status === CalibrationStatus.kCalibrationSkip)
-            .length);
-    assertEquals(
-        4,
-        componentList
-            .filter(
-                component =>
-                    component.status === CalibrationStatus.kCalibrationComplete)
-            .length);
-
-    // Click the camera button to check it.
-    await clickComponentCameraToggle();
-    // Camera should be the first entry in the list.
-    assertEquals(
-        CalibrationStatus.kCalibrationWaiting, getComponentsList()[0].status);
-
-    // Click the camera button to uncheck it.
-    await clickComponentCameraToggle();
-    // Camera should be the first entry in the list.
-    assertEquals(
-        CalibrationStatus.kCalibrationSkip, getComponentsList()[0].status);
-  });
-
-  test('ExitButtonTriggersCalibrationComplete', async () => {
-    const resolver = new PromiseResolver();
-    await initializeCalibrationPage(fakeCalibrationComponentsWithoutFails);
-    let startCalibrationCalls = 0;
-    service.startCalibration = (components) => {
-      assertEquals(5, components.length);
-      components.forEach(
-          component => assertEquals(
-              CalibrationStatus.kCalibrationComplete, component.status));
-      startCalibrationCalls++;
-      return resolver.promise;
-    };
-    await flushTasks();
-
-    const expectedResult = {foo: 'bar'};
-    let savedResult;
-    component.onExitButtonClick().then((result) => savedResult = result);
-    // Resolve to a distinct result to confirm it was not modified.
-    resolver.resolve(expectedResult);
-    await flushTasks();
-
-    assertEquals(1, startCalibrationCalls);
-    assertDeepEquals(savedResult, expectedResult);
-  });
-
-  test('NextButtonTriggersCalibration', async () => {
-    const resolver = new PromiseResolver();
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-
-    await clickComponentCameraToggle();
-
-    let startCalibrationCalls = 0;
-    service.startCalibration = (components) => {
-      assertEquals(7, components.length);
-      components.forEach(component => {
-        let expectedStatus;
-        if (component.component === ComponentType.kCamera) {
-          expectedStatus = CalibrationStatus.kCalibrationWaiting;
-        } else if (
-            component.component === ComponentType.kScreen ||
-            component.component === ComponentType.kBaseGyroscope) {
-          expectedStatus = CalibrationStatus.kCalibrationSkip;
-        } else {
-          expectedStatus = CalibrationStatus.kCalibrationComplete;
-        }
-        assertEquals(expectedStatus, component.status);
-      });
-      startCalibrationCalls++;
-      return resolver.promise;
-    };
-
-    const expectedResult = {foo: 'bar'};
-    let savedResult;
-    component.onNextButtonClick().then((result) => savedResult = result);
-    // Resolve to a distinct result to confirm it was not modified.
-    resolver.resolve(expectedResult);
-    await flushTasks();
-
-    assertEquals(1, startCalibrationCalls);
-    assertDeepEquals(savedResult, expectedResult);
-  });
-
-  test('ComponentChipAllButtonsDisabled', async () => {
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-
-    // Base Gyroscope is a failed component so it starts off not disabled.
-    const baseGyroscopeComponent =
-        component.shadowRoot.querySelector('#componentBaseGyroscope');
-    assertFalse(baseGyroscopeComponent.disabled);
-    component.allButtonsDisabled = true;
-    assertTrue(baseGyroscopeComponent.disabled);
-  });
-
-  test('SkipCalibrationWithFailedComponents', async () => {
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-
-    let wasPromiseRejected = false;
-    component.onExitButtonClick()
-        .then(() => assertNotReached('Do not proceed with failed components'))
-        .catch(() => {
-          wasPromiseRejected = true;
-        });
-
-    await flushTasks();
-    assertTrue(wasPromiseRejected);
-  });
-
-  test('FailedComponentsDialogSkipButton', async () => {
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-
-    const resolver = new PromiseResolver();
-    let startCalibrationCalls = 0;
-    service.startCalibration = (components) => {
-      startCalibrationCalls++;
-      return resolver.promise;
-    };
-
-    component.onExitButtonClick().catch(() => {});
-
-    await flushTasks();
-    assertEquals(0, startCalibrationCalls);
-    assertTrue(
-        component.shadowRoot.querySelector('#failedComponentsDialog').open);
-    component.shadowRoot.querySelector('#dialogSkipButton').click();
-
-    await flushTasks();
-    assertEquals(1, startCalibrationCalls);
-    assertFalse(
-        component.shadowRoot.querySelector('#failedComponentsDialog').open);
-  });
-
-  test('FailedComponentsDialogRetryButton', async () => {
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-
-    const resolver = new PromiseResolver();
-    let startCalibrationCalls = 0;
-    service.startCalibration = (components) => {
-      startCalibrationCalls++;
-      return resolver.promise;
-    };
-
-    component.onExitButtonClick().catch(() => {});
-
-    await flushTasks();
-    assertEquals(0, startCalibrationCalls);
-    assertTrue(
-        component.shadowRoot.querySelector('#failedComponentsDialog').open);
-    component.shadowRoot.querySelector('#dialogRetryButton').click();
-
-    await flushTasks();
-    assertEquals(0, startCalibrationCalls);
-    assertFalse(
-        component.shadowRoot.querySelector('#failedComponentsDialog').open);
-  });
-
-  test('NextButtonIsOnlyEnabledIfAtLeastOneComponentIsSelected', async () => {
-    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
-
-    let disableNextButtonEventFired = false;
-    let disableNextButton = false;
-
-    const componentBaseGyroscopeButton =
-        component.shadowRoot.querySelector('#componentBaseGyroscope')
-            .shadowRoot.querySelector('#componentButton');
-
-    const disableHandler = (event) => {
-      disableNextButtonEventFired = true;
-      disableNextButton = event.detail;
-    };
-
-    component.addEventListener('disable-next-button', disableHandler);
-
-    // If a component is selected, enable the next button.
-    componentBaseGyroscopeButton.click();
-    await flushTasks();
-    assertTrue(disableNextButtonEventFired);
-    assertFalse(disableNextButton);
-
-    // If no components are selected, disable the next button.
-    disableNextButtonEventFired = false;
-    componentBaseGyroscopeButton.click();
-    await flushTasks();
-    assertTrue(disableNextButtonEventFired);
-    assertTrue(disableNextButton);
-
-    component.removeEventListener('disable-next-button', disableHandler);
-  });
-});
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_failed_page_test.ts b/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_failed_page_test.ts
new file mode 100644
index 0000000..6d7c55b
--- /dev/null
+++ b/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_failed_page_test.ts
@@ -0,0 +1,295 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'chrome://shimless-rma/shimless_rma.js';
+
+import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
+import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
+import {PromiseResolver} from 'chrome://resources/ash/common/promise_resolver.js';
+import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
+import {assert} from 'chrome://resources/js/assert.js';
+import {CalibrationComponentChipElement} from 'chrome://shimless-rma/calibration_component_chip.js';
+import {DISABLE_NEXT_BUTTON} from 'chrome://shimless-rma/events.js';
+import {fakeCalibrationComponentsWithFails, fakeCalibrationComponentsWithoutFails} from 'chrome://shimless-rma/fake_data.js';
+import {FakeShimlessRmaService} from 'chrome://shimless-rma/fake_shimless_rma_service.js';
+import {setShimlessRmaServiceForTesting} from 'chrome://shimless-rma/mojo_interface_provider.js';
+import {ReimagingCalibrationFailedPage} from 'chrome://shimless-rma/reimaging_calibration_failed_page.js';
+import {CalibrationComponentStatus, CalibrationStatus, ComponentType, StateResult} from 'chrome://shimless-rma/shimless_rma.mojom-webui.js';
+import {assertEquals, assertFalse, assertNotReached, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
+import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
+import {eventToPromise} from 'chrome://webui-test/test_util.js';
+
+// TODO(crbug/1296829): Add a non-flaky test for keyboard navigation.
+suite('reimagingCalibrationFailedPageTest', function() {
+  let component: ReimagingCalibrationFailedPage|null = null;
+
+  const service: FakeShimlessRmaService = new FakeShimlessRmaService();
+
+  const cameraSelector = '#componentCamera';
+  const baseGyroscopeSelector = '#componentBaseGyroscope';
+  const failedComponentsDialogSelector = '#failedComponentsDialog';
+
+  setup(() => {
+    document.body.innerHTML = window.trustedTypes!.emptyHTML;
+    setShimlessRmaServiceForTesting(service);
+  });
+
+  teardown(() => {
+    component?.remove();
+    component = null;
+  });
+
+  function initializeCalibrationPage(
+      calibrationComponents: CalibrationComponentStatus[]): Promise<void> {
+    // Initialize the fake data.
+    assert(service);
+    service.setGetCalibrationComponentListResult(calibrationComponents);
+
+    assert(!component);
+    component = document.createElement(ReimagingCalibrationFailedPage.is);
+    assert(component);
+    document.body.appendChild(component);
+
+    return flushTasks();
+  }
+
+  function clickComponentCameraToggle(): Promise<void> {
+    assert(component);
+    strictQuery(
+        cameraSelector, component.shadowRoot, CalibrationComponentChipElement)
+        .click();
+    return flushTasks();
+  }
+
+  function getComponentsList(): CalibrationComponentStatus[] {
+    assert(component);
+    return component.getComponentsListForTesting();
+  }
+
+  // Verify the page initializes with component chips in the expected state.
+  test('Initializes', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
+
+    assert(component);
+    const cameraComponent = strictQuery(
+        cameraSelector, component.shadowRoot, CalibrationComponentChipElement);
+    assertEquals('Camera', cameraComponent.componentName);
+    assertFalse(cameraComponent.checked);
+    assertTrue(cameraComponent.failed);
+    assertFalse(cameraComponent.disabled);
+
+    const batteryComponent = strictQuery(
+        '#componentBattery', component.shadowRoot,
+        CalibrationComponentChipElement);
+    assertEquals('Battery', batteryComponent.componentName);
+    assertFalse(batteryComponent.checked);
+    assertFalse(batteryComponent.failed);
+    assertTrue(batteryComponent.disabled);
+
+    const baseAccelerometerComponent = strictQuery(
+        '#componentBaseAccelerometer', component.shadowRoot,
+        CalibrationComponentChipElement);
+    assertEquals(
+        'Base Accelerometer', baseAccelerometerComponent.componentName);
+    assertFalse(baseAccelerometerComponent.checked);
+    assertFalse(baseAccelerometerComponent.failed);
+    assertTrue(baseAccelerometerComponent.disabled);
+
+    const lidAccelerometerComponent = strictQuery(
+        '#componentLidAccelerometer', component.shadowRoot,
+        CalibrationComponentChipElement);
+    assertEquals('Lid Accelerometer', lidAccelerometerComponent.componentName);
+    assertFalse(lidAccelerometerComponent.checked);
+    assertFalse(lidAccelerometerComponent.failed);
+    assertTrue(lidAccelerometerComponent.disabled);
+
+    const touchpadComponent = strictQuery(
+        '#componentTouchpad', component.shadowRoot,
+        CalibrationComponentChipElement);
+    assertEquals('Touchpad', touchpadComponent.componentName);
+    assertFalse(touchpadComponent.checked);
+    assertFalse(touchpadComponent.failed);
+    assertTrue(touchpadComponent.disabled);
+  });
+
+  // Verify clicking the Camera chip toggles it between states.
+  test('ToggleComponent', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
+
+    // Click the camera button to check it.
+    await clickComponentCameraToggle();
+    let cameraComponent = getComponentsList().find(
+        (calibrationComponent: CalibrationComponentStatus) =>
+            calibrationComponent.component === ComponentType.kCamera);
+    assert(cameraComponent);
+    assertEquals(CalibrationStatus.kCalibrationWaiting, cameraComponent.status);
+
+    // Click the camera button again to uncheck it.
+    await clickComponentCameraToggle();
+    cameraComponent = getComponentsList().find(
+        (calibrationComponent: CalibrationComponentStatus) =>
+            calibrationComponent.component === ComponentType.kCamera);
+    assert(cameraComponent);
+    assertEquals(CalibrationStatus.kCalibrationSkip, cameraComponent.status);
+  });
+
+  // Verify clicking the exit button triggers the calibration complete signal.
+  test('ExitButtonTriggersCalibrationComplete', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithoutFails);
+
+    const resolver = new PromiseResolver<{stateResult: StateResult}>();
+    let startCalibrationCalls = 0;
+    assert(service);
+    service.startCalibration = (components: CalibrationComponentStatus[]) => {
+      const expectedComponents = 5;
+      assertEquals(expectedComponents, components.length);
+      components.forEach(
+          (component: CalibrationComponentStatus) => assertEquals(
+              CalibrationStatus.kCalibrationComplete, component.status));
+      ++startCalibrationCalls;
+      return resolver.promise;
+    };
+
+    assert(component);
+    component.onExitButtonClick();
+
+    const expectedCalls = 1;
+    assertEquals(expectedCalls, startCalibrationCalls);
+  });
+
+  // Verify clicking the next button triggers a new calibration.
+  test('NextButtonTriggersCalibration', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
+
+    await clickComponentCameraToggle();
+
+    const resolver = new PromiseResolver<{stateResult: StateResult}>();
+    let startCalibrationCalls = 0;
+    assert(service);
+    service.startCalibration = (components: CalibrationComponentStatus[]) => {
+      const expectedCompnents = 7;
+      assertEquals(expectedCompnents, components.length);
+
+      components.forEach((component: CalibrationComponentStatus) => {
+        let expectedStatus;
+        if (component.component === ComponentType.kCamera) {
+          expectedStatus = CalibrationStatus.kCalibrationWaiting;
+        } else if (
+            component.component === ComponentType.kScreen ||
+            component.component === ComponentType.kBaseGyroscope) {
+          expectedStatus = CalibrationStatus.kCalibrationSkip;
+        } else {
+          expectedStatus = CalibrationStatus.kCalibrationComplete;
+        }
+        assertEquals(expectedStatus, component.status);
+      });
+      ++startCalibrationCalls;
+      return resolver.promise;
+    };
+
+    assert(component);
+    component.onNextButtonClick();
+
+    const expectedCalls = 1;
+    assertEquals(expectedCalls, startCalibrationCalls);
+  });
+
+  // Verify when `allButtonsDisabled` is set all component chips are disabled.
+  test('ComponentChipAllButtonsDisabled', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
+
+    // Base Gyroscope is a failed component so it starts off not disabled.
+    assert(component);
+    const baseGyroscopeComponent = strictQuery(
+        baseGyroscopeSelector, component.shadowRoot,
+        CalibrationComponentChipElement);
+    assertFalse(baseGyroscopeComponent.disabled);
+    component.allButtonsDisabled = true;
+    assertTrue(baseGyroscopeComponent.disabled);
+  });
+
+  // Verify attempting to skip a calibration with failed components is rejected
+  // and opens a dialog.
+  test('SkipCalibrationWithFailedComponents', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
+
+    // Click the skip/exit button and expect the request to be rejected and open
+    // the confirmation dialog.
+    assert(component);
+    let wasPromiseRejected = false;
+    try {
+      await component.onExitButtonClick();
+      assertNotReached('Do not proceed with failed components');
+    } catch (error: unknown) {
+      wasPromiseRejected = true;
+    }
+    assertTrue(wasPromiseRejected);
+    const failedComponentsDialog = strictQuery(
+        failedComponentsDialogSelector, component.shadowRoot, CrDialogElement);
+    assertTrue(failedComponentsDialog.open);
+
+    // Click the skip button and expect the dialog to close.
+    strictQuery('#dialogSkipButton', component.shadowRoot, CrButtonElement)
+        .click();
+    await flushTasks();
+    assertFalse(failedComponentsDialog.open);
+  });
+
+  // Verify clicking the dialog retry button restarts calibration.
+  test('FailedComponentsDialogRetryButton', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
+
+    const resolver = new PromiseResolver<{stateResult: StateResult}>();
+    let startCalibrationCalls = 0;
+    assert(service);
+    service.startCalibration = (/* components= */[]) => {
+      ++startCalibrationCalls;
+      return resolver.promise;
+    };
+
+    // Click the skip/exit button and expect the request to be rejected and open
+    // the confirmation dialog.
+    assert(component);
+    try {
+      await component.onExitButtonClick();
+      assertNotReached('Do not proceed with failed components');
+    } catch (error: unknown) {
+    }
+
+    // Click the retry button and expect the dialog to close without starting a
+    // calibration.
+    strictQuery('#dialogRetryButton', component.shadowRoot, CrButtonElement)
+        .click();
+    assertEquals(0, startCalibrationCalls);
+    assertFalse(strictQuery(
+                    failedComponentsDialogSelector, component.shadowRoot,
+                    CrDialogElement)
+                    .open);
+  });
+
+  // Verify that the next button is only enabled if at least one component is
+  // selected.
+  test('NextButtonOnlyEnabledIfComponentIsSelected', async () => {
+    await initializeCalibrationPage(fakeCalibrationComponentsWithFails);
+
+    assert(component);
+    const disableNextButtonEvent =
+        eventToPromise(DISABLE_NEXT_BUTTON, component);
+
+    // Select a component and expect the next button to be enabled.
+    assert(component);
+    const componentBaseGyroscopeButton = strictQuery(
+        baseGyroscopeSelector, component.shadowRoot,
+        CalibrationComponentChipElement);
+    componentBaseGyroscopeButton.click();
+    const enableNextButtonResponse = await disableNextButtonEvent;
+    assertFalse(enableNextButtonResponse.detail);
+
+    // Select the component again to uncheck it so no components are selected
+    // and expect the next button to be disabled.
+    componentBaseGyroscopeButton.click();
+    const disableNextButtonResponse = await disableNextButtonEvent;
+    assertFalse(disableNextButtonResponse.detail);
+  });
+});
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_run_page_test.js b/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_run_page_test.js
deleted file mode 100644
index deab53a..0000000
--- a/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_run_page_test.js
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright 2021 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
-import {PromiseResolver} from 'chrome://resources/ash/common/promise_resolver.js';
-import {FakeShimlessRmaService} from 'chrome://shimless-rma/fake_shimless_rma_service.js';
-import {setShimlessRmaServiceForTesting} from 'chrome://shimless-rma/mojo_interface_provider.js';
-import {ReimagingCalibrationRunPage} from 'chrome://shimless-rma/reimaging_calibration_run_page.js';
-import {ShimlessRma} from 'chrome://shimless-rma/shimless_rma.js';
-import {CalibrationOverallStatus, CalibrationStatus, ComponentType} from 'chrome://shimless-rma/shimless_rma.mojom-webui.js';
-import {assertDeepEquals, assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
-import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
-
-suite('reimagingCalibrationRunPageTest', function() {
-  /**
-   * ShimlessRma is needed to handle the 'transition-state' event used
-   * when handling calibration overall progress signals.
-   * @type {?ShimlessRma}
-   */
-  let shimlessRmaComponent = null;
-
-  /** @type {?ReimagingCalibrationRunPage} */
-  let component = null;
-
-  /** @type {?FakeShimlessRmaService} */
-  let service = null;
-
-  setup(() => {
-    document.body.innerHTML = trustedTypes.emptyHTML;
-    service = new FakeShimlessRmaService();
-    setShimlessRmaServiceForTesting(service);
-  });
-
-  teardown(() => {
-    component.remove();
-    component = null;
-    shimlessRmaComponent.remove();
-    shimlessRmaComponent = null;
-    service.reset();
-  });
-
-  /**
-   * @return {!Promise}
-   */
-  function initializeCalibrationRunPage() {
-    assertFalse(!!component);
-
-    shimlessRmaComponent =
-        /** @type {!ShimlessRma} */ (document.createElement('shimless-rma'));
-    assertTrue(!!shimlessRmaComponent);
-    document.body.appendChild(shimlessRmaComponent);
-
-    component = /** @type {!ReimagingCalibrationRunPage} */ (
-        document.createElement('reimaging-calibration-run-page'));
-    assertTrue(!!component);
-    document.body.appendChild(component);
-
-    return flushTasks();
-  }
-
-  test('NextButtonBeforeCalibrationCompleteFails', async () => {
-    const resolver = new PromiseResolver();
-    await initializeCalibrationRunPage();
-    let calibrationCompleteCalls = 0;
-    service.calibrationComplete = () => {
-      calibrationCompleteCalls++;
-      return resolver.promise;
-    };
-
-    let savedResult;
-    let savedError;
-    component.onNextButtonClick()
-        .then((result) => savedResult = result)
-        .catch((error) => savedError = error);
-    await flushTasks();
-
-    assertEquals(0, calibrationCompleteCalls);
-    assertTrue(savedError instanceof Error);
-    assertEquals(savedError.message, 'Calibration is not complete.');
-    assertEquals(savedResult, undefined);
-  });
-
-  test('NextButtonAfterCalibrationCompleteTriggersContinue', async () => {
-    const resolver = new PromiseResolver();
-    await initializeCalibrationRunPage();
-
-    const calibrationTitle = component.shadowRoot.querySelector('h1');
-    const progressSpinner =
-        component.shadowRoot.querySelector('paper-spinner-lite');
-    const completeIllustration = component.shadowRoot.querySelector('img');
-
-    assertEquals(
-        loadTimeData.getString('runCalibrationTitleText'),
-        calibrationTitle.textContent.trim());
-    assertFalse(progressSpinner.hidden);
-    assertTrue(completeIllustration.hidden);
-
-    let calibrationCompleteCalls = 0;
-    service.calibrationComplete = () => {
-      calibrationCompleteCalls++;
-      return resolver.promise;
-    };
-    service.triggerCalibrationOverallObserver(
-        CalibrationOverallStatus.kCalibrationOverallComplete, 0);
-    await flushTasks();
-
-    const expectedResult = {foo: 'bar'};
-    let savedResult;
-    component.onNextButtonClick().then((result) => savedResult = result);
-    // Resolve to a distinct result to confirm it was not modified.
-    resolver.resolve(expectedResult);
-    await flushTasks();
-
-    assertEquals(1, calibrationCompleteCalls);
-    assertDeepEquals(savedResult, expectedResult);
-    assertEquals(
-        loadTimeData.getString('runCalibrationCompleteTitleText'),
-        calibrationTitle.textContent.trim());
-    assertTrue(progressSpinner.hidden);
-    assertFalse(completeIllustration.hidden);
-  });
-
-  test(
-      'CalibrationOverallProgressRoundCompleteCallsContinueCalibration',
-      async () => {
-        const resolver = new PromiseResolver();
-        await initializeCalibrationRunPage();
-        let continueCalibrationCalls = 0;
-        service.continueCalibration = () => {
-          continueCalibrationCalls++;
-          return resolver.promise;
-        };
-        service.triggerCalibrationOverallObserver(
-            CalibrationOverallStatus.kCalibrationOverallCurrentRoundComplete,
-            0);
-        await flushTasks();
-
-        assertEquals(1, continueCalibrationCalls);
-      });
-
-  test(
-      'CalibrationOverallProgressRoundFailedCallsContinueCalibration',
-      async () => {
-        const resolver = new PromiseResolver();
-        await initializeCalibrationRunPage();
-        let continueCalibrationCalls = 0;
-        service.continueCalibration = () => {
-          continueCalibrationCalls++;
-          return resolver.promise;
-        };
-        service.triggerCalibrationOverallObserver(
-            CalibrationOverallStatus.kCalibrationOverallCurrentRoundFailed, 0);
-        await flushTasks();
-
-        assertEquals(1, continueCalibrationCalls);
-      });
-
-  test(
-      'CalibrationOverallProgressIniitalizationFailedCallsContinueCalibration',
-      async () => {
-        const resolver = new PromiseResolver();
-        await initializeCalibrationRunPage();
-        let continueCalibrationCalls = 0;
-        service.continueCalibration = () => {
-          continueCalibrationCalls++;
-          return resolver.promise;
-        };
-        service.triggerCalibrationOverallObserver(
-            CalibrationOverallStatus.kCalibrationOverallInitializationFailed,
-            0);
-        await flushTasks();
-
-        assertEquals(1, continueCalibrationCalls);
-      });
-});
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_run_page_test.ts b/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_run_page_test.ts
new file mode 100644
index 0000000..eabbd8b
--- /dev/null
+++ b/chrome/test/data/webui/chromeos/shimless_rma/reimaging_calibration_run_page_test.ts
@@ -0,0 +1,164 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'chrome://shimless-rma/shimless_rma.js';
+
+import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
+import {PromiseResolver} from 'chrome://resources/ash/common/promise_resolver.js';
+import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
+import {assert} from 'chrome://resources/js/assert.js';
+import {FakeShimlessRmaService} from 'chrome://shimless-rma/fake_shimless_rma_service.js';
+import {setShimlessRmaServiceForTesting} from 'chrome://shimless-rma/mojo_interface_provider.js';
+import {ReimagingCalibrationRunPage} from 'chrome://shimless-rma/reimaging_calibration_run_page.js';
+import {ShimlessRma} from 'chrome://shimless-rma/shimless_rma.js';
+import {CalibrationOverallStatus, StateResult} from 'chrome://shimless-rma/shimless_rma.mojom-webui.js';
+import {assertEquals, assertFalse, assertNotReached, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
+import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
+
+suite('reimagingCalibrationRunPageTest', function() {
+  // ShimlessRma is needed to handle the 'transition-state' event used when
+  // handling calibration overall progress signals.
+  let shimlessRmaComponent: ShimlessRma|null = null;
+
+  let component: ReimagingCalibrationRunPage|null = null;
+
+  const service: FakeShimlessRmaService = new FakeShimlessRmaService();
+
+  setup(() => {
+    document.body.innerHTML = window.trustedTypes!.emptyHTML;
+    setShimlessRmaServiceForTesting(service);
+  });
+
+  teardown(() => {
+    component?.remove();
+    component = null;
+    shimlessRmaComponent?.remove();
+    shimlessRmaComponent = null;
+  });
+
+  function initializeCalibrationRunPage(): Promise<void> {
+    assert(!shimlessRmaComponent);
+    shimlessRmaComponent = document.createElement(ShimlessRma.is);
+    assert(shimlessRmaComponent);
+    document.body.appendChild(shimlessRmaComponent);
+
+    assert(!component);
+    component = document.createElement(ReimagingCalibrationRunPage.is);
+    assert(component);
+    document.body.appendChild(component);
+
+    return flushTasks();
+  }
+
+  // Verify clicking the next button before a calibration completes is rejected
+  // but succeeds after calibration.
+  test('NextButtonBeforeAndAfterCalibration', async () => {
+    await initializeCalibrationRunPage();
+
+    assert(service);
+    const resolver = new PromiseResolver<{stateResult: StateResult}>();
+    let calibrationCompleteCalls = 0;
+    service.calibrationComplete = () => {
+      ++calibrationCompleteCalls;
+      return resolver.promise;
+    };
+
+    // Click the next button and expect a rejection.
+    assert(component);
+    let wasPromiseRejected = false;
+    try {
+      component.onNextButtonClick();
+      assertNotReached('Do not proceed while calibration running.');
+    } catch (error: unknown) {
+      wasPromiseRejected = true;
+    }
+    assertTrue(wasPromiseRejected);
+    let expectedCalls = 0;
+    assertEquals(expectedCalls, calibrationCompleteCalls);
+
+    // Trigger the calibration to complete.
+    service.triggerCalibrationOverallObserver(
+        CalibrationOverallStatus.kCalibrationOverallComplete, /* delayMs= */ 0);
+    await flushTasks();
+
+    // Click the next button again expecting it to succeed.
+    component.onNextButtonClick();
+    expectedCalls = 1;
+    assertEquals(expectedCalls, calibrationCompleteCalls);
+  });
+
+  // Verify the calibration complete status updates the UI elements.
+  test('CalibrationCompleteUpdatesUI', async () => {
+    await initializeCalibrationRunPage();
+
+    assert(component);
+    const calibrationTitle =
+        strictQuery('h1', component.shadowRoot, HTMLElement);
+    const progressSpinner =
+        strictQuery('paper-spinner-lite', component.shadowRoot, HTMLElement);
+    const completeIllustration =
+        strictQuery('img', component.shadowRoot, HTMLElement);
+
+    assertEquals(
+        loadTimeData.getString('runCalibrationTitleText'),
+        calibrationTitle.textContent!.trim());
+    assertFalse(progressSpinner.hidden);
+    assertTrue(completeIllustration.hidden);
+
+    // Trigger the calibration to complete.
+    assert(service);
+    service.triggerCalibrationOverallObserver(
+        CalibrationOverallStatus.kCalibrationOverallComplete, /* delayMs= */ 0);
+    await flushTasks();
+
+    // The UI should update to be in calibration complete mode.
+    assertEquals(
+        loadTimeData.getString('runCalibrationCompleteTitleText'),
+        calibrationTitle.textContent!.trim());
+    assertTrue(progressSpinner.hidden);
+    assertFalse(completeIllustration.hidden);
+  });
+
+  // Verify continue calibration is invoked for the correct calibration
+  // statuses.
+  test('CalibrationRoundCompleteContinueCalibration', async () => {
+    await initializeCalibrationRunPage();
+
+    assert(service);
+    const resolver = new PromiseResolver<{stateResult: StateResult}>();
+    let continueCalibrationCalls = 0;
+    service.continueCalibration = () => {
+      ++continueCalibrationCalls;
+      return resolver.promise;
+    };
+
+    service.triggerCalibrationOverallObserver(
+        CalibrationOverallStatus.kCalibrationOverallCurrentRoundComplete,
+        /* delayMs= */ 0);
+    await flushTasks();
+    let expectedCalls = 1;
+    assertEquals(expectedCalls, continueCalibrationCalls);
+
+    service.triggerCalibrationOverallObserver(
+        CalibrationOverallStatus.kCalibrationOverallCurrentRoundFailed,
+        /* delayMs= */ 0);
+    await flushTasks();
+    expectedCalls = 2;
+    assertEquals(expectedCalls, continueCalibrationCalls);
+
+    service.triggerCalibrationOverallObserver(
+        CalibrationOverallStatus.kCalibrationOverallInitializationFailed,
+        /* delayMs= */ 0);
+    await flushTasks();
+    expectedCalls = 3;
+    assertEquals(expectedCalls, continueCalibrationCalls);
+
+    // `kCalibrationOverallComplete` is not expected to continue
+    // calibration.
+    service.triggerCalibrationOverallObserver(
+        CalibrationOverallStatus.kCalibrationOverallComplete, /* delayMs= */ 0);
+    await flushTasks();
+    assertEquals(expectedCalls, continueCalibrationCalls);
+  });
+});
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.cc b/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.cc
index 553cc84..4ed1c7cf 100644
--- a/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.cc
+++ b/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.cc
@@ -115,6 +115,16 @@
   RunTest("chromeos/shimless_rma/reboot_page_test.js", "mocha.run()");
 }
 
+IN_PROC_BROWSER_TEST_F(ShimlessRmaBrowserTest, CalibrationFailedPage) {
+  RunTest("chromeos/shimless_rma/reimaging_calibration_failed_page_test.js",
+          "mocha.run()");
+}
+
+IN_PROC_BROWSER_TEST_F(ShimlessRmaBrowserTest, CalibrationRunPage) {
+  RunTest("chromeos/shimless_rma/reimaging_calibration_run_page_test.js",
+          "mocha.run()");
+}
+
 }  // namespace
 
 }  // namespace ash
diff --git a/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.js b/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.js
index fe336e2..8c1004a 100644
--- a/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.js
+++ b/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_browsertest.js
@@ -34,11 +34,6 @@
 
 const tests = [
   [
-    'ReimagingCalibrationFailedPageTest',
-    'reimaging_calibration_failed_page_test.js'
-  ],
-  ['ReimagingCalibrationRunPageTest', 'reimaging_calibration_run_page_test.js'],
-  [
     'ReimagingCalibrationSetupPageTest',
     'reimaging_calibration_setup_page_test.js'
   ],
diff --git a/chrome/test/data/webui/lens/overlay/region_selection_test.ts b/chrome/test/data/webui/lens/overlay/region_selection_test.ts
index c9c89fb3..27d07ac3 100644
--- a/chrome/test/data/webui/lens/overlay/region_selection_test.ts
+++ b/chrome/test/data/webui/lens/overlay/region_selection_test.ts
@@ -30,20 +30,20 @@
     BrowserProxyImpl.setInstance(testBrowserProxy);
 
     selectionOverlayElement = document.createElement('lens-selection-overlay');
-    // Position absolutely so we can handle logic of drag ending off this
-    // element.
-    selectionOverlayElement.style.position = 'absolute';
-    selectionOverlayElement.style.height = 'calc(100% - 100px)';
-    selectionOverlayElement.style.width = 'calc(100% - 100px)';
-    selectionOverlayElement.style.top = '50px';
-    selectionOverlayElement.style.left = '50px';
     document.body.appendChild(selectionOverlayElement);
+
+    // Set image to be less than fullscreen so we can handle logic of drag
+    // ending off this element.
+    selectionOverlayElement.$.backgroundImage.style.height =
+        'calc(100vh - 100px)';
+    selectionOverlayElement.$.backgroundImage.style.width =
+        'calc(100vw - 100px)';
     return waitAfterNextRender(selectionOverlayElement);
   });
 
   // Normalizes the given values to the size of selection overlay.
   function normalizedBox(box: RectF): RectF {
-    const boundingRect = selectionOverlayElement.getBoundingClientRect();
+    const boundingRect = getImageBoundingRect();
     return {
       x: box.x / boundingRect.width,
       y: box.y / boundingRect.height,
@@ -52,6 +52,10 @@
     };
   }
 
+  function getImageBoundingRect() {
+    return selectionOverlayElement.$.backgroundImage.getBoundingClientRect();
+  }
+
   // Does a drag and verifies that expectedRect is sent via mojo.
   async function assertDragGestureSendsRequest(
       fromPoint: Point, toPoint: Point, expectedRect: CenterRotatedBox) {
@@ -68,14 +72,14 @@
       `verify that completing a drag within the overlay bounds issues correct
       lens request via mojo`,
       async () => {
-        const overlayRect = selectionOverlayElement.getBoundingClientRect();
+        const imageBounds = getImageBoundingRect();
         const startPointInsideOverlay = {
-          x: overlayRect.left + 10,
-          y: overlayRect.top + 10,
+          x: imageBounds.left + 10,
+          y: imageBounds.top + 10,
         };
         const endPointInsideOverlay = {
-          x: overlayRect.left + 100,
-          y: overlayRect.top + 100,
+          x: imageBounds.left + 100,
+          y: imageBounds.top + 100,
         };
 
         const expectedRect: CenterRotatedBox = {
@@ -90,14 +94,14 @@
   test(
       'verify that completing a drag above the selection overlay rounds y to 0',
       async () => {
-        const overlayRect = selectionOverlayElement.getBoundingClientRect();
+        const imageBounds = getImageBoundingRect();
         const startPointInsideOverlay = {
-          x: overlayRect.left + 10,
-          y: overlayRect.top + 10,
+          x: imageBounds.left + 10,
+          y: imageBounds.top + 10,
         };
         const endPointAboveOverlay = {
-          x: overlayRect.left + 100,
-          y: overlayRect.top - 30,
+          x: imageBounds.left + 100,
+          y: imageBounds.top - 30,
         };
 
         const expectedRect: CenterRotatedBox = {
@@ -113,19 +117,19 @@
       `verify that completing a drag below the selection overlay rounds y to
       overlay height`,
       async () => {
-        const overlayRect = selectionOverlayElement.getBoundingClientRect();
+        const imageBounds = getImageBoundingRect();
         const startPointInsideOverlay = {
-          x: overlayRect.left + 10,
-          y: overlayRect.bottom - 20,
+          x: imageBounds.left + 10,
+          y: imageBounds.bottom - 20,
         };
         const endPointBelowOverlay = {
-          x: overlayRect.left + 100,
-          y: overlayRect.bottom + 20,
+          x: imageBounds.left + 100,
+          y: imageBounds.bottom + 20,
         };
 
         const expectedRect: CenterRotatedBox = {
           box: normalizedBox(
-              {x: 55, y: overlayRect.height - 10, width: 90, height: 20}),
+              {x: 55, y: imageBounds.height - 10, width: 90, height: 20}),
           rotation: 0,
           coordinateType: CenterRotatedBox_CoordinateType.kNormalized,
         };
@@ -137,14 +141,14 @@
       `verify that completing a drag to the left of the selection overlay rounds
        x to 0`,
       async () => {
-        const overlayRect = selectionOverlayElement.getBoundingClientRect();
+        const imageBounds = getImageBoundingRect();
         const startPointInsideOverlay = {
-          x: overlayRect.left + 20,
-          y: overlayRect.top + 10,
+          x: imageBounds.left + 20,
+          y: imageBounds.top + 10,
         };
         const endPointLeftOfOverlay = {
-          x: overlayRect.left - 10,
-          y: overlayRect.top + 100,
+          x: imageBounds.left - 10,
+          y: imageBounds.top + 100,
         };
 
         const expectedRect: CenterRotatedBox = {
@@ -160,19 +164,19 @@
       `verify that completing a drag to the right of the selection overlay
       rounds x to overlay width`,
       async () => {
-        const overlayRect = selectionOverlayElement.getBoundingClientRect();
+        const imageBounds = getImageBoundingRect();
         const startPointInsideOverlay = {
-          x: overlayRect.right - 20,
-          y: overlayRect.top + 10,
+          x: imageBounds.right - 20,
+          y: imageBounds.top + 10,
         };
         const endPointRightOfOverlay = {
-          x: overlayRect.right + 10,
-          y: overlayRect.top + 100,
+          x: imageBounds.right + 10,
+          y: imageBounds.top + 100,
         };
 
         const expectedRect: CenterRotatedBox = {
           box: normalizedBox(
-              {x: overlayRect.width - 10, y: 55, width: 20, height: 90}),
+              {x: imageBounds.width - 10, y: 55, width: 20, height: 90}),
           rotation: 0,
           coordinateType: CenterRotatedBox_CoordinateType.kNormalized,
         };
diff --git a/chrome/test/data/webui/lens/overlay/selection_overlay_test.ts b/chrome/test/data/webui/lens/overlay/selection_overlay_test.ts
index badadd5c..c63da15 100644
--- a/chrome/test/data/webui/lens/overlay/selection_overlay_test.ts
+++ b/chrome/test/data/webui/lens/overlay/selection_overlay_test.ts
@@ -11,7 +11,7 @@
 import type {LensPageRemote} from 'chrome-untrusted://lens/lens.mojom-webui.js';
 import type {OverlayObject} from 'chrome-untrusted://lens/overlay_object.mojom-webui.js';
 import type {SelectionOverlayElement} from 'chrome-untrusted://lens/selection_overlay.js';
-import {assertDeepEquals, assertEquals} from 'chrome-untrusted://webui-test/chai_assert.js';
+import {assertDeepEquals, assertEquals, assertNotEquals, assertNull} from 'chrome-untrusted://webui-test/chai_assert.js';
 import {flushTasks, waitAfterNextRender} from 'chrome-untrusted://webui-test/polymer_test_util.js';
 
 import {assertBoxesWithinThreshold, createObject} from '../utils/object_utils.js';
@@ -44,7 +44,7 @@
     // viewport.
     selectionOverlayElement.$.selectionOverlay.style.width = '100%';
     selectionOverlayElement.$.selectionOverlay.style.height = '100%';
-    return flushTasks();
+    return waitAfterNextRender(selectionOverlayElement);
   });
 
   // Normalizes the given values to the size of selection overlay.
@@ -234,4 +234,28 @@
         `${expectedLeft}%`,
         postSelectionStyles.getPropertyValue('--selection-left'));
   });
+
+  test('verify that resizing renders image with padding', async () => {
+    selectionOverlayElement.style.display = 'block';
+    selectionOverlayElement.style.width = '50px';
+    selectionOverlayElement.style.height = '50px';
+    await waitAfterNextRender(selectionOverlayElement);
+    assertNotEquals(null, selectionOverlayElement.getAttribute('is-resized'));
+
+    // Verify resizing back no longer renders with padding
+    selectionOverlayElement.style.width = '100%';
+    selectionOverlayElement.style.height = '100%';
+    await waitAfterNextRender(selectionOverlayElement);
+    assertNull(selectionOverlayElement.getAttribute('is-resized'));
+  });
+
+  test(
+      'verify that resizing within threshold does not rerender image',
+      async () => {
+        selectionOverlayElement.style.display = 'block';
+        selectionOverlayElement.style.width =
+            `${selectionOverlayElement.getBoundingClientRect().width - 4}px`;
+        await waitAfterNextRender(selectionOverlayElement);
+        assertNull(selectionOverlayElement.getAttribute('is-resized'));
+      });
 });
diff --git a/chrome/test/data/webui/settings/security_page_test.ts b/chrome/test/data/webui/settings/security_page_test.ts
index e5179721..ea1d455 100644
--- a/chrome/test/data/webui/settings/security_page_test.ts
+++ b/chrome/test/data/webui/settings/security_page_test.ts
@@ -9,7 +9,7 @@
 import {HttpsFirstModeSetting, SafeBrowsingSetting} from 'chrome://settings/lazy_load.js';
 import type {SettingsPrefsElement, SettingsToggleButtonElement} from 'chrome://settings/settings.js';
 import {HatsBrowserProxyImpl, CrSettingsPrefs, MetricsBrowserProxyImpl, OpenWindowProxyImpl, PrivacyElementInteractions, PrivacyPageBrowserProxyImpl, Router, routes, SafeBrowsingInteractions, SecureDnsMode, SecurityPageInteraction} from 'chrome://settings/settings.js';
-import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {assertEquals, assertFalse, assertTrue, assertNotEquals} from 'chrome://webui-test/chai_assert.js';
 import {isChildVisible, eventToPromise, microtasksFinished} from 'chrome://webui-test/test_util.js';
 import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
 
@@ -994,6 +994,36 @@
     assertTrue(isChildVisible(page, '#learnMoreLabelContainer'));
   });
 
+  test('LearnMoreLinkClickableWhenControlledByPolicy', async () => {
+    page.$.safeBrowsingEnhanced.$.expandButton.click();
+
+    // Set the page to be enterprise policy enforced.
+    page.set(
+        'prefs.generated.safe_browsing.enforcement',
+        chrome.settingsPrivate.Enforcement.ENFORCED);
+    flush();
+
+    const learnMoreLink = page.shadowRoot!.querySelector<HTMLElement>(
+        '#enhancedProtectionLearnMoreLink');
+
+    // Confirm that the learnMoreLink element exists.
+    assertNotEquals(learnMoreLink, null);
+
+    // Confirm that the pointer-events value is auto when enterprise policy is
+    // enforced.
+    assertEquals(
+        'auto',
+        (learnMoreLink!.computedStyleMap()!.get('pointer-events') as
+         CSSKeywordValue)
+            .value);
+
+    // Confirm that the correct link was clicked.
+    learnMoreLink!.click();
+    const url = await openWindowProxy.whenCalled('openUrl');
+    assertEquals(
+        url, loadTimeData.getString('enhancedProtectionHelpCenterURL'));
+  });
+
   // <if expr="_google_chrome">
   test('StandardProtectionDropdownWithProxyString', async () => {
     loadTimeData.overrideValues({
diff --git a/chrome/test/data/webui/welcome/BUILD.gn b/chrome/test/data/webui/welcome/BUILD.gn
index 8d4055a..037b2c7 100644
--- a/chrome/test/data/webui/welcome/BUILD.gn
+++ b/chrome/test/data/webui/welcome/BUILD.gn
@@ -36,7 +36,7 @@
   ]
   ts_deps = [
     "//chrome/browser/resources/welcome:build_ts",
-    "//third_party/polymer/v3_0:library",
+    "//third_party/lit/v3_0:build_ts",
     "//ui/webui/resources/cr_elements:build_ts",
     "//ui/webui/resources/js:build_ts",
   ]
diff --git a/chrome/test/data/webui/welcome/navigation_mixin_test.ts b/chrome/test/data/webui/welcome/navigation_mixin_test.ts
index 07fb21b9..1d62675 100644
--- a/chrome/test/data/webui/welcome/navigation_mixin_test.ts
+++ b/chrome/test/data/webui/welcome/navigation_mixin_test.ts
@@ -3,26 +3,22 @@
 // found in the LICENSE file.
 
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
-import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
 import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {eventToPromise} from 'chrome://webui-test/test_util.js';
-import {NavigationMixin} from 'chrome://welcome/navigation_mixin.js';
+import {NavigationMixinLit} from 'chrome://welcome/navigation_mixin_lit.js';
 import {navigateTo, navigateToNextStep, Routes} from 'chrome://welcome/router.js';
 
-suite('NavigationBehaviorTest', function() {
-  class TestElement extends NavigationMixin
-  (PolymerElement) {
+suite('NavigationMixinTest', function() {
+  const TestElementBase = NavigationMixinLit(CrLitElement);
+  class TestElement extends TestElementBase {
     static get is() {
       return 'test-element';
     }
 
-    static get template() {
-      return html``;
-    }
-
-    static get properties() {
+    static override get properties() {
       return {
-        subtitle: String,
+        subtitle: {type: String},
       };
     }
 
@@ -31,8 +27,7 @@
     changeCalled: boolean = false;
     exitCalled: boolean = false;
 
-    override ready() {
-      super.ready();
+    override firstUpdated() {
       this.reset();
     }
 
diff --git a/chrome/updater/device_management/dm_storage.h b/chrome/updater/device_management/dm_storage.h
index daa9246..10898b5 100644
--- a/chrome/updater/device_management/dm_storage.h
+++ b/chrome/updater/device_management/dm_storage.h
@@ -64,6 +64,7 @@
 //   3) DM policies.
 class DMStorage : public base::RefCountedThreadSafe<DMStorage> {
  public:
+  static constexpr size_t kMaxDmTokenLength = 4096;
 #if BUILDFLAG(IS_WIN)
   explicit DMStorage(const base::FilePath& policy_cache_root);
 #else
diff --git a/chrome/updater/device_management/dm_storage_unittest.cc b/chrome/updater/device_management/dm_storage_unittest.cc
index d8c0b48..c5c07f35 100644
--- a/chrome/updater/device_management/dm_storage_unittest.cc
+++ b/chrome/updater/device_management/dm_storage_unittest.cc
@@ -133,8 +133,10 @@
       legacy_key.Create(HKEY_LOCAL_MACHINE, kRegKeyCompanyLegacyCloudManagement,
                         Wow6432(KEY_WRITE)),
       ERROR_SUCCESS);
-  EXPECT_EQ(legacy_key.WriteValue(kRegValueCloudManagementEnrollmentToken,
-                                  L"legacy_test_enrollment_token"),
+  constexpr char kLegacyEnrollmentToken[] = "legacy_test_enrollment_token";
+  EXPECT_EQ(legacy_key.WriteValue(
+                kRegValueCloudManagementEnrollmentToken, kLegacyEnrollmentToken,
+                sizeof(kLegacyEnrollmentToken) - 1, REG_BINARY),
             ERROR_SUCCESS);
   EXPECT_EQ(storage->GetEnrollmentToken(), "legacy_test_enrollment_token");
 
@@ -142,7 +144,9 @@
   EXPECT_EQ(key.Create(HKEY_LOCAL_MACHINE, kRegKeyCompanyCloudManagement,
                        Wow6432(KEY_WRITE)),
             ERROR_SUCCESS);
-  EXPECT_EQ(key.WriteValue(kRegValueEnrollmentToken, L"test_enrollment_token"),
+  constexpr char kEnrollmentToken[] = "test_enrollment_token";
+  EXPECT_EQ(key.WriteValue(kRegValueEnrollmentToken, kEnrollmentToken,
+                           sizeof(kEnrollmentToken) - 1, REG_BINARY),
             ERROR_SUCCESS);
   EXPECT_EQ(storage->GetEnrollmentToken(), "test_enrollment_token");
 }
diff --git a/chrome/updater/device_management/dm_storage_win.cc b/chrome/updater/device_management/dm_storage_win.cc
index a513f6a2..1b75d56a 100644
--- a/chrome/updater/device_management/dm_storage_win.cc
+++ b/chrome/updater/device_management/dm_storage_win.cc
@@ -5,6 +5,7 @@
 #include "chrome/updater/device_management/dm_storage.h"
 
 #include <string>
+#include <vector>
 
 #include "base/base_paths_win.h"
 #include "base/files/file_path.h"
@@ -18,7 +19,6 @@
 #include "chrome/updater/win/win_constants.h"
 
 namespace updater {
-
 namespace {
 
 // Registry for device ID.
@@ -26,6 +26,68 @@
     L"SOFTWARE\\Microsoft\\Cryptography\\";
 constexpr wchar_t kRegValueMachineGuid[] = L"MachineGuid";
 
+bool ReadTokenBinary(const base::win::RegKey& key,
+                     const wchar_t* name,
+                     std::string& token) {
+  VLOG(2) << __func__;
+  DWORD size = 0;
+  DWORD type = 0;
+  LONG error = key.ReadValue(name, nullptr, &size, &type);
+  if (error != ERROR_SUCCESS) {
+    VLOG(2) << "ReadValue failed: " << error;
+    return false;
+  }
+  if (size > DMStorage::kMaxDmTokenLength) {
+    VLOG(2) << "Value is too large: " << size;
+    return false;
+  }
+  std::vector<char> value(size);
+  error = key.ReadValue(name, &value.front(), &size, &type);
+  if (error != ERROR_SUCCESS) {
+    VLOG(2) << "ReadValue failed: " << error;
+    return false;
+  }
+  token.assign(value.begin(), value.end());
+  return true;
+}
+
+bool WriteTokenBinary(base::win::RegKey& key,
+                      const wchar_t* name,
+                      const std::string& token) {
+  VLOG(2) << __func__;
+  if (token.size() > DMStorage::kMaxDmTokenLength) {
+    VLOG(2) << "Value is too large: " << token.size();
+    return false;
+  }
+  const LONG error =
+      key.WriteValue(name, token.data(), token.size(), REG_BINARY);
+  if (error != ERROR_SUCCESS) {
+    VLOG(2) << "WriteValue failed: " << error;
+    return false;
+  }
+  return true;
+}
+
+// Set `name` in `root`\`key` as a binary `value`.
+bool SetRegistryKeyBinary(HKEY root,
+                          const std::wstring& key,
+                          const std::wstring& name,
+                          const std::string& value) {
+  base::win::RegKey rkey;
+  LONG error = rkey.Create(root, key.c_str(), Wow6432(KEY_WRITE));
+  if (error != ERROR_SUCCESS) {
+    VLOG(1) << "Failed to open (" << root << ") " << key << ": " << error;
+    return false;
+  }
+  error = rkey.WriteValue(name.c_str(), value.data(), value.size(), REG_BINARY);
+  if (error != ERROR_SUCCESS) {
+    VLOG(1) << "Failed to write (" << root << ") " << key << " @ " << name
+            << " (binary): " << error;
+    return false;
+  }
+  return error == ERROR_SUCCESS;
+}
+
 class TokenService : public TokenServiceInterface {
  public:
   TokenService() = default;
@@ -67,9 +129,8 @@
 
 bool TokenService::StoreEnrollmentToken(const std::string& token) {
   const bool result =
-      SetRegistryKey(HKEY_LOCAL_MACHINE, kRegKeyCompanyCloudManagement,
-                     kRegValueEnrollmentToken, base::SysUTF8ToWide(token));
-
+      SetRegistryKeyBinary(HKEY_LOCAL_MACHINE, kRegKeyCompanyCloudManagement,
+                           kRegValueEnrollmentToken, token);
   VLOG(1) << "Update enrollment token to: [" << token
           << "], bool result=" << result;
   return result;
@@ -84,41 +145,35 @@
 }
 
 std::string TokenService::GetEnrollmentToken() const {
-  std::wstring token;
+  std::string token;
   if (base::win::RegKey key;
       key.Open(HKEY_LOCAL_MACHINE, kRegKeyCompanyCloudManagement,
                Wow6432(KEY_READ)) == ERROR_SUCCESS &&
-      key.ReadValue(kRegValueEnrollmentToken, &token) == ERROR_SUCCESS) {
-    return base::SysWideToUTF8(token);
+      ReadTokenBinary(key, kRegValueEnrollmentToken, token)) {
+    return token;
   }
-
   if (base::win::RegKey key;
       key.Open(HKEY_LOCAL_MACHINE, kRegKeyCompanyLegacyCloudManagement,
                Wow6432(KEY_READ)) == ERROR_SUCCESS &&
-      key.ReadValue(kRegValueCloudManagementEnrollmentToken, &token) ==
-          ERROR_SUCCESS) {
-    return base::SysWideToUTF8(token);
+      ReadTokenBinary(key, kRegValueCloudManagementEnrollmentToken, token)) {
+    return token;
   }
   return {};
 }
 
 bool TokenService::StoreDmToken(const std::string& token) {
-  const std::wstring dm_token(base::SysUTF8ToWide(token));
-  if (!SetRegistryKey(HKEY_LOCAL_MACHINE, kRegKeyCompanyEnrollment,
-                      kRegValueDmToken, dm_token)) {
+  if (!SetRegistryKeyBinary(HKEY_LOCAL_MACHINE, kRegKeyCompanyEnrollment,
+                            kRegValueDmToken, token)) {
     VLOG(1) << "Failed to write DM token.";
     return false;
   }
-
   base::win::RegKey legacy_key;
   if (legacy_key.Create(HKEY_LOCAL_MACHINE, kRegKeyCompanyLegacyEnrollment,
                         KEY_WOW64_64KEY | KEY_WRITE) != ERROR_SUCCESS ||
-      legacy_key.WriteValue(kRegValueDmToken, dm_token.c_str()) !=
-          ERROR_SUCCESS) {
+      !WriteTokenBinary(legacy_key, kRegValueDmToken, token)) {
     VLOG(1) << "Failed to write DM token at the legacy place.";
     return false;
   }
-
   VLOG(1) << "Updated DM token to: [" << token << "]";
   return true;
 }
@@ -146,19 +201,18 @@
 }
 
 std::string TokenService::GetDmToken() const {
-  std::wstring token;
+  std::string token;
   if (base::win::RegKey key;
       key.Open(HKEY_LOCAL_MACHINE, kRegKeyCompanyEnrollment,
                Wow6432(KEY_READ)) == ERROR_SUCCESS &&
-      key.ReadValue(kRegValueDmToken, &token) == ERROR_SUCCESS) {
-    return base::SysWideToUTF8(token);
+      ReadTokenBinary(key, kRegValueDmToken, token)) {
+    return token;
   }
-
   if (base::win::RegKey key;
       key.Open(HKEY_LOCAL_MACHINE, kRegKeyCompanyLegacyEnrollment,
                KEY_WOW64_64KEY | KEY_READ) == ERROR_SUCCESS &&
-      key.ReadValue(kRegValueDmToken, &token) == ERROR_SUCCESS) {
-    return base::SysWideToUTF8(token);
+      ReadTokenBinary(key, kRegValueDmToken, token)) {
+    return token;
   }
   return {};
 }
diff --git a/chromeos/ash/components/dbus/debug_daemon/BUILD.gn b/chromeos/ash/components/dbus/debug_daemon/BUILD.gn
index 134fa7f..aa9e336 100644
--- a/chromeos/ash/components/dbus/debug_daemon/BUILD.gn
+++ b/chromeos/ash/components/dbus/debug_daemon/BUILD.gn
@@ -18,6 +18,8 @@
   ]
 
   sources = [
+    "binary_log_files_reader.cc",
+    "binary_log_files_reader.h",
     "debug_daemon_client.cc",
     "debug_daemon_client.h",
     "debug_daemon_client_provider.cc",
diff --git a/chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.cc b/chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.cc
new file mode 100644
index 0000000..99536dbd
--- /dev/null
+++ b/chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.cc
@@ -0,0 +1,84 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/files/scoped_file.h"
+#include "base/functional/bind.h"
+#include "base/logging.h"
+#include "base/task/thread_pool.h"
+#include "chromeos/ash/components/dbus/cryptohome/rpc.pb.h"
+#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
+#include "chromeos/dbus/common/pipe_reader.h"
+#include "third_party/cros_system_api/dbus/debugd/dbus-constants.h"
+
+namespace feedback {
+
+BinaryLogFilesReader::BinaryLogFilesReader() = default;
+BinaryLogFilesReader::~BinaryLogFilesReader() = default;
+
+void BinaryLogFilesReader::GetFeedbackBinaryLogs(
+    const cryptohome::AccountIdentifier& id,
+    debugd::FeedbackBinaryLogType log_type,
+    GetFeedbackBinaryLogsCallback callback) {
+  CHECK(callback);
+  const auto task_runner = base::ThreadPool::CreateTaskRunner(
+      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
+       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN});
+  auto pipe_reader = std::make_unique<chromeos::PipeReader>(task_runner);
+
+  // Sets up stream for data collection and returns the write end of the pipe if
+  // stream was setup correctly. The write end will be passed to debugd which
+  // will write data to it. On completion or any failure, OnIOComplete will be
+  // called.
+  base::ScopedFD pipe_write_end = pipe_reader->StartIO(base::BindOnce(
+      &BinaryLogFilesReader::OnIOComplete, weak_ptr_factory_.GetWeakPtr(),
+      log_type, std::move(pipe_reader), std::move(callback)));
+
+  // Current implementation only fetches one log file.
+  std::map<debugd::FeedbackBinaryLogType, base::ScopedFD> log_fd_map;
+  log_fd_map[log_type] = std::move(pipe_write_end);
+  // Pass the write end of the pipe to debugd to collect data.
+  // OnGetFeedbackBinaryLogsCompleted will be called after debugd has started
+  // writing logs to the pipe. The purpose of the callback is merely for
+  // logging. The debugd method GetFeedbackBinaryLogs is async and will return
+  // without waiting for IO completion. Once debugd finishes writing to the
+  // pipe, it will close its write end. The read end of pipe will receive data
+  // through the OnIOComplete callback. In case of timeout or other failures,
+  // the write end will be closed and OnIOComplete will be called with empty
+  // data.
+  ash::DebugDaemonClient::Get()->GetFeedbackBinaryLogs(
+      id, log_fd_map,
+      base::BindOnce(&BinaryLogFilesReader::OnGetFeedbackBinaryLogsCompleted,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void BinaryLogFilesReader::OnIOComplete(
+    debugd::FeedbackBinaryLogType log_type,
+    std::unique_ptr<chromeos::PipeReader> pipe_reader,
+    GetFeedbackBinaryLogsCallback callback,
+    std::optional<std::string> data) {
+  CHECK(callback);
+  // Shut down data collection.
+  pipe_reader.reset();
+  // Current implementation supports only one log type at a time. Therefore, it
+  // is ok to run the callback here.
+  BinaryLogsResponse response =
+      std::make_unique<std::map<FeedbackBinaryLogType, std::string>>();
+  response->emplace(log_type, data.value_or(std::string()));
+  std::move(callback).Run(std::move(response));
+}
+
+void BinaryLogFilesReader::OnGetFeedbackBinaryLogsCompleted(bool succeeded) {
+  if (!succeeded) {
+    LOG(ERROR) << "GetFeedbackBinaryLogs failed.";
+  }
+}
+
+}  // namespace feedback
diff --git a/chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.h b/chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.h
new file mode 100644
index 0000000..6ea2f75
--- /dev/null
+++ b/chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.h
@@ -0,0 +1,58 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_ASH_COMPONENTS_DBUS_DEBUG_DAEMON_BINARY_LOG_FILES_READER_H_
+#define CHROMEOS_ASH_COMPONENTS_DBUS_DEBUG_DAEMON_BINARY_LOG_FILES_READER_H_
+
+#include <map>
+#include <memory>
+
+#include "base/component_export.h"
+#include "base/memory/weak_ptr.h"
+#include "chromeos/dbus/common/pipe_reader.h"
+#include "third_party/cros_system_api/dbus/debugd/dbus-constants.h"
+
+namespace cryptohome {
+class AccountIdentifier;
+}
+
+namespace feedback {
+
+using debugd::FeedbackBinaryLogType;
+
+// Helper class to fetch binary log files over dbus using debugd's
+// GetFeedbackBinaryLogs method. The method can fetch multiple logs, one for
+// each log types in one trip. Since currently there is one log type only, this
+// class only supports fetching one log for now. It can be expanded when needed.
+class COMPONENT_EXPORT(DEBUG_DAEMON) BinaryLogFilesReader {
+ public:
+  BinaryLogFilesReader();
+  BinaryLogFilesReader(const BinaryLogFilesReader&) = delete;
+  BinaryLogFilesReader& operator=(const BinaryLogFilesReader&) = delete;
+  ~BinaryLogFilesReader();
+
+  using BinaryLogsResponse =
+      std::unique_ptr<std::map<FeedbackBinaryLogType, std::string>>;
+  // Callback type for GetFeedbackBinaryLogs();
+  using GetFeedbackBinaryLogsCallback =
+      base::OnceCallback<void(BinaryLogsResponse logs_response)>;
+  // Start calling debugd's GetFeedbackBinaryLogs method to fetch log files. The
+  // callback will be invoked once fetching is completed.
+  void GetFeedbackBinaryLogs(const cryptohome::AccountIdentifier& id,
+                             debugd::FeedbackBinaryLogType log_type,
+                             GetFeedbackBinaryLogsCallback callback);
+
+ private:
+  void OnIOComplete(debugd::FeedbackBinaryLogType log_type,
+                    std::unique_ptr<chromeos::PipeReader> pipe_reader,
+                    GetFeedbackBinaryLogsCallback callback,
+                    std::optional<std::string> data);
+  void OnGetFeedbackBinaryLogsCompleted(bool succeeded);
+
+  base::WeakPtrFactory<BinaryLogFilesReader> weak_ptr_factory_{this};
+};
+
+}  // namespace feedback
+
+#endif  // CHROMEOS_ASH_COMPONENTS_DBUS_DEBUG_DAEMON_BINARY_LOG_FILES_READER_H_
diff --git a/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.cc b/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.cc
index 241a2003..584e077 100644
--- a/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.cc
+++ b/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.cc
@@ -16,6 +16,7 @@
 #include <vector>
 
 #include "base/files/file_path.h"
+#include "base/files/scoped_file.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/json/json_string_value_serializer.h"
@@ -38,6 +39,7 @@
 #include "dbus/message.h"
 #include "dbus/object_path.h"
 #include "dbus/object_proxy.h"
+#include "third_party/cros_system_api/dbus/debugd/dbus-constants.h"
 
 namespace ash {
 
@@ -282,6 +284,35 @@
                        pipe_reader->AsWeakPtr()));
   }
 
+  void GetFeedbackBinaryLogs(
+      const cryptohome::AccountIdentifier& id,
+      const std::map<debugd::FeedbackBinaryLogType, base::ScopedFD>&
+          log_type_fds,
+      chromeos::VoidDBusMethodCallback callback) override {
+    dbus::MethodCall method_call(debugd::kDebugdInterface,
+                                 debugd::kGetFeedbackBinaryLogs);
+    dbus::MessageWriter writer(&method_call);
+    writer.AppendString(id.account_id());
+
+    dbus::MessageWriter array_writer(nullptr);
+    // Write map of log_type and fd.
+    writer.OpenArray("{ih}", &array_writer);
+    for (const auto& log_type : log_type_fds) {
+      dbus::MessageWriter dict_entry_writer(nullptr);
+      array_writer.OpenDictEntry(&dict_entry_writer);
+      dict_entry_writer.AppendInt32(log_type.first);
+      dict_entry_writer.AppendFileDescriptor(log_type.second.get());
+      array_writer.CloseContainer(&dict_entry_writer);
+    }
+    writer.CloseContainer(&array_writer);
+
+    DVLOG(1) << "Requesting feedback binary logs";
+    debugdaemon_proxy_->CallMethodWithErrorResponse(
+        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
+        base::BindOnce(&DebugDaemonClientImpl::OnFeedbackBinaryLogsResponse,
+                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+  }
+
   void BackupArcBugReport(const cryptohome::AccountIdentifier& id,
                           chromeos::VoidDBusMethodCallback callback) override {
     dbus::MethodCall method_call(debugd::kDebugdInterface,
@@ -749,6 +780,17 @@
     }
   }
 
+  void OnFeedbackBinaryLogsResponse(chromeos::VoidDBusMethodCallback callback,
+                                    dbus::Response* response,
+                                    dbus::ErrorResponse* err_response) {
+    bool succeeded = !err_response;
+    if (!succeeded) {
+      LOG(ERROR) << "Failed to GetFeedbackBinaryLogs. Error: "
+                 << err_response->GetErrorName();
+    }
+    std::move(callback).Run(succeeded);
+  }
+
   // Called when a response for a simple start is received.
   void OnStartMethod(dbus::Response* response) {
     if (!response) {
diff --git a/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h b/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h
index 9477cd8..697399d 100644
--- a/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h
+++ b/chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h
@@ -23,6 +23,7 @@
 #include "chromeos/dbus/common/dbus_client.h"
 #include "chromeos/dbus/common/dbus_method_call_status.h"
 #include "dbus/message.h"
+#include "third_party/cros_system_api/dbus/debugd/dbus-constants.h"
 #include "third_party/cros_system_api/dbus/service_constants.h"
 
 namespace cryptohome {
@@ -142,6 +143,16 @@
       const std::vector<debugd::FeedbackLogType>& requested_logs,
       GetLogsCallback callback) = 0;
 
+  // Gets feedback binary logs from debugd.
+  // |id|: Cryptohome Account identifier for the user to get logs for.
+  // |log_type_fds|: The map of FeedbackBinaryLogType and its FD pair.
+  // |callback|: The callback to be invoked once the debugd method is completed.
+  virtual void GetFeedbackBinaryLogs(
+      const cryptohome::AccountIdentifier& id,
+      const std::map<debugd::FeedbackBinaryLogType, base::ScopedFD>&
+          log_type_fds,
+      chromeos::VoidDBusMethodCallback callback) = 0;
+
   // Retrieves the ARC bug report for user identified by |userhash|
   // and saves it in debugd daemon store.
   // If a backup already exists, it is overwritten.
diff --git a/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.cc b/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.cc
index 376e939..bf0cb99c 100644
--- a/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.cc
+++ b/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.cc
@@ -15,11 +15,14 @@
 
 #include "base/command_line.h"
 #include "base/containers/contains.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_file.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/location.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/task/single_thread_task_runner.h"
+#include "base/task/thread_pool.h"
 #include "chromeos/dbus/constants/dbus_switches.h"
 
 namespace {
@@ -27,6 +30,11 @@
 const char kCrOSTracingAgentName[] = "cros";
 const char kCrOSTraceLabel[] = "systemTraceEvents";
 
+// Writes the |data| to |fd|, then close |fd|.
+void WriteData(base::ScopedFD fd, const std::string& data) {
+  base::WriteFileDescriptor(fd.get(), data);
+}
+
 }  // namespace
 
 namespace ash {
@@ -133,6 +141,24 @@
       base::BindOnce(std::move(callback), /*succeeded=*/true, sample));
 }
 
+void FakeDebugDaemonClient::GetFeedbackBinaryLogs(
+    const cryptohome::AccountIdentifier& id,
+    const std::map<debugd::FeedbackBinaryLogType, base::ScopedFD>& log_type_fds,
+    chromeos::VoidDBusMethodCallback callback) {
+  constexpr char kTestData[] = "TestData";
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, base::BindOnce(std::move(callback), /*succeeded=*/true));
+
+  // Write dummy data to the pipes after callback is invoked to simulate
+  // potential delay writing bug chunk of data.
+  for (const auto& item : log_type_fds) {
+    base::ThreadPool::PostTask(
+        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
+        base::BindOnce(&WriteData, base::ScopedFD(dup(item.second.get())),
+                       kTestData));
+  }
+}
+
 void FakeDebugDaemonClient::BackupArcBugReport(
     const cryptohome::AccountIdentifier& id,
     chromeos::VoidDBusMethodCallback callback) {
diff --git a/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.h b/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.h
index 8a61c57..4b674c0 100644
--- a/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.h
+++ b/chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.h
@@ -65,6 +65,11 @@
       const cryptohome::AccountIdentifier& id,
       const std::vector<debugd::FeedbackLogType>& requested_logs,
       GetLogsCallback callback) override;
+  void GetFeedbackBinaryLogs(
+      const cryptohome::AccountIdentifier& id,
+      const std::map<debugd::FeedbackBinaryLogType, base::ScopedFD>&
+          log_type_fds,
+      chromeos::VoidDBusMethodCallback callback) override;
   void BackupArcBugReport(const cryptohome::AccountIdentifier& id,
                           chromeos::VoidDBusMethodCallback callback) override;
   void GetAllLogs(GetLogsCallback callback) override;
diff --git a/chromeos/ash/components/phonehub/BUILD.gn b/chromeos/ash/components/phonehub/BUILD.gn
index d2b268a..e5ea928 100644
--- a/chromeos/ash/components/phonehub/BUILD.gn
+++ b/chromeos/ash/components/phonehub/BUILD.gn
@@ -141,6 +141,7 @@
 
   deps = [
     "//ash/constants",
+    "//ash/public/cpp:cpp",
     "//ash/resources/vector_icons",
     "//ash/webui/eche_app_ui:eche_app_ui_pref",
     "//ash/webui/eche_app_ui:eche_connection_status",
@@ -159,6 +160,7 @@
     "//chromeos/dbus/power",
     "//chromeos/services/network_config/public/cpp",
     "//components/keyed_service/core",
+    "//components/metrics/structured:structured_metrics_features",
     "//components/prefs",
     "//components/session_manager/core",
     "//device/bluetooth",
@@ -283,6 +285,7 @@
     "notification_manager_impl_unittest.cc",
     "notification_processor_unittest.cc",
     "onboarding_ui_tracker_impl_unittest.cc",
+    "phone_hub_structured_metrics_logger_unittest.cc",
     "phone_hub_ui_readiness_recorder_unittest.cc",
     "phone_status_model_unittest.cc",
     "phone_status_processor_unittest.cc",
@@ -318,6 +321,7 @@
     "//chromeos/ash/services/secure_channel/public/cpp/client:test_support",
     "//chromeos/ash/services/secure_channel/public/mojom",
     "//chromeos/dbus/power",
+    "//components/metrics/structured:structured_metrics_features",
     "//components/prefs:test_support",
     "//components/session_manager/core",
     "//device/bluetooth:mocks",
diff --git a/chromeos/ash/components/phonehub/DEPS b/chromeos/ash/components/phonehub/DEPS
index 1b1ffbe..aea2fed8 100644
--- a/chromeos/ash/components/phonehub/DEPS
+++ b/chromeos/ash/components/phonehub/DEPS
@@ -1,9 +1,11 @@
 include_rules = [
+  "+ash/public/cpp",
   "+ash/resources/vector_icons",
   "+ash/webui/eche_app_ui",
   "+chromeos/ash/components/multidevice",
   "+components/keyed_service/core/keyed_service.h",
   "+components/session_manager/core",
+  "+components/metrics/structured",
   "+device/bluetooth",
   "+services/data_decoder/public",
   "+third_party/skia",
diff --git a/chromeos/ash/components/phonehub/feature_status_provider_impl.cc b/chromeos/ash/components/phonehub/feature_status_provider_impl.cc
index 61bc1c1..dbf045a 100644
--- a/chromeos/ash/components/phonehub/feature_status_provider_impl.cc
+++ b/chromeos/ash/components/phonehub/feature_status_provider_impl.cc
@@ -144,12 +144,15 @@
     multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
     secure_channel::ConnectionManager* connection_manager,
     session_manager::SessionManager* session_manager,
-    chromeos::PowerManagerClient* power_manager_client)
+    chromeos::PowerManagerClient* power_manager_client,
+    PhoneHubStructuredMetricsLogger* phone_hub_structured_metrics_logger)
     : device_sync_client_(device_sync_client),
       multidevice_setup_client_(multidevice_setup_client),
       connection_manager_(connection_manager),
       session_manager_(session_manager),
-      power_manager_client_(power_manager_client) {
+      power_manager_client_(power_manager_client),
+      phone_hub_structured_metrics_logger_(
+          phone_hub_structured_metrics_logger) {
   DCHECK(session_manager_);
   DCHECK(power_manager_client_);
   device_sync_client_->AddObserver(this);
@@ -250,6 +253,22 @@
   PA_LOG(INFO) << "Phone Hub feature status: " << *status_ << " => "
                << computed_status;
   *status_ = computed_status;
+  switch (status_.value()) {
+    case FeatureStatus::kDisabled:
+    case FeatureStatus::kLockOrSuspended:
+      phone_hub_structured_metrics_logger_->ResetSessionId();
+      break;
+    case FeatureStatus::kEligiblePhoneButNotSetUp:
+    case FeatureStatus::kNotEligibleForFeature:
+    case FeatureStatus::kPhoneSelectedAndPendingSetup:
+      phone_hub_structured_metrics_logger_->ResetCachedInformation();
+      break;
+    case FeatureStatus::kEnabledAndConnecting:
+    case FeatureStatus::kEnabledAndConnected:
+    case FeatureStatus::kUnavailableBluetoothOff:
+    case FeatureStatus::kEnabledButDisconnected:
+      break;
+  }
   NotifyStatusChanged();
 
   UMA_HISTOGRAM_ENUMERATION("PhoneHub.Adoption.FeatureStatusChangesSinceLogin",
diff --git a/chromeos/ash/components/phonehub/feature_status_provider_impl.h b/chromeos/ash/components/phonehub/feature_status_provider_impl.h
index 8ca0e79..d7a6ed1 100644
--- a/chromeos/ash/components/phonehub/feature_status_provider_impl.h
+++ b/chromeos/ash/components/phonehub/feature_status_provider_impl.h
@@ -9,6 +9,7 @@
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "chromeos/ash/components/phonehub/feature_status_provider.h"
+#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/services/device_sync/public/cpp/device_sync_client.h"
 #include "chromeos/ash/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
 #include "chromeos/ash/services/secure_channel/public/cpp/client/connection_manager.h"
@@ -35,7 +36,8 @@
       multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
       secure_channel::ConnectionManager* connection_manager,
       session_manager::SessionManager* session_manager,
-      chromeos::PowerManagerClient* power_manager_client);
+      chromeos::PowerManagerClient* power_manager_client,
+      PhoneHubStructuredMetricsLogger* phone_hub_structured_metrics_logger);
   ~FeatureStatusProviderImpl() override;
 
  private:
@@ -85,6 +87,7 @@
   raw_ptr<secure_channel::ConnectionManager> connection_manager_;
   raw_ptr<session_manager::SessionManager> session_manager_;
   raw_ptr<chromeos::PowerManagerClient> power_manager_client_;
+  raw_ptr<PhoneHubStructuredMetricsLogger> phone_hub_structured_metrics_logger_;
 
   scoped_refptr<device::BluetoothAdapter> bluetooth_adapter_;
   std::optional<FeatureStatus> status_;
diff --git a/chromeos/ash/components/phonehub/feature_status_provider_impl_unittest.cc b/chromeos/ash/components/phonehub/feature_status_provider_impl_unittest.cc
index c756c69b..048eed51 100644
--- a/chromeos/ash/components/phonehub/feature_status_provider_impl_unittest.cc
+++ b/chromeos/ash/components/phonehub/feature_status_provider_impl_unittest.cc
@@ -11,11 +11,13 @@
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "chromeos/ash/components/multidevice/remote_device_test_util.h"
+#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/services/device_sync/public/cpp/fake_device_sync_client.h"
 #include "chromeos/ash/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h"
 #include "chromeos/ash/services/secure_channel/public/cpp/client/fake_connection_manager.h"
 #include "chromeos/dbus/power/fake_power_manager_client.h"
 #include "chromeos/dbus/power/power_manager_client.h"
+#include "components/prefs/testing_pref_service.h"
 #include "components/session_manager/core/session_manager.h"
 #include "device/bluetooth/bluetooth_adapter_factory.h"
 #include "device/bluetooth/test/mock_bluetooth_adapter.h"
@@ -140,10 +142,14 @@
     session_manager_ = std::make_unique<session_manager::SessionManager>();
     fake_power_manager_client_ =
         std::make_unique<chromeos::FakePowerManagerClient>();
+    PhoneHubStructuredMetricsLogger::RegisterPrefs(pref_service_.registry());
+    phone_hub_structured_metrics_logger_ =
+        std::make_unique<PhoneHubStructuredMetricsLogger>(&pref_service_);
     provider_ = std::make_unique<FeatureStatusProviderImpl>(
         &fake_device_sync_client_, &fake_multidevice_setup_client_,
         &fake_connection_manager_, session_manager_.get(),
-        fake_power_manager_client_.get());
+        fake_power_manager_client_.get(),
+        phone_hub_structured_metrics_logger_.get());
     provider_->AddObserver(&fake_observer_);
   }
 
@@ -254,6 +260,9 @@
   bool is_adapter_present_ = true;
   bool is_adapter_powered_ = true;
 
+  TestingPrefServiceSimple pref_service_;
+  std::unique_ptr<PhoneHubStructuredMetricsLogger>
+      phone_hub_structured_metrics_logger_;
   FakeObserver fake_observer_;
   std::unique_ptr<session_manager::SessionManager> session_manager_;
   std::unique_ptr<chromeos::FakePowerManagerClient> fake_power_manager_client_;
diff --git a/chromeos/ash/components/phonehub/message_receiver_unittest.cc b/chromeos/ash/components/phonehub/message_receiver_unittest.cc
index d3a57b38..c22891f 100644
--- a/chromeos/ash/components/phonehub/message_receiver_unittest.cc
+++ b/chromeos/ash/components/phonehub/message_receiver_unittest.cc
@@ -2,17 +2,19 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chromeos/ash/components/phonehub/message_receiver_impl.h"
-
 #include <netinet/in.h>
+
 #include <memory>
 
 #include "ash/constants/ash_features.h"
 #include "base/strings/strcat.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "chromeos/ash/components/phonehub/message_receiver_impl.h"
 #include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
 #include "chromeos/ash/services/secure_channel/public/cpp/client/fake_connection_manager.h"
+#include "components/prefs/testing_pref_service.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace ash::phonehub {
@@ -192,14 +194,14 @@
  protected:
   MessageReceiverImplTest()
       : fake_connection_manager_(
-            std::make_unique<secure_channel::FakeConnectionManager>()),
-        phone_hub_structured_metrics_logger_(
-            std::make_unique<PhoneHubStructuredMetricsLogger>()) {}
+            std::make_unique<secure_channel::FakeConnectionManager>()) {}
   MessageReceiverImplTest(const MessageReceiverImplTest&) = delete;
   MessageReceiverImplTest& operator=(const MessageReceiverImplTest&) = delete;
   ~MessageReceiverImplTest() override = default;
 
   void SetUp() override {
+    phone_hub_structured_metrics_logger_ =
+        std::make_unique<PhoneHubStructuredMetricsLogger>(&pref_service_);
     message_receiver_ = std::make_unique<MessageReceiverImpl>(
         fake_connection_manager_.get(),
         phone_hub_structured_metrics_logger_.get());
@@ -280,6 +282,8 @@
     return fake_observer_.last_app_list_incremental_update();
   }
 
+  base::test::TaskEnvironment task_environment_;
+  TestingPrefServiceSimple pref_service_;
   FakeObserver fake_observer_;
   std::unique_ptr<secure_channel::FakeConnectionManager>
       fake_connection_manager_;
diff --git a/chromeos/ash/components/phonehub/message_sender_impl.cc b/chromeos/ash/components/phonehub/message_sender_impl.cc
index 44c6684e..2c53aad 100644
--- a/chromeos/ash/components/phonehub/message_sender_impl.cc
+++ b/chromeos/ash/components/phonehub/message_sender_impl.cc
@@ -59,6 +59,7 @@
   proto::CrosState request;
   request.set_notification_setting(is_notification_enabled);
   request.set_camera_roll_setting(is_camera_roll_enabled);
+  phone_hub_structured_metrics_logger_->SetChromebookInfo(request);
 
   if (attestation_certs != nullptr) {
     proto::AttestationData* attestation_data =
diff --git a/chromeos/ash/components/phonehub/message_sender_unittest.cc b/chromeos/ash/components/phonehub/message_sender_unittest.cc
index 73fbc82..967b57cb 100644
--- a/chromeos/ash/components/phonehub/message_sender_unittest.cc
+++ b/chromeos/ash/components/phonehub/message_sender_unittest.cc
@@ -2,21 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chromeos/ash/components/phonehub/message_sender_impl.h"
-
 #include <netinet/in.h>
 #include <stdint.h>
+
 #include <memory>
 #include <string>
 
 #include "ash/constants/ash_features.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/test/task_environment.h"
 #include "chromeos/ash/components/phonehub/fake_feature_status_provider.h"
+#include "chromeos/ash/components/phonehub/message_sender_impl.h"
 #include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/components/phonehub/phone_hub_ui_readiness_recorder.h"
 #include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
 #include "chromeos/ash/services/secure_channel/public/cpp/client/fake_connection_manager.h"
+#include "components/prefs/testing_pref_service.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace ash {
@@ -38,8 +40,9 @@
         std::make_unique<PhoneHubUiReadinessRecorder>(
             fake_feature_status_provider_.get(),
             fake_connection_manager_.get());
+    PhoneHubStructuredMetricsLogger::RegisterPrefs(pref_service_.registry());
     phone_hub_structured_metrics_logger_ =
-        std::make_unique<PhoneHubStructuredMetricsLogger>();
+        std::make_unique<PhoneHubStructuredMetricsLogger>(&pref_service_);
     message_sender_ = std::make_unique<MessageSenderImpl>(
         fake_connection_manager_.get(), phone_hub_ui_readiness_recorder_.get(),
         phone_hub_structured_metrics_logger_.get());
@@ -68,6 +71,8 @@
     EXPECT_EQ(expected_proto_message, actual_proto_message);
   }
 
+  base::test::TaskEnvironment task_environment_;
+  TestingPrefServiceSimple pref_service_;
   std::unique_ptr<secure_channel::FakeConnectionManager>
       fake_connection_manager_;
   std::unique_ptr<FakeFeatureStatusProvider> fake_feature_status_provider_;
@@ -84,6 +89,7 @@
   request.set_camera_roll_setting(proto::CameraRollSetting::CAMERA_ROLL_OFF);
   request.set_allocated_attestation_data(nullptr);
   request.set_should_provide_eche_status(true);
+  phone_hub_structured_metrics_logger_->SetChromebookInfo(request);
   message_sender_->SendCrosState(/*notification_enabled=*/true,
                                  /*camera_roll_enabled=*/false,
                                  /*certs=*/nullptr);
@@ -100,6 +106,7 @@
   request.mutable_attestation_data()->set_type(
       proto::AttestationData::CROS_SOFT_BIND_CERT_CHAIN);
   request.mutable_attestation_data()->add_certificates("certificate");
+  phone_hub_structured_metrics_logger_->SetChromebookInfo(request);
 
   std::vector<std::string> certificates = {"certificate"};
 
diff --git a/chromeos/ash/components/phonehub/phone_hub_manager_impl.cc b/chromeos/ash/components/phonehub/phone_hub_manager_impl.cc
index a1aa627..4b1240d 100644
--- a/chromeos/ash/components/phonehub/phone_hub_manager_impl.cc
+++ b/chromeos/ash/components/phonehub/phone_hub_manager_impl.cc
@@ -70,7 +70,7 @@
         attestation_certificate_generator)
     : icon_decoder_(std::make_unique<IconDecoderImpl>()),
       phone_hub_structured_metrics_logger_(
-          std::make_unique<PhoneHubStructuredMetricsLogger>()),
+          std::make_unique<PhoneHubStructuredMetricsLogger>(pref_service)),
       connection_manager_(
           std::make_unique<secure_channel::ConnectionManagerImpl>(
               multidevice_setup_client,
@@ -84,7 +84,8 @@
           multidevice_setup_client,
           connection_manager_.get(),
           session_manager::SessionManager::Get(),
-          chromeos::PowerManagerClient::Get())),
+          chromeos::PowerManagerClient::Get(),
+          phone_hub_structured_metrics_logger_.get())),
       user_action_recorder_(std::make_unique<UserActionRecorderImpl>(
           feature_status_provider_.get())),
       phone_hub_ui_readiness_recorder_(
@@ -166,7 +167,8 @@
           app_stream_manager_.get(),
           app_stream_launcher_data_model_.get(),
           icon_decoder_.get(),
-          phone_hub_ui_readiness_recorder_.get())),
+          phone_hub_ui_readiness_recorder_.get(),
+          phone_hub_structured_metrics_logger_.get())),
       tether_controller_(
           std::make_unique<TetherControllerImpl>(phone_model_.get(),
                                                  user_action_recorder_.get(),
diff --git a/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.cc b/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.cc
index 201fad7..736b464 100644
--- a/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.cc
+++ b/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.cc
@@ -4,44 +4,301 @@
 
 #include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 
-#include "base/notreached.h"
+#include <string>
+
+#include "ash/public/cpp/network_config_service.h"
+#include "base/feature_list.h"
+#include "base/functional/bind.h"
+#include "base/i18n/rtl.h"
+#include "base/system/sys_info.h"
+#include "base/uuid.h"
+#include "chromeos/ash/components/phonehub/pref_names.h"
+#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
+#include "components/metrics/structured/structured_metrics_features.h"
+#include "crypto/sha2.h"
+#include "device/bluetooth/floss/floss_features.h"
 
 namespace ash::phonehub {
 
-PhoneHubStructuredMetricsLogger::PhoneHubStructuredMetricsLogger() = default;
+void PhoneHubStructuredMetricsLogger::RegisterPrefs(
+    PrefRegistrySimple* registry) {
+  registry->RegisterStringPref(prefs::kPhoneManufacturer, std::string());
+  registry->RegisterStringPref(prefs::kPhoneModel, std::string());
+  registry->RegisterStringPref(prefs::kPhoneLocale, std::string());
+  registry->RegisterStringPref(prefs::kPhonePseudonymousId, std::string());
+  registry->RegisterInt64Pref(prefs::kPhoneAmbientApkVersion, 0);
+  registry->RegisterInt64Pref(prefs::kPhoneGmsCoreVersion, 0);
+  registry->RegisterIntegerPref(prefs::kPhoneAndroidVersion, 0);
+  registry->RegisterIntegerPref(
+      prefs::kPhoneProfileType,
+      -1);  // Make default value different from normal default profile type 0
+  registry->RegisterTimePref(prefs::kPhoneInfoLastUpdatedTime, base::Time());
+  registry->RegisterStringPref(prefs::kChromebookPseudonymousId, std::string());
+  registry->RegisterTimePref(prefs::kPseudonymousIdRotationDate, base::Time());
+}
+
+PhoneHubStructuredMetricsLogger::PhoneHubStructuredMetricsLogger(
+    PrefService* pref_service)
+    : bluetooth_stack_(floss::features::IsFlossEnabled()
+                           ? BluetoothStack::kFloss
+                           : BluetoothStack::kBlueZ),
+      chromebook_locale_(base::i18n::GetConfiguredLocale()),
+      pref_service_(pref_service) {
+  ash::GetNetworkConfigService(
+      cros_network_config_.BindNewPipeAndPassReceiver());
+}
 PhoneHubStructuredMetricsLogger::~PhoneHubStructuredMetricsLogger() = default;
 
 void PhoneHubStructuredMetricsLogger::LogPhoneHubDiscoveryStarted(
     DiscoveryEntryPoint entry_point) {
-  NOTIMPLEMENTED();
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  UpdateIdentifiersIfNeeded();
 }
 
 void PhoneHubStructuredMetricsLogger::LogDiscoveryAttempt(
     secure_channel::mojom::DiscoveryResult result,
     std::optional<secure_channel::mojom::DiscoveryErrorCode> error_code) {
-  NOTIMPLEMENTED();
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  UpdateIdentifiersIfNeeded();
 }
 
 void PhoneHubStructuredMetricsLogger::LogNearbyConnectionState(
     secure_channel::mojom::NearbyConnectionStep step,
     secure_channel::mojom::NearbyConnectionStepResult result) {
-  NOTIMPLEMENTED();
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  UpdateIdentifiersIfNeeded();
 }
 
 void PhoneHubStructuredMetricsLogger::LogSecureChannelState(
     secure_channel::mojom::SecureChannelState state) {
-  NOTIMPLEMENTED();
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  UpdateIdentifiersIfNeeded();
 }
 
 void PhoneHubStructuredMetricsLogger::LogPhoneHubMessageEvent(
     proto::MessageType message_type,
     PhoneHubMessageDirection message_direction) {
-  NOTIMPLEMENTED();
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  UpdateIdentifiersIfNeeded();
 }
 
 void PhoneHubStructuredMetricsLogger::LogPhoneHubUiStateUpdated(
     PhoneHubUiState ui_state) {
-  std::string state;
-  NOTIMPLEMENTED();
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  UpdateIdentifiersIfNeeded();
+}
+
+void PhoneHubStructuredMetricsLogger::ProcessPhoneInformation(
+    const proto::PhoneProperties& phone_properties) {
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  if (phone_properties.has_pseudonymous_id_next_rotation_date()) {
+    base::Time pseudonymous_id_rotation_date =
+        base::Time::FromMillisecondsSinceUnixEpoch(
+            phone_properties.pseudonymous_id_next_rotation_date());
+    if (pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null() ||
+        pseudonymous_id_rotation_date <
+            pref_service_->GetTime(prefs::kPseudonymousIdRotationDate)) {
+      pref_service_->SetTime(prefs::kPseudonymousIdRotationDate,
+                             pseudonymous_id_rotation_date);
+    }
+  }
+  if (phone_properties.has_phone_pseudonymous_id()) {
+    if (pref_service_->GetString(prefs::kPhonePseudonymousId).empty() ||
+        pref_service_->GetString(prefs::kPhonePseudonymousId) !=
+            phone_properties.phone_pseudonymous_id()) {
+      pref_service_->SetString(prefs::kPhonePseudonymousId,
+                               phone_properties.phone_pseudonymous_id());
+    }
+  }
+  if (phone_properties.has_phone_manufacturer()) {
+    if (pref_service_->GetString(prefs::kPhoneManufacturer).empty() ||
+        pref_service_->GetString(prefs::kPhoneManufacturer) !=
+            phone_properties.phone_manufacturer()) {
+      pref_service_->SetString(prefs::kPhoneManufacturer,
+                               phone_properties.phone_manufacturer());
+    }
+  }
+  if (phone_properties.has_phone_model()) {
+    if (pref_service_->GetString(prefs::kPhoneModel).empty() ||
+        pref_service_->GetString(prefs::kPhoneModel) !=
+            phone_properties.phone_model()) {
+      pref_service_->SetString(prefs::kPhoneModel,
+                               phone_properties.phone_model());
+    }
+  }
+
+  if (pref_service_->GetInt64(prefs::kPhoneGmsCoreVersion) !=
+      phone_properties.gmscore_version()) {
+    pref_service_->SetInt64(prefs::kPhoneGmsCoreVersion,
+                            phone_properties.gmscore_version());
+  }
+
+  if (pref_service_->GetInteger(prefs::kPhoneAndroidVersion) !=
+      phone_properties.android_version()) {
+    pref_service_->SetInteger(prefs::kPhoneAndroidVersion,
+                              phone_properties.android_version());
+  }
+
+  if (phone_properties.has_ambient_version() &&
+      pref_service_->GetInt64(prefs::kPhoneAmbientApkVersion) !=
+          phone_properties.ambient_version()) {
+    pref_service_->SetInt64(prefs::kPhoneAmbientApkVersion,
+                            phone_properties.ambient_version());
+  }
+
+  pref_service_->SetTime(prefs::kPhoneInfoLastUpdatedTime,
+                         base::Time::NowFromSystemTime());
+
+  if (phone_properties.has_network_status()) {
+    phone_network_status_ = phone_properties.network_status();
+    if (phone_network_status_ == proto::NetworkStatus::CELLULAR) {
+      network_state_ = NetworkState::kPhoneOnCellular;
+    } else if (phone_network_status_ == proto::NetworkStatus::WIFI) {
+      if (phone_properties.has_ssid()) {
+        phone_network_ssid_ = phone_properties.ssid();
+      }
+      cros_network_config_->GetNetworkStateList(
+          chromeos::network_config::mojom::NetworkFilter::New(
+              chromeos::network_config::mojom::FilterType::kActive,
+              chromeos::network_config::mojom::NetworkType::kWiFi,
+              chromeos::network_config::mojom::kNoLimit),
+          base::BindOnce(
+              &PhoneHubStructuredMetricsLogger::OnNetworkStateListFetched,
+              base::Unretained(this)));
+    } else {
+      network_state_ = NetworkState::kDifferentNetwork;
+    }
+  }
+
+  if (pref_service_->GetInteger(prefs::kPhoneProfileType) !=
+      phone_properties.profile_type()) {
+    pref_service_->SetInteger(prefs::kPhoneProfileType,
+                              phone_properties.profile_type());
+  }
+
+  if (phone_properties.has_locale()) {
+    if (pref_service_->GetString(prefs::kPhoneLocale).empty() ||
+        pref_service_->GetString(prefs::kPhoneLocale) !=
+            phone_properties.locale()) {
+      pref_service_->SetString(prefs::kPhoneLocale, phone_properties.locale());
+    }
+  }
+}
+
+void PhoneHubStructuredMetricsLogger::UpdateIdentifiersIfNeeded() {
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  if (pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null() ||
+      pref_service_->GetTime(prefs::kPseudonymousIdRotationDate) <=
+          base::Time::NowFromSystemTime()) {
+    ResetCachedInformation();
+  }
+  if (pref_service_->GetString(prefs::kChromebookPseudonymousId).empty()) {
+    pref_service_->SetString(
+        prefs::kChromebookPseudonymousId,
+        base::Uuid::GenerateRandomV4().AsLowercaseString());
+  }
+  if (pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null() ||
+      pref_service_->GetTime(prefs::kPseudonymousIdRotationDate) >
+          (base::Time::NowFromSystemTime() +
+           kMaxStructuredMetricsPseudonymousIdDays)) {
+    pref_service_->SetTime(prefs::kPseudonymousIdRotationDate,
+                           base::Time::NowFromSystemTime() +
+                               kMaxStructuredMetricsPseudonymousIdDays);
+  }
+  if (phone_hub_session_id_.empty()) {
+    phone_hub_session_id_ = base::Uuid::GenerateRandomV4().AsLowercaseString();
+  }
+}
+
+void PhoneHubStructuredMetricsLogger::ResetCachedInformation() {
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  phone_network_status_ = std::nullopt;
+  pref_service_->SetInteger(prefs::kPhoneProfileType, -1);
+  phone_network_ssid_ = std::nullopt;
+  pref_service_->SetInt64(prefs::kPhoneGmsCoreVersion, 0);
+  pref_service_->SetInteger(prefs::kPhoneAndroidVersion, 0);
+  pref_service_->SetInt64(prefs::kPhoneAmbientApkVersion, 0);
+  pref_service_->SetTime(prefs::kPhoneInfoLastUpdatedTime, base::Time());
+  pref_service_->SetString(prefs::kPhonePseudonymousId, std::string());
+  pref_service_->SetString(prefs::kPhoneManufacturer, std::string());
+  pref_service_->SetString(prefs::kPhoneModel, std::string());
+  pref_service_->SetString(prefs::kPhoneLocale, std::string());
+
+  network_state_ = NetworkState::kUnknown;
+  pref_service_->SetString(prefs::kChromebookPseudonymousId, std::string());
+  pref_service_->SetTime(prefs::kPseudonymousIdRotationDate, base::Time());
+
+  ResetSessionId();
+}
+
+void PhoneHubStructuredMetricsLogger::ResetSessionId() {
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+  phone_hub_session_id_ = std::string();
+}
+
+void PhoneHubStructuredMetricsLogger::SetChromebookInfo(
+    proto::CrosState& cros_state_message) {
+  if (!base::FeatureList::IsEnabled(
+          metrics::structured::kPhoneHubStructuredMetrics)) {
+    return;
+  }
+
+  if (!phone_hub_session_id_.empty()) {
+    cros_state_message.set_phone_hub_session_id(phone_hub_session_id_);
+  }
+  if (!pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null()) {
+    cros_state_message.set_pseudonymous_id_next_rotation_date(
+        pref_service_->GetTime(prefs::kPseudonymousIdRotationDate)
+            .InMillisecondsSinceUnixEpoch());
+  }
+}
+
+void PhoneHubStructuredMetricsLogger::OnNetworkStateListFetched(
+    std::vector<chromeos::network_config::mojom::NetworkStatePropertiesPtr>
+        networks) {
+  for (const auto& network : networks) {
+    if (network->type == chromeos::network_config::mojom::NetworkType::kWiFi) {
+      std::string hashed_wifi_ssid =
+          crypto::SHA256HashString(network->type_state->get_wifi()->ssid);
+      if (phone_network_ssid_.has_value() &&
+          phone_network_ssid_.value() == hashed_wifi_ssid) {
+        network_state_ = NetworkState::kSameNetwork;
+      } else {
+        network_state_ = NetworkState::kDifferentNetwork;
+      }
+      return;
+    }
+  }
+  network_state_ = NetworkState::kDifferentNetwork;
 }
 }  // namespace ash::phonehub
diff --git a/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h b/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h
index 1e9dd116..dfa182c 100644
--- a/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h
+++ b/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h
@@ -7,13 +7,23 @@
 
 #include <optional>
 
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/time/time.h"
 #include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
 #include "chromeos/ash/services/secure_channel/public/cpp/client/secure_channel_structured_metrics_logger.h"
 #include "chromeos/ash/services/secure_channel/public/mojom/nearby_connector.mojom-shared.h"
 #include "chromeos/ash/services/secure_channel/public/mojom/secure_channel.mojom-shared.h"
 #include "chromeos/ash/services/secure_channel/public/mojom/secure_channel.mojom.h"
+#include "chromeos/services/network_config/public/mojom/cros_network_config.mojom.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
 #include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
 
+namespace {
+base::TimeDelta kMaxStructuredMetricsPseudonymousIdDays = base::Days(90);
+}
 namespace ash::phonehub {
 
 enum class DiscoveryEntryPoint {
@@ -42,10 +52,21 @@
   kConnected = 2
 };
 
+enum class BluetoothStack { kBlueZ = 0, kFloss = 1 };
+
+enum class NetworkState {
+  kUnknown = 0,
+  kSameNetwork = 1,
+  kDifferentNetwork = 2,
+  kPhoneOnCellular = 3
+};
+
 class PhoneHubStructuredMetricsLogger
     : public ash::secure_channel::SecureChannelStructuredMetricsLogger {
  public:
-  PhoneHubStructuredMetricsLogger();
+  static void RegisterPrefs(PrefRegistrySimple* registry);
+
+  explicit PhoneHubStructuredMetricsLogger(PrefService* pref_service);
   ~PhoneHubStructuredMetricsLogger() override;
 
   PhoneHubStructuredMetricsLogger(const PhoneHubStructuredMetricsLogger&) =
@@ -69,6 +90,41 @@
   void LogPhoneHubMessageEvent(proto::MessageType message_type,
                                PhoneHubMessageDirection message_direction);
   void LogPhoneHubUiStateUpdated(PhoneHubUiState ui_state);
+
+  void ProcessPhoneInformation(const proto::PhoneProperties& phone_properties);
+
+  void ResetCachedInformation();
+
+  void ResetSessionId();
+
+  void SetChromebookInfo(proto::CrosState& cros_state_message);
+
+ private:
+  FRIEND_TEST_ALL_PREFIXES(PhoneHubStructuredMetricsLoggerTest,
+                           ProcessPhoneInformation_MissingFields);
+  FRIEND_TEST_ALL_PREFIXES(PhoneHubStructuredMetricsLoggerTest,
+                           ProcessPhoneInformation_AllFields);
+  FRIEND_TEST_ALL_PREFIXES(PhoneHubStructuredMetricsLoggerTest, LogEvents);
+
+  void UpdateIdentifiersIfNeeded();
+  void OnNetworkStateListFetched(
+      std::vector<chromeos::network_config::mojom::NetworkStatePropertiesPtr>
+          networks);
+
+  // Phone information
+  std::optional<proto::NetworkStatus> phone_network_status_;
+  std::optional<std::string> phone_network_ssid_;
+
+  // Chromebook information
+  BluetoothStack bluetooth_stack_;
+  NetworkState network_state_ = NetworkState::kUnknown;
+  std::string chromebook_locale_;
+
+  std::string phone_hub_session_id_;
+
+  mojo::Remote<chromeos::network_config::mojom::CrosNetworkConfig>
+      cros_network_config_;
+  raw_ptr<PrefService> pref_service_;
 };
 
 }  // namespace ash::phonehub
diff --git a/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger_unittest.cc b/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger_unittest.cc
new file mode 100644
index 0000000..9959b4b
--- /dev/null
+++ b/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger_unittest.cc
@@ -0,0 +1,241 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
+
+#include <memory>
+#include <optional>
+
+#include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "base/time/time.h"
+#include "chromeos/ash/components/phonehub/pref_names.h"
+#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
+#include "chromeos/ash/services/network_config/public/cpp/cros_network_config_test_helper.h"
+#include "chromeos/ash/services/secure_channel/public/mojom/nearby_connector.mojom-shared.h"
+#include "components/metrics/structured/structured_metrics_features.h"
+#include "components/prefs/testing_pref_service.h"
+#include "crypto/sha2.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace ash::phonehub {
+
+class PhoneHubStructuredMetricsLoggerTest : public testing::Test {
+ protected:
+  PhoneHubStructuredMetricsLoggerTest() = default;
+  PhoneHubStructuredMetricsLoggerTest(
+      const PhoneHubStructuredMetricsLoggerTest&) = delete;
+  PhoneHubStructuredMetricsLoggerTest& operator=(
+      const PhoneHubStructuredMetricsLoggerTest&) = delete;
+  ~PhoneHubStructuredMetricsLoggerTest() override = default;
+
+  void SetUp() override {
+    PhoneHubStructuredMetricsLogger::RegisterPrefs(pref_service_.registry());
+    feature_list_.InitWithFeatures(
+        {metrics::structured::kPhoneHubStructuredMetrics}, {});
+    logger_ = std::make_unique<PhoneHubStructuredMetricsLogger>(&pref_service_);
+  }
+
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  std::unique_ptr<PhoneHubStructuredMetricsLogger> logger_;
+  TestingPrefServiceSimple pref_service_;
+  network_config::CrosNetworkConfigTestHelper network_config_test_helper_;
+  base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(PhoneHubStructuredMetricsLoggerTest,
+       ProcessPhoneInformation_MissingFields) {
+  auto phone_property = proto::PhoneProperties();
+  phone_property.set_android_version(32);
+  phone_property.set_gmscore_version(11111111);
+  phone_property.set_profile_type(proto::ProfileType::DEFAULT_PROFILE);
+
+  logger_->ProcessPhoneInformation(phone_property);
+
+  EXPECT_TRUE(
+      pref_service_.GetTime(prefs::kPseudonymousIdRotationDate).is_null());
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhonePseudonymousId).empty());
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhoneManufacturer).empty());
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhoneModel).empty());
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhoneLocale).empty());
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneGmsCoreVersion), 11111111);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneAndroidVersion), 32);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneProfileType),
+            proto::ProfileType::DEFAULT_PROFILE);
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneAmbientApkVersion), 0);
+  EXPECT_EQ(logger_->network_state_, NetworkState::kUnknown);
+}
+
+TEST_F(PhoneHubStructuredMetricsLoggerTest, ProcessPhoneInformation_AllFields) {
+  int id_rotation_time_in_milliseconds = 123456789;
+  int phone_android_version = 32;
+  int gms_version = 11111111;
+  int ambient_version = 1234567;
+
+  auto phone_property = proto::PhoneProperties();
+  phone_property.set_android_version(phone_android_version);
+  phone_property.set_gmscore_version(gms_version);
+  phone_property.set_profile_type(proto::ProfileType::DEFAULT_PROFILE);
+  phone_property.set_ambient_version(ambient_version);
+  phone_property.set_phone_pseudonymous_id("test_uuid");
+  phone_property.set_pseudonymous_id_next_rotation_date(
+      id_rotation_time_in_milliseconds);
+  phone_property.set_locale("US");
+  phone_property.set_phone_manufacturer("google");
+  phone_property.set_phone_model("pixle 7");
+  phone_property.set_network_status(proto::NetworkStatus::CELLULAR);
+
+  logger_->ProcessPhoneInformation(phone_property);
+
+  EXPECT_EQ(pref_service_.GetTime(prefs::kPseudonymousIdRotationDate),
+            base::Time::FromMillisecondsSinceUnixEpoch(
+                id_rotation_time_in_milliseconds));
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhonePseudonymousId), "test_uuid");
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhoneManufacturer), "google");
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhoneModel), "pixle 7");
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhoneLocale), "US");
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneGmsCoreVersion), gms_version);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneAndroidVersion),
+            phone_android_version);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneProfileType),
+            proto::ProfileType::DEFAULT_PROFILE);
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneAmbientApkVersion),
+            ambient_version);
+  EXPECT_EQ(logger_->network_state_, NetworkState::kPhoneOnCellular);
+
+  // Simulate phone info update
+  int update_id_rotation_time_in_milliseconds = 12345678;
+  int update_phone_android_version = 34;
+  int update_gms_version = 111111112;
+  int update_ambient_version = 123456777;
+
+  phone_property.set_android_version(update_phone_android_version);
+  phone_property.set_gmscore_version(update_gms_version);
+  phone_property.set_profile_type(proto::ProfileType::DEFAULT_PROFILE);
+  phone_property.set_ambient_version(update_ambient_version);
+  phone_property.set_phone_pseudonymous_id("test_uuid_2");
+  phone_property.set_pseudonymous_id_next_rotation_date(
+      update_id_rotation_time_in_milliseconds);
+  phone_property.set_locale("us");
+  phone_property.set_phone_manufacturer("Google");
+  phone_property.set_phone_model("Pixle 7");
+  phone_property.set_network_status(proto::NetworkStatus::WIFI);
+  phone_property.set_ssid(crypto::SHA256HashString("WIFI1"));
+  phone_property.set_profile_type(proto::ProfileType::WORK_PROFILE);
+
+  auto wifi_path =
+      network_config_test_helper_.network_state_helper().ConfigureService(
+          R"({"GUID": "WIFI1_guid", "Type": "wifi", "SSID": "WIFI1",
+             "State": "ready", "Strength": 100,
+            "Connectable": true})");
+  base::RunLoop().RunUntilIdle();
+  logger_->ProcessPhoneInformation(phone_property);
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(pref_service_.GetTime(prefs::kPseudonymousIdRotationDate),
+            base::Time::FromMillisecondsSinceUnixEpoch(
+                update_id_rotation_time_in_milliseconds));
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhonePseudonymousId),
+            "test_uuid_2");
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhoneManufacturer), "Google");
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhoneModel), "Pixle 7");
+  EXPECT_EQ(pref_service_.GetString(prefs::kPhoneLocale), "us");
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneGmsCoreVersion),
+            update_gms_version);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneAndroidVersion),
+            update_phone_android_version);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneProfileType),
+            proto::ProfileType::WORK_PROFILE);
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneAmbientApkVersion),
+            update_ambient_version);
+  EXPECT_EQ(logger_->network_state_, NetworkState::kSameNetwork);
+
+  // Simulate phone wifi change
+  phone_property.set_ssid(crypto::SHA256HashString("WIFI2"));
+  logger_->ProcessPhoneInformation(phone_property);
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(logger_->network_state_, NetworkState::kDifferentNetwork);
+}
+
+TEST_F(PhoneHubStructuredMetricsLoggerTest, LogEvents) {
+  logger_->LogPhoneHubDiscoveryStarted(
+      DiscoveryEntryPoint::kPhoneHubBubbleOpen);
+
+  int id_rotation_time_in_milliseconds =
+      base::Time::Now().InMillisecondsSinceUnixEpoch() +
+      5 * 24 * 60 * 60 * 1000;
+  int phone_android_version = 32;
+  int gms_version = 11111111;
+  int ambient_version = 1234567;
+  auto phone_property = proto::PhoneProperties();
+  phone_property.set_android_version(phone_android_version);
+  phone_property.set_gmscore_version(gms_version);
+  phone_property.set_profile_type(proto::ProfileType::DEFAULT_PROFILE);
+  phone_property.set_ambient_version(ambient_version);
+  phone_property.set_phone_pseudonymous_id("test_uuid");
+  phone_property.set_pseudonymous_id_next_rotation_date(
+      id_rotation_time_in_milliseconds);
+  phone_property.set_locale("US");
+  phone_property.set_phone_manufacturer("google");
+  phone_property.set_phone_model("pixle 7");
+  phone_property.set_network_status(proto::NetworkStatus::CELLULAR);
+
+  logger_->ProcessPhoneInformation(phone_property);
+
+  std::string chromebook_pseudonymouse_id =
+      pref_service_.GetString(prefs::kChromebookPseudonymousId);
+  EXPECT_FALSE(chromebook_pseudonymouse_id.empty());
+  std::string phone_hub_session_id = logger_->phone_hub_session_id_;
+  EXPECT_FALSE(phone_hub_session_id.empty());
+
+  logger_->LogDiscoveryAttempt(secure_channel::mojom::DiscoveryResult::kSuccess,
+                               std::nullopt);
+  EXPECT_EQ(chromebook_pseudonymouse_id,
+            pref_service_.GetString(prefs::kChromebookPseudonymousId));
+  EXPECT_EQ(phone_hub_session_id, logger_->phone_hub_session_id_);
+
+  logger_->LogNearbyConnectionState(
+      secure_channel::mojom::NearbyConnectionStep::kDisconnectionStarted,
+      secure_channel::mojom::NearbyConnectionStepResult::kSuccess);
+  EXPECT_EQ(chromebook_pseudonymouse_id,
+            pref_service_.GetString(prefs::kChromebookPseudonymousId));
+  EXPECT_EQ(phone_hub_session_id, logger_->phone_hub_session_id_);
+
+  logger_->LogSecureChannelState(
+      secure_channel::mojom::SecureChannelState::kAuthenticationSuccess);
+  EXPECT_EQ(chromebook_pseudonymouse_id,
+            pref_service_.GetString(prefs::kChromebookPseudonymousId));
+  EXPECT_EQ(phone_hub_session_id, logger_->phone_hub_session_id_);
+
+  logger_->LogPhoneHubUiStateUpdated(PhoneHubUiState::kConnected);
+  EXPECT_EQ(chromebook_pseudonymouse_id,
+            pref_service_.GetString(prefs::kChromebookPseudonymousId));
+  EXPECT_EQ(phone_hub_session_id, logger_->phone_hub_session_id_);
+
+  logger_->LogPhoneHubMessageEvent(
+      proto::MessageType::PHONE_STATUS_SNAPSHOT,
+      PhoneHubMessageDirection::kPhoneToChromebook);
+  EXPECT_EQ(chromebook_pseudonymouse_id,
+            pref_service_.GetString(prefs::kChromebookPseudonymousId));
+  EXPECT_EQ(phone_hub_session_id, logger_->phone_hub_session_id_);
+
+  task_environment_.FastForwardBy(base::Days(6));
+  logger_->LogDiscoveryAttempt(secure_channel::mojom::DiscoveryResult::kSuccess,
+                               std::nullopt);
+  EXPECT_FALSE(chromebook_pseudonymouse_id ==
+               pref_service_.GetString(prefs::kChromebookPseudonymousId));
+  EXPECT_FALSE(phone_hub_session_id == logger_->phone_hub_session_id_);
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhonePseudonymousId).empty());
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhoneManufacturer).empty());
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhoneModel).empty());
+  EXPECT_TRUE(pref_service_.GetString(prefs::kPhoneLocale).empty());
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneGmsCoreVersion), 0);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneAndroidVersion), 0);
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kPhoneProfileType), -1);
+  EXPECT_EQ(pref_service_.GetInt64(prefs::kPhoneAmbientApkVersion), 0);
+  EXPECT_EQ(logger_->network_state_, NetworkState::kUnknown);
+}
+
+}  // namespace ash::phonehub
diff --git a/chromeos/ash/components/phonehub/phone_status_processor.cc b/chromeos/ash/components/phonehub/phone_status_processor.cc
index c3f7432..daf4a55 100644
--- a/chromeos/ash/components/phonehub/phone_status_processor.cc
+++ b/chromeos/ash/components/phonehub/phone_status_processor.cc
@@ -23,6 +23,7 @@
 #include "chromeos/ash/components/phonehub/multidevice_feature_access_manager.h"
 #include "chromeos/ash/components/phonehub/mutable_phone_model.h"
 #include "chromeos/ash/components/phonehub/notification_processor.h"
+#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/components/phonehub/phone_hub_ui_readiness_recorder.h"
 #include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
 #include "chromeos/ash/components/phonehub/recent_apps_interaction_handler.h"
@@ -231,7 +232,8 @@
     AppStreamManager* app_stream_manager,
     AppStreamLauncherDataModel* app_stream_launcher_data_model,
     IconDecoder* icon_decoder,
-    PhoneHubUiReadinessRecorder* phone_hub_ui_readiness_recorder)
+    PhoneHubUiReadinessRecorder* phone_hub_ui_readiness_recorder,
+    PhoneHubStructuredMetricsLogger* phone_hub_structured_metrics_logger)
     : do_not_disturb_controller_(do_not_disturb_controller),
       feature_status_provider_(feature_status_provider),
       message_receiver_(message_receiver),
@@ -246,7 +248,9 @@
       app_stream_manager_(app_stream_manager),
       app_stream_launcher_data_model_(app_stream_launcher_data_model),
       icon_decoder_(icon_decoder),
-      phone_hub_ui_readiness_recorder_(phone_hub_ui_readiness_recorder) {
+      phone_hub_ui_readiness_recorder_(phone_hub_ui_readiness_recorder),
+      phone_hub_structured_metrics_logger_(
+          phone_hub_structured_metrics_logger) {
   DCHECK(do_not_disturb_controller_);
   DCHECK(feature_status_provider_);
   DCHECK(message_receiver_);
@@ -259,6 +263,7 @@
   DCHECK(app_stream_manager_);
   DCHECK(icon_decoder_);
   DCHECK(phone_hub_ui_readiness_recorder_);
+  DCHECK(phone_hub_structured_metrics_logger_);
 
   message_receiver_->AddObserver(this);
   feature_status_provider_->AddObserver(this);
@@ -304,6 +309,8 @@
 
 void PhoneStatusProcessor::SetReceivedPhoneStatusModelStates(
     const proto::PhoneProperties& phone_properties) {
+  phone_hub_structured_metrics_logger_->ProcessPhoneInformation(
+      phone_properties);
   phone_model_->SetPhoneStatusModel(CreatePhoneStatusModel(phone_properties));
 
   do_not_disturb_controller_->SetDoNotDisturbStateInternal(
diff --git a/chromeos/ash/components/phonehub/phone_status_processor.h b/chromeos/ash/components/phonehub/phone_status_processor.h
index 70dc4ec..5358807 100644
--- a/chromeos/ash/components/phonehub/phone_status_processor.h
+++ b/chromeos/ash/components/phonehub/phone_status_processor.h
@@ -13,6 +13,7 @@
 #include "chromeos/ash/components/phonehub/feature_status_provider.h"
 #include "chromeos/ash/components/phonehub/icon_decoder.h"
 #include "chromeos/ash/components/phonehub/message_receiver.h"
+#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
 #include "chromeos/ash/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
 
@@ -62,7 +63,8 @@
       AppStreamManager* app_stream_manager,
       AppStreamLauncherDataModel* app_stream_launcher_data_model,
       IconDecoder* icon_decoder_,
-      PhoneHubUiReadinessRecorder* phone_hub_ui_readiness_recorder);
+      PhoneHubUiReadinessRecorder* phone_hub_ui_readiness_recorder,
+      PhoneHubStructuredMetricsLogger* phone_hub_structured_metrics_logger);
   ~PhoneStatusProcessor() override;
 
   PhoneStatusProcessor(const PhoneStatusProcessor&) = delete;
@@ -127,6 +129,7 @@
   raw_ptr<AppStreamLauncherDataModel> app_stream_launcher_data_model_;
   raw_ptr<IconDecoder> icon_decoder_;
   raw_ptr<PhoneHubUiReadinessRecorder> phone_hub_ui_readiness_recorder_;
+  raw_ptr<PhoneHubStructuredMetricsLogger> phone_hub_structured_metrics_logger_;
   base::TimeTicks connection_initialized_timestamp_ = base::TimeTicks();
   bool has_received_first_app_list_update_ = false;
 
diff --git a/chromeos/ash/components/phonehub/phone_status_processor_unittest.cc b/chromeos/ash/components/phonehub/phone_status_processor_unittest.cc
index 14246f59..ba04f59 100644
--- a/chromeos/ash/components/phonehub/phone_status_processor_unittest.cc
+++ b/chromeos/ash/components/phonehub/phone_status_processor_unittest.cc
@@ -33,6 +33,7 @@
 #include "chromeos/ash/components/phonehub/mutable_phone_model.h"
 #include "chromeos/ash/components/phonehub/notification_manager.h"
 #include "chromeos/ash/components/phonehub/notification_processor.h"
+#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
 #include "chromeos/ash/components/phonehub/phone_hub_ui_readiness_recorder.h"
 #include "chromeos/ash/components/phonehub/phone_model_test_util.h"
 #include "chromeos/ash/components/phonehub/phone_status_model.h"
@@ -175,6 +176,8 @@
         std::make_unique<PhoneHubUiReadinessRecorder>(
             fake_feature_status_provider_.get(),
             fake_connection_manager_.get());
+    phone_hub_structured_metrics_logger_ =
+        std::make_unique<PhoneHubStructuredMetricsLogger>(&pref_service_);
 
     multidevice_setup::RegisterFeaturePrefs(pref_service_.registry());
   }
@@ -189,7 +192,8 @@
         fake_multidevice_setup_client_.get(), mutable_phone_model_.get(),
         fake_recent_apps_interaction_handler_.get(), &pref_service_,
         &app_stream_manager_, app_stream_launcher_data_model_.get(),
-        icon_decoder_.get(), phone_hub_ui_readiness_recorder_.get());
+        icon_decoder_.get(), phone_hub_ui_readiness_recorder_.get(),
+        phone_hub_structured_metrics_logger_.get());
   }
 
   void InitializeNotificationProto(proto::Notification* notification,
@@ -246,6 +250,8 @@
   std::unique_ptr<secure_channel::FakeConnectionManager>
       fake_connection_manager_;
   std::unique_ptr<PhoneHubUiReadinessRecorder> phone_hub_ui_readiness_recorder_;
+  std::unique_ptr<PhoneHubStructuredMetricsLogger>
+      phone_hub_structured_metrics_logger_;
   raw_ptr<TestDecoderDelegate> decoder_delegate_;
   TestingPrefServiceSimple pref_service_;
   AppStreamManager app_stream_manager_;
diff --git a/chromeos/ash/components/phonehub/pref_names.cc b/chromeos/ash/components/phonehub/pref_names.cc
index 1fa97be..222434d 100644
--- a/chromeos/ash/components/phonehub/pref_names.cc
+++ b/chromeos/ash/components/phonehub/pref_names.cc
@@ -70,6 +70,22 @@
 const char kFeatureSetupRequestSupported[] =
     "cros.phonehub.feature_setup_request_supported";
 
+const char kPhoneManufacturer[] = "cros.phonehub.phone_manufacturer";
+const char kPhoneModel[] = "cros.phonehub.phone_model";
+const char kPhoneLocale[] = "cros.phonehub.phone_locale";
+const char kPhonePseudonymousId[] = "cros.phonehub.phone_pseudonymous_id";
+const char kPhoneAndroidVersion[] = "cros.phonehub.phone_android_version";
+const char kPhoneGmsCoreVersion[] = "cros.phonehub.phone_gms_core_version";
+const char kPhoneAmbientApkVersion[] =
+    "cros.phonehub.phone_ambient_apk_version";
+const char kPhoneProfileType[] = "cros.phonehub.phone_profile_type";
+const char kPhoneInfoLastUpdatedTime[] =
+    "cros.phonehub.phone_info_last_updated_time";
+const char kChromebookPseudonymousId[] =
+    "cros.phonehub.chromebook_pseudonymous_id";
+const char kPseudonymousIdRotationDate[] =
+    "cros.phonehub.pseudonymous_id_rotation_date";
+
 }  // namespace prefs
 }  // namespace phonehub
 }  // namespace ash
diff --git a/chromeos/ash/components/phonehub/pref_names.h b/chromeos/ash/components/phonehub/pref_names.h
index 341645af..94006773 100644
--- a/chromeos/ash/components/phonehub/pref_names.h
+++ b/chromeos/ash/components/phonehub/pref_names.h
@@ -20,6 +20,19 @@
 extern const char kRecentAppsHistory[];
 extern const char kFeatureSetupRequestSupported[];
 
+// Connected phone information used by Phone Hub Structured Metrics
+extern const char kPhoneManufacturer[];
+extern const char kPhoneModel[];
+extern const char kPhoneLocale[];
+extern const char kPhonePseudonymousId[];
+extern const char kPhoneAndroidVersion[];
+extern const char kPhoneGmsCoreVersion[];
+extern const char kPhoneAmbientApkVersion[];
+extern const char kPhoneProfileType[];
+extern const char kPhoneInfoLastUpdatedTime[];
+extern const char kChromebookPseudonymousId[];
+extern const char kPseudonymousIdRotationDate[];
+
 }  // namespace prefs
 }  // namespace phonehub
 }  // namespace ash
diff --git a/chromeos/ash/components/phonehub/proto/phonehub_api.proto b/chromeos/ash/components/phonehub/proto/phonehub_api.proto
index d77cf00..0ac84862 100644
--- a/chromeos/ash/components/phonehub/proto/phonehub_api.proto
+++ b/chromeos/ash/components/phonehub/proto/phonehub_api.proto
@@ -187,6 +187,12 @@
   bool ping_capability_supported = 2;
 }
 
+enum NetworkStatus {
+  DISCONNECTED = 0;
+  CELLULAR = 1;
+  WIFI = 2;
+}
+
 message PhoneProperties {
   int32 battery_percentage = 1;
   ChargingState charging_state = 2;
@@ -224,7 +230,17 @@
 
   FeatureStatus eche_feature_status = 20;
 
-  // Next ID: 21
+  // Used for Phone Hub Structured Metrics
+  optional string phone_manufacturer = 21;
+  optional string phone_model = 22;
+  optional int64 ambient_version = 23;
+  optional NetworkStatus network_status = 24;
+  optional bytes ssid = 25;
+  optional string locale = 26;
+  optional string phone_pseudonymous_id = 27;
+  optional int64 pseudonymous_id_next_rotation_date = 28;
+
+  // Next ID: 29
 }
 
 message UserState {
@@ -274,6 +290,9 @@
   // Request Eche feature status info from the phone through Phone hub.
   bool should_provide_eche_status = 5;
 
+  optional int64 pseudonymous_id_next_rotation_date = 6;
+  optional string phone_hub_session_id = 7;
+
   reserved 3;  // deprecated notification_icon_styling field.
 }
 
diff --git a/chromeos/ash/services/bluetooth_config/device_pairing_handler.cc b/chromeos/ash/services/bluetooth_config/device_pairing_handler.cc
index 4e379da4..a837792 100644
--- a/chromeos/ash/services/bluetooth_config/device_pairing_handler.cc
+++ b/chromeos/ash/services/bluetooth_config/device_pairing_handler.cc
@@ -60,6 +60,10 @@
     case device::ConnectionFailureReason::kNotConnectable:
       [[fallthrough]];
     case device::ConnectionFailureReason::kInprogress:
+      [[fallthrough]];
+    case device::ConnectionFailureReason::kNotFound:
+      [[fallthrough]];
+    case device::ConnectionFailureReason::kBluetoothDisabled:
       return mojom::PairingResult::kNonAuthFailure;
   }
 }
@@ -190,7 +194,8 @@
   if (!IsBluetoothEnabled()) {
     BLUETOOTH_LOG(ERROR) << "Pairing failed due to Bluetooth not being "
                          << "enabled, device identifier: " << device_id;
-    FinishCurrentPairingRequest(device::ConnectionFailureReason::kFailed);
+    FinishCurrentPairingRequest(
+        device::ConnectionFailureReason::kBluetoothDisabled);
     return;
   }
 
diff --git a/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl.cc b/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl.cc
index cabf744..33fa77e3 100644
--- a/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl.cc
+++ b/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl.cc
@@ -136,7 +136,7 @@
         << "Could not cancel pairing for device to due device no longer being "
            "found, identifier: "
         << current_pairing_device_id();
-    FinishCurrentPairingRequest(device::ConnectionFailureReason::kAuthFailed);
+    FinishCurrentPairingRequest(device::ConnectionFailureReason::kNotFound);
     return;
   }
 
@@ -151,7 +151,7 @@
     BLUETOOTH_LOG(ERROR)
         << "OnRequestPinCode failed due to device no longer being "
         << "found, identifier: " << current_pairing_device_id();
-    FinishCurrentPairingRequest(device::ConnectionFailureReason::kFailed);
+    FinishCurrentPairingRequest(device::ConnectionFailureReason::kNotFound);
     return;
   }
 
@@ -166,7 +166,7 @@
     BLUETOOTH_LOG(ERROR)
         << "OnRequestPasskey failed due to device no longer being "
         << "found, identifier: " << current_pairing_device_id();
-    FinishCurrentPairingRequest(device::ConnectionFailureReason::kFailed);
+    FinishCurrentPairingRequest(device::ConnectionFailureReason::kNotFound);
     return;
   }
 
@@ -194,7 +194,7 @@
     BLUETOOTH_LOG(ERROR)
         << "OnConfirmPairing failed due to device no longer being "
         << "found, identifier: " << current_pairing_device_id();
-    FinishCurrentPairingRequest(device::ConnectionFailureReason::kFailed);
+    FinishCurrentPairingRequest(device::ConnectionFailureReason::kNotFound);
     return;
   }
 
diff --git a/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl_unittest.cc b/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl_unittest.cc
index 13ce66d..a9ee8a8 100644
--- a/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl_unittest.cc
+++ b/chromeos/ash/services/bluetooth_config/device_pairing_handler_impl_unittest.cc
@@ -415,8 +415,9 @@
   CheckPairingHistograms(device::BluetoothTransportType::kInvalid,
                          /*type_count=*/1, /*failure_count=*/1,
                          /*success_count=*/0);
-  CheckPairingFailureHistogram(device::ConnectionFailureReason::kFailed,
-                               /*failure_count=*/1, /*filtered_count=*/1);
+  CheckPairingFailureHistogram(
+      device::ConnectionFailureReason::kBluetoothDisabled,
+      /*failure_count=*/1, /*filtered_count=*/1);
   CheckDurationHistogramMetrics(base::Milliseconds(0), /*success_count=*/0,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Invalid");
@@ -567,11 +568,11 @@
   // CancelPairing() won't be called since the device won't be found. We should
   // still return with a pairing result.
   EXPECT_EQ(num_cancel_pairing_calls(), 0u);
-  EXPECT_EQ(pairing_result(), mojom::PairingResult::kAuthFailed);
+  EXPECT_EQ(pairing_result(), mojom::PairingResult::kNonAuthFailure);
   CheckPairingHistograms(device::BluetoothTransportType::kInvalid,
                          /*type_count=*/1, /*failure_count=*/1,
                          /*success_count=*/0);
-  CheckPairingFailureHistogram(device::ConnectionFailureReason::kAuthFailed,
+  CheckPairingFailureHistogram(device::ConnectionFailureReason::kNotFound,
                                /*failure_count=*/1, /*filtered_count=*/1);
   CheckDurationHistogramMetrics(kTestDuration, /*success_count=*/0,
                                 /*failure_count=*/1,
@@ -737,7 +738,7 @@
   CheckPairingHistograms(device::BluetoothTransportType::kInvalid,
                          /*type_count=*/1, /*failure_count=*/1,
                          /*success_count=*/0);
-  CheckPairingFailureHistogram(device::ConnectionFailureReason::kFailed,
+  CheckPairingFailureHistogram(device::ConnectionFailureReason::kNotFound,
                                /*failure_count=*/1, /*filtered_count=*/1);
   CheckDurationHistogramMetrics(kTestDuration, /*success_count=*/0,
                                 /*failure_count=*/1,
@@ -787,7 +788,7 @@
   CheckPairingHistograms(device::BluetoothTransportType::kInvalid,
                          /*type_count=*/1, /*failure_count=*/1,
                          /*success_count=*/0);
-  CheckPairingFailureHistogram(device::ConnectionFailureReason::kFailed,
+  CheckPairingFailureHistogram(device::ConnectionFailureReason::kNotFound,
                                /*failure_count=*/1, /*filtered_count=*/1);
   CheckDurationHistogramMetrics(kTestDuration, /*success_count=*/0,
                                 /*failure_count=*/1,
@@ -1048,7 +1049,7 @@
   CheckPairingHistograms(device::BluetoothTransportType::kInvalid,
                          /*type_count=*/1, /*failure_count=*/1,
                          /*success_count=*/0);
-  CheckPairingFailureHistogram(device::ConnectionFailureReason::kFailed,
+  CheckPairingFailureHistogram(device::ConnectionFailureReason::kNotFound,
                                /*failure_count=*/1, /*filtered_count=*/1);
   CheckDurationHistogramMetrics(kTestDuration, /*success_count=*/0,
                                 /*failure_count=*/1,
diff --git a/clank b/clank
index 29f7853..f022452 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit 29f78538308b953b929c2d0fa89c036d889ad135
+Subproject commit f022452f20012a03b32a2db96eeda986d644287f
diff --git a/components/omnibox/browser/autocomplete_match.cc b/components/omnibox/browser/autocomplete_match.cc
index 21e7d38b..39c0408 100644
--- a/components/omnibox/browser/autocomplete_match.cc
+++ b/components/omnibox/browser/autocomplete_match.cc
@@ -1400,6 +1400,11 @@
       shortcut_boosted) {
     return 1;
   }
+  // IPH message always appears at the bottom of the Omnibox, after all other
+  // suggestions.
+  if (type == AutocompleteMatchType::NULL_RESULT_MESSAGE) {
+    return 4;
+  }
   return 3;
 }
 
diff --git a/components/omnibox/browser/featured_search_provider.cc b/components/omnibox/browser/featured_search_provider.cc
index 30c0890..105ecfc 100644
--- a/components/omnibox/browser/featured_search_provider.cc
+++ b/components/omnibox/browser/featured_search_provider.cc
@@ -47,6 +47,13 @@
   }
 
   DoStarterPackAutocompletion(input);
+
+  // TODO(crbug.com/333762301): Implement smarter triggering for the IPH match.
+  //  As is, the IPH message will always be displayed. This might make sense to
+  //  move to the ZPS provider.
+  if (OmniboxFieldTrial::IsStarterPackIPHEnabled()) {
+    AddIPHMatch();
+  }
 }
 
 FeaturedSearchProvider::~FeaturedSearchProvider() = default;
@@ -148,3 +155,19 @@
   }
   matches_.push_back(match);
 }
+
+void FeaturedSearchProvider::AddIPHMatch() {
+  // This value doesn't really matter as this suggestion is grouped after all
+  // other suggestions. Use an arbitrary constant.
+  constexpr int kRelevanceScore = 1000;
+  AutocompleteMatch match(this, kRelevanceScore, /*deletable=*/false,
+                          AutocompleteMatchType::NULL_RESULT_MESSAGE);
+
+  // Use this suggestion's contents field to display a message to the user that
+  // cannot be acted upon.
+  match.contents = l10n_util::GetStringUTF16(IDS_OMNIBOX_GEMINI_IPH);
+  match.contents_class.emplace_back(0, ACMatchClassification::NONE);
+  match.from_keyword = true;
+
+  matches_.push_back(match);
+}
diff --git a/components/omnibox/browser/featured_search_provider.h b/components/omnibox/browser/featured_search_provider.h
index d3f8ebc1..30d2204 100644
--- a/components/omnibox/browser/featured_search_provider.h
+++ b/components/omnibox/browser/featured_search_provider.h
@@ -40,6 +40,11 @@
   void AddStarterPackMatch(const TemplateURL& template_url,
                            const AutocompleteInput& input);
 
+  // Constructs a NULL_RESULT_MESSAGE match that is informational only and
+  // cannot be acted upon.  This match delivers an IPH message directing users
+  // to the starter pack feature.
+  void AddIPHMatch();
+
   raw_ptr<AutocompleteProviderClient> client_;
   raw_ptr<TemplateURLService> template_url_service_;
 };
diff --git a/components/omnibox/browser/omnibox_field_trial.cc b/components/omnibox/browser/omnibox_field_trial.cc
index d71d6f49..33c54bf8 100644
--- a/components/omnibox/browser/omnibox_field_trial.cc
+++ b/components/omnibox/browser/omnibox_field_trial.cc
@@ -1133,6 +1133,10 @@
 bool IsStarterPackExpansionEnabled() {
   return base::FeatureList::IsEnabled(omnibox::kStarterPackExpansion);
 }
+
+bool IsStarterPackIPHEnabled() {
+  return base::FeatureList::IsEnabled(omnibox::kStarterPackIPH);
+}
 // <- Site Search Starter Pack
 // ---------------------------------------------------------
 
diff --git a/components/omnibox/browser/omnibox_field_trial.h b/components/omnibox/browser/omnibox_field_trial.h
index b3a4a4f..25a3d984 100644
--- a/components/omnibox/browser/omnibox_field_trial.h
+++ b/components/omnibox/browser/omnibox_field_trial.h
@@ -779,6 +779,11 @@
 
 // Whether the expansion pack for the site search starter pack is enabled.
 bool IsStarterPackExpansionEnabled();
+
+// When true, enables an informational IPH message at the bottom of the Omnibox
+// directing users to certain starter pack engines.
+bool IsStarterPackIPHEnabled();
+
 // <- Site Search Starter Pack
 // ---------------------------------------------------------
 
diff --git a/components/omnibox/common/omnibox_features.cc b/components/omnibox/common/omnibox_features.cc
index 00aabe1..eed29101 100644
--- a/components/omnibox/common/omnibox_features.cc
+++ b/components/omnibox/common/omnibox_features.cc
@@ -551,6 +551,12 @@
              "StarterPackExpansion",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Enables an informational IPH message at the bottom of the Omnibox directing
+// users to certain starter pack engines.
+BASE_FEATURE(kStarterPackIPH,
+             "StarterPackIPH",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 // If enabled, |SearchProvider| will not function in Zero Suggest.
 BASE_FEATURE(kAblateSearchProviderWarmup,
              "AblateSearchProviderWarmup",
diff --git a/components/omnibox/common/omnibox_features.h b/components/omnibox/common/omnibox_features.h
index 6e12f6b..32b55e9 100644
--- a/components/omnibox/common/omnibox_features.h
+++ b/components/omnibox/common/omnibox_features.h
@@ -164,6 +164,7 @@
 BASE_DECLARE_FEATURE(kSiteSearchSettingsPolicy);
 BASE_DECLARE_FEATURE(kPolicyIndicationForManagedDefaultSearch);
 BASE_DECLARE_FEATURE(kStarterPackExpansion);
+BASE_DECLARE_FEATURE(kStarterPackIPH);
 
 // Search and Suggest requests and params.
 BASE_DECLARE_FEATURE(kAblateSearchProviderWarmup);
diff --git a/components/omnibox_strings.grdp b/components/omnibox_strings.grdp
index 3e34a9a..91b25b8 100644
--- a/components/omnibox_strings.grdp
+++ b/components/omnibox_strings.grdp
@@ -300,6 +300,10 @@
     No results found
   </message>
 
+  <message name="IDS_OMNIBOX_GEMINI_IPH" desc = "The string displayed as the last row in the Omnibox as IPH directing users to the @gemini starter pack.">
+    Type @gemini to Chat with Gemini
+  </message>
+
   <message name="IDS_OMNIBOX_ONE_LINE_CALCULATOR_SUGGESTION_TEMPLATE" desc = "The string displayed when a calculator answer is suggested.">
     <ph name="EXPRESSION">$1</ph> = <ph name="ANSWER">$2</ph>
   </message>
diff --git a/components/omnibox_strings_grdp/IDS_OMNIBOX_GEMINI_IPH.png.sha1 b/components/omnibox_strings_grdp/IDS_OMNIBOX_GEMINI_IPH.png.sha1
new file mode 100644
index 0000000..6a943c4e7
--- /dev/null
+++ b/components/omnibox_strings_grdp/IDS_OMNIBOX_GEMINI_IPH.png.sha1
@@ -0,0 +1 @@
+c4db86b58a21d4d9450080e885e6aef6c41ce7ae
\ No newline at end of file
diff --git a/components/optimization_guide/core/model_execution/session_impl.cc b/components/optimization_guide/core/model_execution/session_impl.cc
index 9c585a4..3323da2 100644
--- a/components/optimization_guide/core/model_execution/session_impl.cc
+++ b/components/optimization_guide/core/model_execution/session_impl.cc
@@ -658,11 +658,11 @@
   proto::OnDeviceModelServiceResponse* logged_response =
       on_device_state_->MutableLoggedResponse();
 
-  std::string current_response = on_device_state_->current_response;
-  logged_response->set_output_string(current_response);
+  logged_response->set_output_string(on_device_state_->current_response);
 
+  std::string redacted_response = on_device_state_->current_response;
   auto redact_result =
-      on_device_state_->opts.adapter->Redact(*last_message_, current_response);
+      on_device_state_->opts.adapter->Redact(*last_message_, redacted_response);
   if (redact_result == RedactResult::kReject) {
     logged_response->set_status(
         proto::ON_DEVICE_MODEL_SERVICE_RESPONSE_STATUS_RETRACTED);
@@ -699,8 +699,8 @@
     }
   }
 
-  auto output =
-      on_device_state_->opts.adapter->ConstructOutputMetadata(current_response);
+  auto output = on_device_state_->opts.adapter->ConstructOutputMetadata(
+      redacted_response);
   if (!output) {
     CancelPendingResponse(
         ExecuteModelResult::kFailedConstructingResponseMessage,
diff --git a/components/safe_browsing/core/browser/ping_manager.cc b/components/safe_browsing/core/browser/ping_manager.cc
index 28120c7c..26b68da 100644
--- a/components/safe_browsing/core/browser/ping_manager.cc
+++ b/components/safe_browsing/core/browser/ping_manager.cc
@@ -6,11 +6,13 @@
 
 #include <memory>
 #include <utility>
+#include <vector>
 
 #include "base/base64url.h"
 #include "base/check.h"
 #include "base/containers/contains.h"
 #include "base/containers/fixed_flat_set.h"
+#include "base/files/file_enumerator.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
@@ -39,6 +41,11 @@
 
 namespace {
 
+using WriteResult = safe_browsing::PingManager::Persister::WriteResult;
+
+// Delay before reading persisted reports at startup.
+base::TimeDelta kReadPersistedReportsDelay = base::Seconds(15);
+
 GURL GetSanitizedUrl(const GURL& url) {
   GURL::Replacements replacements;
   replacements.ClearUsername();
@@ -61,6 +68,10 @@
         DANGEROUS_DOWNLOAD_BY_API:
     case safe_browsing::ClientSafeBrowsingReportRequest::
         DANGEROUS_DOWNLOAD_OPENED:
+    case safe_browsing::ClientSafeBrowsingReportRequest::
+        DANGEROUS_DOWNLOAD_AUTO_DELETED:
+    case safe_browsing::ClientSafeBrowsingReportRequest::
+        DANGEROUS_DOWNLOAD_PROFILE_CLOSED:
       return true;
     default:
       return false;
@@ -72,6 +83,12 @@
       base::RandGenerator(std::numeric_limits<uint64_t>::max()));
 }
 
+void RecordPersisterWriteResult(WriteResult write_result) {
+  base::UmaHistogramEnumeration(
+      "SafeBrowsing.ClientSafeBrowsingReport.PersisterWriteResult",
+      write_result);
+}
+
 const net::NetworkTrafficAnnotationTag kTrafficAnnotation =
     net::DefineNetworkTrafficAnnotation("safe_browsing_extended_reporting",
                                         R"(
@@ -122,10 +139,41 @@
 void PingManager::Persister::WriteReport(const std::string& serialized_report) {
   base::File::Error error;
   if (!base::CreateDirectoryAndGetError(dir_path_, &error)) {
+    RecordPersisterWriteResult(WriteResult::kFailedCreateDirectory);
     return;
   }
   base::FilePath file_path = dir_path_.AppendASCII((GetRandFileName()));
-  base::WriteFile(file_path, serialized_report);
+  bool success = base::WriteFile(file_path, serialized_report);
+  RecordPersisterWriteResult(success ? WriteResult::kSuccess
+                                     : WriteResult::kFailedWriteFile);
+}
+
+std::vector<std::string> PingManager::Persister::ReadAndDeleteReports() {
+  if (!base::DirectoryExists(dir_path_)) {
+    return {};
+  }
+  base::FileEnumerator directory_enumerator(dir_path_,
+                                            /*recursive=*/false,
+                                            base::FileEnumerator::FILES);
+  std::vector<std::string> persisted_reports;
+  for (base::FilePath file_name = directory_enumerator.Next();
+       !file_name.empty(); file_name = directory_enumerator.Next()) {
+    std::string persisted_report;
+    bool success = base::ReadFileToString(file_name, &persisted_report);
+    base::UmaHistogramBoolean(
+        "SafeBrowsing.ClientSafeBrowsingReport.PersisterReadReportSuccessful",
+        success);
+    if (success) {
+      persisted_reports.emplace_back(std::move(persisted_report));
+    }
+  }
+  // Since persisted reports are uncommon, delete the directory so that we don't
+  // leave an empty directory going forward.
+  base::DeletePathRecursively(dir_path_);
+  base::UmaHistogramCounts1000(
+      "SafeBrowsing.ClientSafeBrowsingReport.PersisterReportCountOnStartup",
+      persisted_reports.size());
+  return persisted_reports;
 }
 
 // SafeBrowsingPingManager implementation ----------------------------------
@@ -143,12 +191,14 @@
     base::RepeatingCallback<ChromeUserPopulation::PageLoadToken(GURL)>
         get_page_load_token_callback,
     std::unique_ptr<SafeBrowsingHatsDelegate> hats_delegate,
-    const base::FilePath& persister_root_path) {
+    const base::FilePath& persister_root_path,
+    base::RepeatingCallback<bool()> get_should_send_persisted_report) {
   return std::make_unique<PingManager>(
       config, url_loader_factory, std::move(token_fetcher),
       get_should_fetch_access_token, webui_delegate, ui_task_runner,
       get_user_population_callback, get_page_load_token_callback,
-      std::move(hats_delegate), persister_root_path);
+      std::move(hats_delegate), persister_root_path,
+      std::move(get_should_send_persisted_report));
 }
 
 PingManager::PingManager(
@@ -163,7 +213,8 @@
     base::RepeatingCallback<ChromeUserPopulation::PageLoadToken(GURL)>
         get_page_load_token_callback,
     std::unique_ptr<SafeBrowsingHatsDelegate> hats_delegate,
-    const base::FilePath& persister_root_path)
+    const base::FilePath& persister_root_path,
+    base::RepeatingCallback<bool()> get_should_send_persisted_report)
     : config_(config),
       url_loader_factory_(url_loader_factory),
       token_fetcher_(std::move(token_fetcher)),
@@ -172,14 +223,21 @@
       ui_task_runner_(ui_task_runner),
       get_user_population_callback_(get_user_population_callback),
       get_page_load_token_callback_(get_page_load_token_callback),
-      hats_delegate_(std::move(hats_delegate)) {
+      hats_delegate_(std::move(hats_delegate)),
+      get_should_send_persisted_report_(
+          std::move(get_should_send_persisted_report)) {
   persister_ = base::SequenceBound<Persister>(
       base::ThreadPool::CreateSequencedTaskRunner(
           {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
            base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
       persister_root_path);
-  // TODO(crbug.com/329471668): Schedule tasks to read reports from persister
-  // and send them.
+  // Post this task with a delay to avoid running right at Chrome startup
+  // when a lot of other startup tasks are running.
+  ui_task_runner_->PostDelayedTask(
+      FROM_HERE,
+      base::BindOnce(&PingManager::ReadPersistedReports,
+                     weak_factory_.GetWeakPtr()),
+      kReadPersistedReportsDelay);
 }
 
 PingManager::~PingManager() {}
@@ -314,6 +372,29 @@
   return PersistThreatDetailsResult::kPersistTaskPosted;
 }
 
+void PingManager::ReadPersistedReports() {
+  persister_.AsyncCall(&PingManager::Persister::ReadAndDeleteReports)
+      .Then(base::BindOnce(&PingManager::OnReadPersistedReportsDone,
+                           weak_factory_.GetWeakPtr()));
+}
+
+void PingManager::OnReadPersistedReportsDone(
+    std::vector<std::string> serialized_reports) {
+  CHECK(!get_should_send_persisted_report_.is_null());
+  if (!get_should_send_persisted_report_.Run()) {
+    return;
+  }
+  for (const std::string& seralized_report : serialized_reports) {
+    if (seralized_report.empty()) {
+      continue;
+    }
+    auto report = std::make_unique<ClientSafeBrowsingReportRequest>();
+    if (report->ParseFromString(seralized_report)) {
+      ReportThreatDetails(std::move(report));
+    }
+  }
+}
+
 void PingManager::AttachThreatDetailsAndLaunchSurvey(
     std::unique_ptr<ClientSafeBrowsingReportRequest> report) {
   // Return early if HaTS survey is disabled by policy.
diff --git a/components/safe_browsing/core/browser/ping_manager.h b/components/safe_browsing/core/browser/ping_manager.h
index d8b5341..ab8cf16 100644
--- a/components/safe_browsing/core/browser/ping_manager.h
+++ b/components/safe_browsing/core/browser/ping_manager.h
@@ -42,6 +42,8 @@
     EMPTY_REPORT = 2,
   };
 
+  // These values are persisted to logs. Entries should not be renumbered and
+  // numeric values should never be reused.
   enum class PersistThreatDetailsResult {
     // The task to persist the report has posted. The actual file write
     // operation may still fail.
@@ -50,6 +52,7 @@
     kSerializationError = 1,
     // The report is empty, so it is not sent.
     kEmptyReport = 2,
+    kMaxValue = kEmptyReport,
   };
 
   // Interface via which a client of this class can surface relevant events in
@@ -69,6 +72,15 @@
   // Helper class to read/write a report on disk.
   class Persister {
    public:
+    // These values are persisted to logs. Entries should not be renumbered and
+    // numeric values should never be reused.
+    enum class WriteResult {
+      kSuccess = 0,
+      kFailedCreateDirectory = 1,
+      kFailedWriteFile = 2,
+      kMaxValue = kFailedWriteFile,
+    };
+
     explicit Persister(const base::FilePath& persister_root_path);
     Persister(const Persister&) = delete;
     Persister& operator=(const Persister&) = delete;
@@ -78,6 +90,11 @@
     // Writes |serialized_report| to a new file in |dir_path_|.
     void WriteReport(const std::string& serialized_report);
 
+    // Reads all persisted reports in |dir_path_|. The reports are deleted
+    // regardless of whether the read was successful or not.
+    // Returns a list of string representation of the reports.
+    std::vector<std::string> ReadAndDeleteReports();
+
    private:
     // The directory that the files will be written in.
     base::FilePath dir_path_;
@@ -95,7 +112,8 @@
       base::RepeatingCallback<ChromeUserPopulation::PageLoadToken(GURL)>
           get_page_load_token_callback,
       std::unique_ptr<SafeBrowsingHatsDelegate> hats_delegate,
-      const base::FilePath& persister_root_path);
+      const base::FilePath& persister_root_path,
+      base::RepeatingCallback<bool()> get_should_send_persisted_report);
   PingManager(const PingManager&) = delete;
   PingManager& operator=(const PingManager&) = delete;
 
@@ -114,7 +132,8 @@
       base::RepeatingCallback<ChromeUserPopulation::PageLoadToken(GURL)>
           get_page_load_token_callback,
       std::unique_ptr<SafeBrowsingHatsDelegate> hats_delegate,
-      const base::FilePath& persister_root_path);
+      const base::FilePath& persister_root_path,
+      base::RepeatingCallback<bool()> get_should_send_persisted_report);
 
   void OnURLLoaderComplete(network::SimpleURLLoader* source,
                            std::unique_ptr<std::string> response_body);
@@ -187,6 +206,12 @@
   void ReportThreatDetailsOnGotAccessToken(const std::string& serialized_report,
                                            const std::string& access_token);
 
+  // Reads persisted reports from disk.
+  void ReadPersistedReports();
+
+  // Sends `serialized_reports` to Safe Browsing.
+  void OnReadPersistedReportsDone(std::vector<std::string> serialized_reports);
+
   // Track outstanding SafeBrowsing report fetchers for clean up.
   // We add both "hit" and "detail" fetchers in this set.
   Reports safebrowsing_reports_;
@@ -221,6 +246,9 @@
 
   base::SequenceBound<Persister> persister_;
 
+  // Determines whether the user has opted in to send persisted reports.
+  base::RepeatingCallback<bool()> get_should_send_persisted_report_;
+
   base::WeakPtrFactory<PingManager> weak_factory_{this};
 };
 
diff --git a/components/safe_browsing/core/browser/ping_manager_unittest.cc b/components/safe_browsing/core/browser/ping_manager_unittest.cc
index ff4b5d5..5019f4b 100644
--- a/components/safe_browsing/core/browser/ping_manager_unittest.cc
+++ b/components/safe_browsing/core/browser/ping_manager_unittest.cc
@@ -76,14 +76,21 @@
           get_user_population_callback,
       std::optional<
           base::RepeatingCallback<ChromeUserPopulation::PageLoadToken(GURL)>>
-          get_page_load_token_callback);
+          get_page_load_token_callback,
+      std::optional<base::RepeatingCallback<bool()>>
+          get_should_send_persisted_report);
   void SetUpFeatureList(bool should_enable_remove_cookies);
+  // Returns a copy of the serialized persisted report that can be used to
+  // verify the data sent through URL loader.
+  std::string CallPersistThreatDetails(const std::string& url);
 
-  base::test::TaskEnvironment task_environment_;
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
   std::string key_param_;
   std::unique_ptr<MockWebUIDelegate> webui_delegate_ =
       std::make_unique<MockWebUIDelegate>();
   base::FilePath persister_root_path_;
+  base::FilePath persister_dir_;
   FakeSafeBrowsingHatsDelegate* SetUpHatsDelegate();
 
  private:
@@ -100,7 +107,8 @@
         "&key=%s", base::EscapeQueryParamValue(key, true).c_str());
   }
   persister_root_path_ = base::CreateUniqueTempDirectoryScopedToTest();
-  SetNewPingManager(std::nullopt, std::nullopt, std::nullopt);
+  persister_dir_ = persister_root_path_.AppendASCII("DownloadReports");
+  SetNewPingManager(std::nullopt, std::nullopt, std::nullopt, std::nullopt);
 }
 
 void PingManagerTest::TearDown() {
@@ -119,7 +127,9 @@
         get_user_population_callback,
     std::optional<
         base::RepeatingCallback<ChromeUserPopulation::PageLoadToken(GURL)>>
-        get_page_load_token_callback) {
+        get_page_load_token_callback,
+    std::optional<base::RepeatingCallback<bool()>>
+        get_should_send_persisted_report) {
   ping_manager_.reset(new PingManager(
       safe_browsing::GetTestV4ProtocolConfig(), nullptr, nullptr,
       get_should_fetch_access_token.value_or(
@@ -127,7 +137,9 @@
       webui_delegate_.get(), base::SequencedTaskRunner::GetCurrentDefault(),
       get_user_population_callback.value_or(base::NullCallback()),
       get_page_load_token_callback.value_or(base::NullCallback()), nullptr,
-      persister_root_path_));
+      persister_root_path_,
+      get_should_send_persisted_report.value_or(
+          base::BindRepeating([]() { return false; }))));
 }
 
 void PingManagerTest::SetUpFeatureList(bool should_enable_remove_cookies) {
@@ -141,6 +153,24 @@
   feature_list_.InitWithFeatures(enabled_features, disabled_features);
 }
 
+std::string PingManagerTest::CallPersistThreatDetails(const std::string& url) {
+  std::unique_ptr<ClientSafeBrowsingReportRequest> report =
+      std::make_unique<ClientSafeBrowsingReportRequest>();
+  report->set_type(
+      ClientSafeBrowsingReportRequest::DANGEROUS_DOWNLOAD_PROFILE_CLOSED);
+  report->set_url(url);
+  std::string serialized_report;
+  EXPECT_TRUE(report->SerializeToString(&serialized_report));
+
+  PingManager::PersistThreatDetailsResult result =
+      ping_manager()->PersistThreatDetailsAndReportOnNextStartup(
+          std::move(report));
+  EXPECT_EQ(result,
+            PingManager::PersistThreatDetailsResult::kPersistTaskPosted);
+  task_environment_.RunUntilIdle();
+  return serialized_report;
+}
+
 TestSafeBrowsingTokenFetcher* PingManagerTest::SetUpTokenFetcher() {
   auto token_fetcher = std::make_unique<TestSafeBrowsingTokenFetcher>();
   auto* raw_token_fetcher = token_fetcher.get();
@@ -519,7 +549,8 @@
       /*get_should_fetch_access_token=*/base::BindRepeating(
           []() { return true; }),
       /*get_user_population_callback=*/std::nullopt,
-      /*get_page_load_token_callback=*/std::nullopt);
+      /*get_page_load_token_callback=*/std::nullopt,
+      /*get_should_send_persisted_report=*/std::nullopt);
   SetUpFeatureList(/*should_enable_remove_cookies=*/true);
   RunReportThreatDetailsTest(/*expect_access_token=*/true,
                              /*expected_user_population=*/std::nullopt,
@@ -532,7 +563,8 @@
       /*get_should_fetch_access_token=*/base::BindRepeating(
           []() { return true; }),
       /*get_user_population_callback=*/std::nullopt,
-      /*get_page_load_token_callback=*/std::nullopt);
+      /*get_page_load_token_callback=*/std::nullopt,
+      /*get_should_send_persisted_report=*/std::nullopt);
   SetUpFeatureList(/*should_enable_remove_cookies=*/false);
   RunReportThreatDetailsTest(/*expect_access_token=*/true,
                              /*expected_user_population=*/std::nullopt,
@@ -547,7 +579,8 @@
         population.set_user_population(ChromeUserPopulation::SAFE_BROWSING);
         return population;
       }),
-      /*get_page_load_token_callback=*/std::nullopt);
+      /*get_page_load_token_callback=*/std::nullopt,
+      /*get_should_send_persisted_report=*/std::nullopt);
   auto population = ChromeUserPopulation();
   population.set_user_population(ChromeUserPopulation::SAFE_BROWSING);
   RunReportThreatDetailsTest(/*expect_access_token=*/false,
@@ -564,7 +597,8 @@
         ChromeUserPopulation::PageLoadToken token;
         token.set_token_value("testing_page_load_token");
         return token;
-      }));
+      }),
+      /*get_should_send_persisted_report=*/std::nullopt);
   RunReportThreatDetailsTest(
       /*expect_access_token=*/false,
       /*expected_user_population=*/std::nullopt,
@@ -572,23 +606,12 @@
       /*expect_cookies_removed=*/false);
 }
 
-TEST_F(PingManagerTest, PersistThreatDetails) {
-  std::unique_ptr<ClientSafeBrowsingReportRequest> report =
-      std::make_unique<ClientSafeBrowsingReportRequest>();
-  report->set_type(
-      ClientSafeBrowsingReportRequest::DANGEROUS_DOWNLOAD_PROFILE_CLOSED);
-  report->set_url("https://some.url.com/");
-  PingManager::PersistThreatDetailsResult result =
-      ping_manager()->PersistThreatDetailsAndReportOnNextStartup(
-          std::move(report));
-  EXPECT_EQ(result,
-            PingManager::PersistThreatDetailsResult::kPersistTaskPosted);
-  task_environment_.RunUntilIdle();
+TEST_F(PingManagerTest, PersistThreatDetailsAtShutdown) {
+  base::HistogramTester histogram_tester;
 
-  base::FilePath persister_dir =
-      persister_root_path_.AppendASCII("DownloadReports");
-  ASSERT_TRUE(base::PathExists(persister_dir));
-  base::FileEnumerator directory_enumerator(persister_dir,
+  CallPersistThreatDetails("https://some.url.com/");
+  ASSERT_TRUE(base::PathExists(persister_dir_));
+  base::FileEnumerator directory_enumerator(persister_dir_,
                                             /*recursive=*/false,
                                             base::FileEnumerator::FILES);
   int number_of_files = 0;
@@ -608,6 +631,10 @@
     EXPECT_EQ(persisted_report->url(), "https://some.url.com/");
   }
   EXPECT_EQ(number_of_files, 1);
+  histogram_tester.ExpectUniqueSample(
+      "SafeBrowsing.ClientSafeBrowsingReport.PersisterWriteResult",
+      /*sample=*/PingManager::Persister::WriteResult::kSuccess,
+      /*expected_bucket_count=*/1);
 }
 
 TEST_F(PingManagerTest, PersistThreatDetailsAtShutdown_EmptyReport) {
@@ -619,6 +646,135 @@
   EXPECT_EQ(result, PingManager::PersistThreatDetailsResult::kEmptyReport);
 }
 
+TEST_F(PingManagerTest, SendPersistedThreatDetailsOnStartup) {
+  base::HistogramTester histogram_tester;
+  std::string persisted_report1 =
+      CallPersistThreatDetails("https://some.url1.com/");
+  std::string persisted_report2 =
+      CallPersistThreatDetails("https://some.url2.com/");
+  EXPECT_TRUE(base::PathExists(persister_dir_));
+
+  // Create a new ping manager instance to simulate browser startup.
+  SetNewPingManager(
+      /*get_should_fetch_access_token=*/std::nullopt,
+      /*get_user_population_callback=*/std::nullopt,
+      /*get_page_load_token_callback=*/std::nullopt,
+      /*get_should_send_persisted_report=*/base::BindRepeating([]() {
+        return true;
+      }));
+
+  network::TestURLLoaderFactory test_url_loader_factory;
+  bool report1_sent = false, report2_sent = false;
+  test_url_loader_factory.SetInterceptor(
+      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+        std::string upload_data = GetUploadData(request);
+        if (upload_data == persisted_report1) {
+          report1_sent = true;
+        } else if (upload_data == persisted_report2) {
+          report2_sent = true;
+        }
+      }));
+  ping_manager()->SetURLLoaderFactoryForTesting(
+      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+          &test_url_loader_factory));
+  EXPECT_CALL(*webui_delegate_.get(), AddToCSBRRsSent(_)).Times(2);
+
+  // Request is sent after delay.
+  task_environment_.FastForwardBy(base::Seconds(14));
+  EXPECT_EQ(test_url_loader_factory.total_requests(), 0u);
+  task_environment_.FastForwardBy(base::Seconds(1));
+  EXPECT_EQ(test_url_loader_factory.total_requests(), 2u);
+  EXPECT_TRUE(report1_sent);
+  EXPECT_TRUE(report2_sent);
+  // Directory is deleted.
+  EXPECT_FALSE(base::PathExists(persister_dir_));
+  histogram_tester.ExpectUniqueSample(
+      "SafeBrowsing.ClientSafeBrowsingReport.PersisterReadReportSuccessful",
+      /*sample=*/true,
+      /*expected_bucket_count=*/2);
+  histogram_tester.ExpectUniqueSample(
+      "SafeBrowsing.ClientSafeBrowsingReport.PersisterReportCountOnStartup",
+      /*sample=*/2,
+      /*expected_bucket_count=*/1);
+}
+
+TEST_F(PingManagerTest, SendPersistedThreatDetailsOnStartup_MalformedReports) {
+  base::CreateDirectory(persister_dir_);
+  base::FilePath empty_file = persister_dir_.AppendASCII("empty");
+  base::WriteFile(empty_file, "");
+  base::FilePath malformed_file = persister_dir_.AppendASCII("malformed");
+  base::WriteFile(malformed_file, "malformed_report");
+
+  // Create a new ping manager instance to simulate browser startup.
+  SetNewPingManager(
+      /*get_should_fetch_access_token=*/std::nullopt,
+      /*get_user_population_callback=*/std::nullopt,
+      /*get_page_load_token_callback=*/std::nullopt,
+      /*get_should_send_persisted_report=*/base::BindRepeating([]() {
+        return true;
+      }));
+
+  network::TestURLLoaderFactory test_url_loader_factory;
+  ping_manager()->SetURLLoaderFactoryForTesting(
+      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+          &test_url_loader_factory));
+
+  task_environment_.FastForwardBy(base::Seconds(15));
+  // Persisted report should not be sent because their content is invalid.
+  EXPECT_EQ(test_url_loader_factory.total_requests(), 0u);
+  // Persisted report should still be deleted from disk.
+  EXPECT_FALSE(base::PathExists(persister_dir_));
+}
+
+TEST_F(PingManagerTest, SendPersistedThreatDetailsOnStartup_EmptyDirectory) {
+  base::CreateDirectory(persister_dir_);
+
+  // Create a new ping manager instance to simulate browser startup.
+  SetNewPingManager(
+      /*get_should_fetch_access_token=*/std::nullopt,
+      /*get_user_population_callback=*/std::nullopt,
+      /*get_page_load_token_callback=*/std::nullopt,
+      /*get_should_send_persisted_report=*/base::BindRepeating([]() {
+        return true;
+      }));
+
+  network::TestURLLoaderFactory test_url_loader_factory;
+  ping_manager()->SetURLLoaderFactoryForTesting(
+      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+          &test_url_loader_factory));
+
+  task_environment_.FastForwardBy(base::Seconds(15));
+  // The directory should be cleaned up.
+  EXPECT_FALSE(base::PathExists(persister_dir_));
+}
+
+TEST_F(PingManagerTest,
+       SendPersistedThreatDetailsOnStartup_ShouldNotSendReport) {
+  CallPersistThreatDetails("https://some.url1.com/");
+  EXPECT_TRUE(base::PathExists(persister_dir_));
+
+  // Create a new ping manager instance to simulate browser startup.
+  SetNewPingManager(
+      /*get_should_fetch_access_token=*/std::nullopt,
+      /*get_user_population_callback=*/std::nullopt,
+      /*get_page_load_token_callback=*/std::nullopt,
+      /*get_should_send_persisted_report=*/base::BindRepeating([]() {
+        return false;
+      }));
+
+  network::TestURLLoaderFactory test_url_loader_factory;
+  ping_manager()->SetURLLoaderFactoryForTesting(
+      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+          &test_url_loader_factory));
+
+  task_environment_.FastForwardBy(base::Seconds(15));
+  // Persisted report should not be sent because
+  // get_should_send_persisted_report is false.
+  EXPECT_EQ(test_url_loader_factory.total_requests(), 0u);
+  // Persisted report should still be deleted from disk.
+  EXPECT_FALSE(base::PathExists(persister_dir_));
+}
+
 TEST_F(PingManagerTest, ReportSafeBrowsingHit) {
   std::unique_ptr<HitReport> hit_report = std::make_unique<HitReport>();
   std::string post_data = "testing_hit_report_post_data";
@@ -663,7 +819,8 @@
         ChromeUserPopulation::PageLoadToken token;
         token.set_token_value("testing_page_load_token");
         return token;
-      }));
+      }),
+      /*get_should_send_persisted_report=*/std::nullopt);
   FakeSafeBrowsingHatsDelegate* raw_fake_sb_hats_delegate = SetUpHatsDelegate();
   ping_manager()->AttachThreatDetailsAndLaunchSurvey(std::move(report));
   std::string deserialized_report_string;
diff --git a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc
index 5a80b17..6e6b32f4 100644
--- a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc
+++ b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc
@@ -80,6 +80,31 @@
                                          std::numeric_limits<int>::max(),
                                          std::numeric_limits<int>::max());
 
+// Note about RGBA/BGRA/ARGB pixel format names:
+// In FrameSinkVideoCapturer, ARGB is a "format name", the frames it gives
+// could be RGBA/BGRA depends on platform and the preference of the buffer
+// format. When user wants ARGB result, it requests a CopyOutputRequest with
+// ResultFormat::RGBA which gives RGBA/BGRA results depends on platform and
+// where the result is stored (system memory or shared texture).
+// In our case, when requesting a kPreferGpuMemoryBuffer, it will create a blit
+// request, results in CopyOutputRequest uses whatever RGBA/BGRA pixel format
+// the GMB is, which we created in advance. For now, it is determined by
+// GetFramePoolPlatformPixelFormat.
+// This is also documented in the mojom comments (https://crrev.com/c/5418235)
+// about SetFormat, indicating the ARGB format may produce RGBA/BGRA frames
+// depends on platform.
+
+media::VideoPixelFormat GetFramePoolPlatformPixelFormat(
+    media::VideoPixelFormat format,
+    mojom::BufferFormatPreference buffer_format_preference) {
+  if (format == media::PIXEL_FORMAT_ARGB &&
+      buffer_format_preference ==
+          mojom::BufferFormatPreference::kPreferGpuMemoryBuffer) {
+    return media::PIXEL_FORMAT_ABGR;
+  }
+  return format;
+}
+
 // Get the frame pool for the specific format. We need context_provider if the
 // format is NV12 or ARGB (when buffer_format_preference is kNativeTexture).
 // Thus, buffer_format_preference is also needed to tell which mode ARGB use.
@@ -99,8 +124,9 @@
       switch (buffer_format_preference) {
         case mojom::BufferFormatPreference::kPreferGpuMemoryBuffer:
           return std::make_unique<GpuMemoryBufferVideoFramePool>(
-              capacity, format, gfx::ColorSpace::CreateSRGB(),
-              context_provider);
+              capacity,
+              GetFramePoolPlatformPixelFormat(format, buffer_format_preference),
+              gfx::ColorSpace::CreateSRGB(), context_provider);
         case mojom::BufferFormatPreference::kDefault:
           return std::make_unique<SharedMemoryVideoFramePool>(capacity);
         default:
@@ -837,7 +863,10 @@
                         region_properties->render_pass_subrect.ToString());
     auto reserve_start_time = base::TimeTicks::Now();
 
-    frame = frame_pool_->ReserveVideoFrame(pixel_format_, capture_size);
+    frame = frame_pool_->ReserveVideoFrame(
+        GetFramePoolPlatformPixelFormat(pixel_format_,
+                                        buffer_format_preference_),
+        capture_size);
 
     UMA_HISTOGRAM_CUSTOM_TIMES(
         "Viz.FrameSinkVideoCapturer.ReserveFrameDuration",
@@ -1171,7 +1200,7 @@
     // NV12 is currently supported only via GpuMemoryBuffers, everything else is
     // returned as a bitmap:
     const bool is_bitmap =
-        pixel_format_ != media::VideoPixelFormat::PIXEL_FORMAT_NV12;
+        buffer_format_preference_ == mojom::BufferFormatPreference::kDefault;
     consumer_->OnLog(base::StringPrintf(
         "FrameSinkVideoCapturerImpl: Sending CopyRequest: "
         "format=%s (%s) area:%s "
diff --git a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl_unittest.cc b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl_unittest.cc
index 9ad60e5f..e99ac30 100644
--- a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl_unittest.cc
+++ b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl_unittest.cc
@@ -33,6 +33,7 @@
 #include "components/viz/test/test_context_provider.h"
 #include "gpu/command_buffer/client/client_shared_image.h"
 #include "gpu/command_buffer/common/gpu_memory_buffer_support.h"
+#include "media/base/format_utils.h"
 #include "media/base/limits.h"
 #include "media/base/test_helpers.h"
 #include "media/base/video_util.h"
@@ -89,6 +90,15 @@
          gfx::PointF(*rso_x, *rso_y) == root_scroll_offset;
 }
 
+// The following functions, CopyOutputRequestFormatToVideoPixelFormat and
+// GetColorSpaceForPixelFormat only deal with pixel_format_ which is the user
+// requested format, so we only have to care media::PIXEL_FORMAT_ARGB, and
+// ResultFormat::RGBA. GetBufferFormatForVideoPixelFormat and
+// GetBufferSizeInPixelsForVideoPixelFormat needs to deal with the GMB and frame
+// result passed from the capture callback, it could be RGBA/BGRA depends on
+// which platform we are, so we have to handle both media::PIXEL_FORMAT_ARGB
+// and media::PIXEL_FORMAT_ABGR.
+
 media::VideoPixelFormat CopyOutputRequestFormatToVideoPixelFormat(
     CopyOutputRequest::ResultFormat format) {
   switch (format) {
@@ -116,23 +126,12 @@
   }
 }
 
-gfx::BufferFormat GetBufferFormatForVideoPixelFormat(
-    media::VideoPixelFormat format) {
-  switch (format) {
-    case media::PIXEL_FORMAT_ABGR:
-      return gfx::BufferFormat::RGBA_8888;
-    case media::PIXEL_FORMAT_NV12:
-      return gfx::BufferFormat::YUV_420_BIPLANAR;
-    default:
-      NOTREACHED_NORETURN();
-  }
-}
-
 gfx::Size GetBufferSizeInPixelsForVideoPixelFormat(
     media::VideoPixelFormat format,
     const gfx::Size& coded_size) {
   switch (format) {
     case media::PIXEL_FORMAT_ABGR:
+    case media::PIXEL_FORMAT_ARGB:
       return coded_size;
     case media::PIXEL_FORMAT_NV12:
       return {cc::MathUtil::CheckedRoundUp(coded_size.width(), 2),
@@ -282,7 +281,7 @@
       auto gmb_dummy = std::make_unique<media::FakeGpuMemoryBuffer>(
           GetBufferSizeInPixelsForVideoPixelFormat(info->pixel_format,
                                                    info->coded_size),
-          GetBufferFormatForVideoPixelFormat(info->pixel_format));
+          VideoPixelFormatToGfxBufferFormat(info->pixel_format).value());
       gpu::MailboxHolder mailbox_dummy[4];
 
       // The frame is only gonna tell Letterbox to skip the test.
diff --git a/content/browser/bluetooth/bluetooth_metrics.h b/content/browser/bluetooth/bluetooth_metrics.h
index 2d04af2..fa03609 100644
--- a/content/browser/bluetooth/bluetooth_metrics.h
+++ b/content/browser/bluetooth/bluetooth_metrics.h
@@ -53,6 +53,7 @@
   NOT_CONNECTED = 13,
   DOES_NOT_EXIST = 14,
   INVALID_ARGS = 15,
+  NON_AUTH_TIMEOUT = 16,
   // Note: Add new ConnectGATT outcomes immediately above this line. Make sure
   // to update the enum list in tools/metrics/histograms/histograms.xml
   // accordingly.
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.cc b/content/browser/bluetooth/web_bluetooth_service_impl.cc
index 7ff0f0ed..46a3efc 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl.cc
@@ -262,6 +262,9 @@
     case BluetoothDevice::ERROR_INVALID_ARGS:
       RecordConnectGATTOutcome(UMAConnectGATTOutcome::INVALID_ARGS);
       return blink::mojom::WebBluetoothResult::CONNECT_INVALID_ARGS;
+    case BluetoothDevice::ERROR_NON_AUTH_TIMEOUT:
+      RecordConnectGATTOutcome(UMAConnectGATTOutcome::NON_AUTH_TIMEOUT);
+      return blink::mojom::WebBluetoothResult::CONNECT_NON_AUTH_TIMEOUT;
     case BluetoothDevice::NUM_CONNECT_ERROR_CODES:
       NOTREACHED();
       return blink::mojom::WebBluetoothResult::CONNECT_UNKNOWN_FAILURE;
diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index 74f95ce..3703599 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -636,6 +636,18 @@
 
   for (auto& observer : observers_)
     observer->MediaSessionPositionChanged(position_);
+
+  const bool is_considered_live =
+      position_.has_value() && position_->duration().is_max();
+  if (is_considered_live == is_considered_live_) {
+    return;
+  }
+
+  // The available actions can be different depending on whether we're
+  // considered live or not, so if that has changed we must re-notify for the
+  // new state.
+  is_considered_live_ = is_considered_live;
+  RebuildAndNotifyActionsChanged();
 }
 
 void MediaSessionImpl::Resume(SuspendType suspend_type) {
@@ -1750,10 +1762,14 @@
     actions.insert(media_session::mojom::MediaSessionAction::kPlay);
     actions.insert(media_session::mojom::MediaSessionAction::kPause);
     actions.insert(media_session::mojom::MediaSessionAction::kStop);
-    actions.insert(media_session::mojom::MediaSessionAction::kSeekTo);
-    actions.insert(media_session::mojom::MediaSessionAction::kScrubTo);
-    actions.insert(media_session::mojom::MediaSessionAction::kSeekForward);
-    actions.insert(media_session::mojom::MediaSessionAction::kSeekBackward);
+
+    // Support seeking as long as this isn't live media.
+    if (!is_considered_live_) {
+      actions.insert(media_session::mojom::MediaSessionAction::kSeekTo);
+      actions.insert(media_session::mojom::MediaSessionAction::kScrubTo);
+      actions.insert(media_session::mojom::MediaSessionAction::kSeekForward);
+      actions.insert(media_session::mojom::MediaSessionAction::kSeekBackward);
+    }
   }
 
   // If the website has specified an action handler for 'enterpictureinpicture',
diff --git a/content/browser/media/session/media_session_impl.h b/content/browser/media/session/media_session_impl.h
index abb402e..c6e8baf2 100644
--- a/content/browser/media/session/media_session_impl.h
+++ b/content/browser/media/session/media_session_impl.h
@@ -653,6 +653,11 @@
   // active session.
   bool always_ignore_for_active_session_for_testing_ = false;
 
+  // True if the given media has infinite duration OR has a duration that
+  // changes often enough to be considered live. See
+  // `MaybeGuardDurationUpdate()` for details on duration changes.
+  bool is_considered_live_ = false;
+
   WEB_CONTENTS_USER_DATA_KEY_DECL();
 };
 
diff --git a/content/browser/media/session/media_session_impl_unittest.cc b/content/browser/media/session/media_session_impl_unittest.cc
index b4cd784..6f8312fd 100644
--- a/content/browser/media/session/media_session_impl_unittest.cc
+++ b/content/browser/media/session/media_session_impl_unittest.cc
@@ -35,6 +35,7 @@
 
 using media_session::mojom::AudioFocusType;
 using media_session::mojom::MediaPlaybackState;
+using media_session::mojom::MediaSessionAction;
 using media_session::mojom::MediaSessionInfo;
 using media_session::mojom::MediaSessionInfoPtr;
 using media_session::test::MockMediaSessionMojoObserver;
@@ -98,15 +99,13 @@
   MediaSessionImplTest()
       : RenderViewHostTestHarness(
             base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
-    default_actions_.insert(media_session::mojom::MediaSessionAction::kPlay);
-    default_actions_.insert(media_session::mojom::MediaSessionAction::kPause);
-    default_actions_.insert(media_session::mojom::MediaSessionAction::kStop);
-    default_actions_.insert(media_session::mojom::MediaSessionAction::kSeekTo);
-    default_actions_.insert(media_session::mojom::MediaSessionAction::kScrubTo);
-    default_actions_.insert(
-        media_session::mojom::MediaSessionAction::kSeekForward);
-    default_actions_.insert(
-        media_session::mojom::MediaSessionAction::kSeekBackward);
+    default_actions_.insert(MediaSessionAction::kPlay);
+    default_actions_.insert(MediaSessionAction::kPause);
+    default_actions_.insert(MediaSessionAction::kStop);
+    default_actions_.insert(MediaSessionAction::kSeekTo);
+    default_actions_.insert(MediaSessionAction::kScrubTo);
+    default_actions_.insert(MediaSessionAction::kSeekForward);
+    default_actions_.insert(MediaSessionAction::kSeekBackward);
   }
 
   MediaSessionImplTest(const MediaSessionImplTest&) = delete;
@@ -212,8 +211,7 @@
     return player_observer_.get();
   }
 
-  const std::set<media_session::mojom::MediaSessionAction>& default_actions()
-      const {
+  const std::set<MediaSessionAction>& default_actions() const {
     return default_actions_;
   }
 
@@ -226,7 +224,7 @@
   }
 
  private:
-  std::set<media_session::mojom::MediaSessionAction> default_actions_;
+  std::set<MediaSessionAction> default_actions_;
 
   base::test::ScopedFeatureList scoped_feature_list_;
 
@@ -376,9 +374,8 @@
 }
 
 TEST_F(MediaSessionImplTest, SuspendUI) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPause, _))
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPause, _))
       .Times(0);
 
   StartNewPlayer();
@@ -392,14 +389,12 @@
 }
 
 TEST_F(MediaSessionImplTest, SuspendContent_WithAction) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPause, _))
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPause, _))
       .Times(0);
 
   StartNewPlayer();
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPause);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPause);
 
   GetMediaSession()->Suspend(MediaSession::SuspendType::kContent);
   mock_media_session_service().FlushForTesting();
@@ -410,14 +405,12 @@
 }
 
 TEST_F(MediaSessionImplTest, SuspendSystem_WithAction) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPause, _))
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPause, _))
       .Times(0);
 
   StartNewPlayer();
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPause);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPause);
 
   GetMediaSession()->Suspend(MediaSession::SuspendType::kSystem);
   mock_media_session_service().FlushForTesting();
@@ -428,13 +421,11 @@
 }
 
 TEST_F(MediaSessionImplTest, SuspendUI_WithAction) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPause, _));
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPause, _));
 
   StartNewPlayer();
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPause);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPause);
 
   GetMediaSession()->Suspend(MediaSession::SuspendType::kUI);
   mock_media_session_service().FlushForTesting();
@@ -445,9 +436,8 @@
 }
 
 TEST_F(MediaSessionImplTest, ResumeUI) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPlay, _))
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPlay, _))
       .Times(0);
 
   StartNewPlayer();
@@ -462,14 +452,12 @@
 }
 
 TEST_F(MediaSessionImplTest, ResumeContent_WithAction) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPlay, _))
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPlay, _))
       .Times(0);
 
   StartNewPlayer();
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPlay);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPlay);
 
   GetMediaSession()->Suspend(MediaSession::SuspendType::kSystem);
   GetMediaSession()->Resume(MediaSession::SuspendType::kContent);
@@ -481,14 +469,12 @@
 }
 
 TEST_F(MediaSessionImplTest, ResumeSystem_WithAction) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPlay, _))
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPlay, _))
       .Times(0);
 
   StartNewPlayer();
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPlay);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPlay);
 
   GetMediaSession()->Suspend(MediaSession::SuspendType::kSystem);
   GetMediaSession()->Resume(MediaSession::SuspendType::kSystem);
@@ -500,13 +486,11 @@
 }
 
 TEST_F(MediaSessionImplTest, ResumeUI_WithAction) {
-  EXPECT_CALL(
-      mock_media_session_service().mock_client(),
-      DidReceiveAction(media_session::mojom::MediaSessionAction::kPlay, _));
+  EXPECT_CALL(mock_media_session_service().mock_client(),
+              DidReceiveAction(MediaSessionAction::kPlay, _));
 
   StartNewPlayer();
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPlay);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPlay);
 
   GetMediaSession()->Suspend(MediaSession::SuspendType::kSystem);
   GetMediaSession()->Resume(MediaSession::SuspendType::kUI);
@@ -806,18 +790,15 @@
   media_session::test::MockMediaSessionMojoObserver observer(
       *GetMediaSession());
   mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kEnterPictureInPicture);
+      MediaSessionAction::kEnterPictureInPicture);
   mock_media_session_service().FlushForTesting();
 
-  EXPECT_TRUE(base::Contains(
-      observer.actions(),
-      media_session::mojom::MediaSessionAction::kEnterPictureInPicture));
-  EXPECT_TRUE(base::Contains(
-      observer.actions(),
-      media_session::mojom::MediaSessionAction::kEnterAutoPictureInPicture));
-  EXPECT_TRUE(base::Contains(
-      observer.actions(),
-      media_session::mojom::MediaSessionAction::kExitPictureInPicture));
+  EXPECT_TRUE(base::Contains(observer.actions(),
+                             MediaSessionAction::kEnterPictureInPicture));
+  EXPECT_TRUE(base::Contains(observer.actions(),
+                             MediaSessionAction::kEnterAutoPictureInPicture));
+  EXPECT_TRUE(base::Contains(observer.actions(),
+                             MediaSessionAction::kExitPictureInPicture));
 }
 
 TEST_F(MediaSessionImplTest, WebContentsHasPictureInPictureVideo) {
@@ -828,16 +809,13 @@
   StartNewPlayer();
   media_session::test::MockMediaSessionMojoObserver observer(
       *GetMediaSession());
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPause);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPause);
   mock_media_session_service().FlushForTesting();
 
-  EXPECT_FALSE(base::Contains(
-      observer.actions(),
-      media_session::mojom::MediaSessionAction::kEnterPictureInPicture));
-  EXPECT_TRUE(base::Contains(
-      observer.actions(),
-      media_session::mojom::MediaSessionAction::kExitPictureInPicture));
+  EXPECT_FALSE(base::Contains(observer.actions(),
+                              MediaSessionAction::kEnterPictureInPicture));
+  EXPECT_TRUE(base::Contains(observer.actions(),
+                             MediaSessionAction::kExitPictureInPicture));
 }
 
 TEST_F(MediaSessionImplTest, WebContentsHasPictureInPictureDocument) {
@@ -848,16 +826,13 @@
   StartNewPlayer();
   media_session::test::MockMediaSessionMojoObserver observer(
       *GetMediaSession());
-  mock_media_session_service().EnableAction(
-      media_session::mojom::MediaSessionAction::kPause);
+  mock_media_session_service().EnableAction(MediaSessionAction::kPause);
   mock_media_session_service().FlushForTesting();
 
-  EXPECT_FALSE(base::Contains(
-      observer.actions(),
-      media_session::mojom::MediaSessionAction::kEnterPictureInPicture));
-  EXPECT_TRUE(base::Contains(
-      observer.actions(),
-      media_session::mojom::MediaSessionAction::kExitPictureInPicture));
+  EXPECT_FALSE(base::Contains(observer.actions(),
+                              MediaSessionAction::kEnterPictureInPicture));
+  EXPECT_TRUE(base::Contains(observer.actions(),
+                             MediaSessionAction::kExitPictureInPicture));
 }
 
 TEST_F(MediaSessionImplTest, SufficientlyVisibleVideo_NoPlayer) {
@@ -927,6 +902,45 @@
   EXPECT_TRUE(GetMediaSession()->IsControllable());
 }
 
+TEST_F(MediaSessionImplTest, SeekingAndScrubbingNotAllowedWithMaxDuration) {
+  MockMediaSessionMojoObserver observer(*GetMediaSession());
+  int player_id = player_observer_->StartNewPlayer();
+  GetMediaSession()->AddPlayer(player_observer_.get(), player_id);
+
+  media_session::MediaPosition pos;
+  pos = media_session::MediaPosition(
+      /*playback_rate=*/1.0,
+      /*duration=*/base::TimeDelta::Max(),
+      /*position=*/base::TimeDelta(), /*end_of_media=*/false);
+
+  player_observer_->SetPosition(player_id, pos);
+  GetMediaSession()->RebuildAndNotifyMediaPositionChanged();
+  FlushForTesting(GetMediaSession());
+
+  // With a max duration, we should be considered live media and should not
+  // allow seeking and scrubbing actions by default.
+  EXPECT_FALSE(base::Contains(observer.actions(), MediaSessionAction::kSeekTo));
+  EXPECT_FALSE(
+      base::Contains(observer.actions(), MediaSessionAction::kScrubTo));
+  EXPECT_FALSE(
+      base::Contains(observer.actions(), MediaSessionAction::kSeekForward));
+  EXPECT_FALSE(
+      base::Contains(observer.actions(), MediaSessionAction::kSeekBackward));
+
+  // However, if the website explicitly supports the action, then we will still
+  // route it.
+  mock_media_session_service().EnableAction(MediaSessionAction::kSeekTo);
+  FlushForTesting(GetMediaSession());
+
+  EXPECT_TRUE(base::Contains(observer.actions(), MediaSessionAction::kSeekTo));
+  EXPECT_FALSE(
+      base::Contains(observer.actions(), MediaSessionAction::kScrubTo));
+  EXPECT_FALSE(
+      base::Contains(observer.actions(), MediaSessionAction::kSeekForward));
+  EXPECT_FALSE(
+      base::Contains(observer.actions(), MediaSessionAction::kSeekBackward));
+}
+
 class MediaSessionImplWithMediaSessionClientTest : public MediaSessionImplTest {
  protected:
   TestMediaSessionClient client_;
@@ -1043,8 +1057,30 @@
                     /*playback_rate=*/0.0,
                     /*duration=*/base::TimeDelta::Max(),
                     /*position=*/base::TimeDelta(), /*end_of_media=*/false));
+
+      // Since we're now considered live, the seeking and scrubbing actions
+      // should no longer be available.
+      EXPECT_FALSE(
+          base::Contains(observer.actions(), MediaSessionAction::kSeekTo));
+      EXPECT_FALSE(
+          base::Contains(observer.actions(), MediaSessionAction::kScrubTo));
+      EXPECT_FALSE(
+          base::Contains(observer.actions(), MediaSessionAction::kSeekForward));
+      EXPECT_FALSE(base::Contains(observer.actions(),
+                                  MediaSessionAction::kSeekBackward));
     } else {
       EXPECT_EQ(**observer.session_position(), pos);
+
+      // If we're not considered live, then the seeking and scrubbing actions
+      // should still be available.
+      EXPECT_TRUE(
+          base::Contains(observer.actions(), MediaSessionAction::kSeekTo));
+      EXPECT_TRUE(
+          base::Contains(observer.actions(), MediaSessionAction::kScrubTo));
+      EXPECT_TRUE(
+          base::Contains(observer.actions(), MediaSessionAction::kSeekForward));
+      EXPECT_TRUE(base::Contains(observer.actions(),
+                                 MediaSessionAction::kSeekBackward));
     }
   }
 
diff --git a/content/browser/shared_storage/shared_storage_browsertest.cc b/content/browser/shared_storage/shared_storage_browsertest.cc
index 837b7fa2..af3374c6 100644
--- a/content/browser/shared_storage/shared_storage_browsertest.cc
+++ b/content/browser/shared_storage/shared_storage_browsertest.cc
@@ -870,7 +870,7 @@
               {"SharedStorageStalenessThreshold",
                TimeDeltaToString(base::Days(kStalenessThresholdDays))},
           }},
-         {blink::features::kSharedStorageAPIM124, {}}},
+         {blink::features::kSharedStorageAPIM125, {}}},
         /*disabled_features=*/{});
 
     fenced_frame_feature_.InitAndEnableFeature(blink::features::kFencedFrames);
diff --git a/content/browser/shared_storage/shared_storage_document_service_impl.cc b/content/browser/shared_storage/shared_storage_document_service_impl.cc
index aee9aab..f9831e6 100644
--- a/content/browser/shared_storage/shared_storage_document_service_impl.cc
+++ b/content/browser/shared_storage/shared_storage_document_service_impl.cc
@@ -115,9 +115,9 @@
     mojo::PendingAssociatedReceiver<blink::mojom::SharedStorageWorkletHost>
         worklet_host,
     CreateWorkletCallback callback) {
-  // A document can only create multiple worklets with `kSharedStorageAPIM124`
+  // A document can only create multiple worklets with `kSharedStorageAPIM125`
   // enabled.
-  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM124)) {
+  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM125)) {
     if (create_worklet_called_) {
       // This could indicate a compromised renderer, so let's terminate it.
       receiver_.ReportBadMessage("Attempted to create multiple worklets.");
@@ -130,8 +130,8 @@
   create_worklet_called_ = true;
 
   // A document can only create cross-origin worklets with
-  // `kSharedStorageAPIM124` enabled.
-  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM124) &&
+  // `kSharedStorageAPIM125` enabled.
+  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM125) &&
       !render_frame_host().GetLastCommittedOrigin().IsSameOriginWith(
           script_source_url)) {
     // This could indicate a compromised renderer, so let's terminate it.
diff --git a/content/browser/shared_storage/shared_storage_worklet_host_manager.cc b/content/browser/shared_storage/shared_storage_worklet_host_manager.cc
index 1e0d83e3..5de98fb 100644
--- a/content/browser/shared_storage/shared_storage_worklet_host_manager.cc
+++ b/content/browser/shared_storage/shared_storage_worklet_host_manager.cc
@@ -73,9 +73,9 @@
   auto worklet_hosts_it =
       attached_shared_storage_worklet_hosts_.find(document_service);
 
-  // A document can only create multiple worklets with `kSharedStorageAPIM124`
+  // A document can only create multiple worklets with `kSharedStorageAPIM125`
   // enabled.
-  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM124)) {
+  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM125)) {
     CHECK(worklet_hosts_it == attached_shared_storage_worklet_hosts_.end());
   }
 
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index 671eec05..3161cd58b 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -234,8 +234,8 @@
            kSetOnlyIfOverridden},
           {wf::EnableSharedStorageAPIM118,
            raw_ref(blink::features::kSharedStorageAPIM118), kDefault},
-          {wf::EnableSharedStorageAPIM124,
-           raw_ref(blink::features::kSharedStorageAPIM124), kDefault},
+          {wf::EnableSharedStorageAPIM125,
+           raw_ref(blink::features::kSharedStorageAPIM125), kDefault},
           {wf::EnableFedCmMultipleIdentityProviders,
            raw_ref(features::kFedCmMultipleIdentityProviders), kDefault},
           {wf::EnableFedCmSelectiveDisclosure,
@@ -675,15 +675,15 @@
     WebRuntimeFeatures::EnableSharedStorageAPIM118(false);
   }
 
-  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM124) ||
+  if (!base::FeatureList::IsEnabled(blink::features::kSharedStorageAPIM125) ||
       !base::FeatureList::IsEnabled(blink::features::kSharedStorageAPI)) {
-    LOG_IF(WARNING, WebRuntimeFeatures::IsSharedStorageAPIM124Enabled())
-        << "SharedStorage for M124+ cannot be enabled in this "
+    LOG_IF(WARNING, WebRuntimeFeatures::IsSharedStorageAPIM125Enabled())
+        << "SharedStorage for M125+ cannot be enabled in this "
            "configuration. Use --"
         << switches::kEnableFeatures << "="
         << blink::features::kSharedStorageAPI.name << ","
-        << blink::features::kSharedStorageAPIM124.name << " in addition.";
-    WebRuntimeFeatures::EnableSharedStorageAPIM124(false);
+        << blink::features::kSharedStorageAPIM125.name << " in addition.";
+    WebRuntimeFeatures::EnableSharedStorageAPIM125(false);
   }
 
   if (!base::FeatureList::IsEnabled(
diff --git a/content/test/data/interest_group/bidding_argument_validator.js b/content/test/data/interest_group/bidding_argument_validator.js
index 1bfcbd20..ed9c737 100644
--- a/content/test/data/interest_group/bidding_argument_validator.js
+++ b/content/test/data/interest_group/bidding_argument_validator.js
@@ -214,7 +214,7 @@
     throw 'Wrong topLevelSeller ' + browserSignals.topLevelSeller;
 
   if (isGenerateBid) {
-    if (Object.keys(browserSignals).length !== 10) {
+    if (Object.keys(browserSignals).length !== 9) {
       throw 'Wrong number of browser signals fields ' +
           JSON.stringify(browserSignals);
     }
@@ -231,8 +231,6 @@
     if (browserSignals.forDebuggingOnlyInCooldownOrLockout)
       throw 'Wrong forDebuggingOnlyInCooldownOrLockout ' +
           browserSignals.forDebuggingOnlyInCooldownOrLockout;
-    if (browserSignals.multiBidLimit !== 1)
-      throw 'Wrong multiBidLimit ' + browserSignals.multiBidLimit;
   } else {
     // FledgePassKAnonStatusToReportWin feature adds a new parameter
     // KAnonStatus to reportWin(), which is under a Finch trial for some enabled
diff --git a/content/test/data/interest_group/component_auction_bidding_argument_validator.js b/content/test/data/interest_group/component_auction_bidding_argument_validator.js
index 04d3ad5..165b21d7 100644
--- a/content/test/data/interest_group/component_auction_bidding_argument_validator.js
+++ b/content/test/data/interest_group/component_auction_bidding_argument_validator.js
@@ -194,7 +194,7 @@
     throw 'Wrong topLevelSeller ' + browserSignals.topLevelSeller;
 
   if (isGenerateBid) {
-    if (Object.keys(browserSignals).length !== 11) {
+    if (Object.keys(browserSignals).length !== 10) {
       throw 'Wrong number of browser signals fields ' +
           JSON.stringify(browserSignals);
     }
@@ -211,8 +211,6 @@
     if (browserSignals.forDebuggingOnlyInCooldownOrLockout)
       throw 'Wrong forDebuggingOnlyInCooldownOrLockout ' +
           browserSignals.forDebuggingOnlyInCooldownOrLockout;
-    if (browserSignals.multiBidLimit !== 1)
-      throw 'Wrong multiBidLimit ' + browserSignals.multiBidLimit;
   } else {
     // FledgePassKAnonStatusToReportWin feature adds a new parameter
     // KAnonStatus to reportWin(), which is under a Finch trial for some enabled
diff --git a/device/bluetooth/bluetooth_device.h b/device/bluetooth/bluetooth_device.h
index a5fe634..f63d5c2 100644
--- a/device/bluetooth/bluetooth_device.h
+++ b/device/bluetooth/bluetooth_device.h
@@ -110,6 +110,7 @@
     ERROR_DEVICE_UNCONNECTED = 11,
     ERROR_DOES_NOT_EXIST = 12,
     ERROR_INVALID_ARGS = 13,
+    ERROR_NON_AUTH_TIMEOUT = 14,
     NUM_CONNECT_ERROR_CODES,  // Keep as last enum.
   };
 
diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.mm b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
index 9229019..5bcbde96 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.mm
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
@@ -183,6 +183,7 @@
     IOBluetoothL2CAPChannel* channel) {
   DCHECK_EQ(channel_, channel);
   channel_ = nil;
+  [delegate_ resetOwner];
   delegate_ = nil;
   socket()->OnChannelClosed();
 }
diff --git a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
index 85d092a..97d1a46e 100644
--- a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
+++ b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
@@ -184,6 +184,7 @@
     IOBluetoothRFCOMMChannel* channel) {
   DCHECK_EQ(channel_, channel);
   channel_ = nil;
+  [delegate_ resetOwner];
   delegate_ = nil;
   socket()->OnChannelClosed();
 }
diff --git a/device/bluetooth/chromeos/bluetooth_utils.cc b/device/bluetooth/chromeos/bluetooth_utils.cc
index 4ff25ba..783cde53 100644
--- a/device/bluetooth/chromeos/bluetooth_utils.cc
+++ b/device/bluetooth/chromeos/bluetooth_utils.cc
@@ -181,6 +181,10 @@
     case ConnectionFailureReason::kFailed:
       [[fallthrough]];
     case ConnectionFailureReason::kInprogress:
+      [[fallthrough]];
+    case ConnectionFailureReason::kNotFound:
+      [[fallthrough]];
+    case ConnectionFailureReason::kBluetoothDisabled:
       const std::string result_histogram_name_prefix =
           "Bluetooth.ChromeOS.Pairing.Result";
       base::UmaHistogramEnumeration(
diff --git a/device/bluetooth/chromeos/bluetooth_utils.h b/device/bluetooth/chromeos/bluetooth_utils.h
index 7aacf74..1cec69a 100644
--- a/device/bluetooth/chromeos/bluetooth_utils.h
+++ b/device/bluetooth/chromeos/bluetooth_utils.h
@@ -56,7 +56,9 @@
   kAuthCanceled = 8,
   kAuthRejected = 9,
   kInprogress = 10,
-  kMaxValue = kInprogress
+  kNotFound = 11,
+  kBluetoothDisabled = 12,
+  kMaxValue = kBluetoothDisabled
 };
 
 // This enum is tied directly to a UMA enum defined in
diff --git a/device/bluetooth/floss/bluetooth_device_floss.cc b/device/bluetooth/floss/bluetooth_device_floss.cc
index 0de2a59b..226d358 100644
--- a/device/bluetooth/floss/bluetooth_device_floss.cc
+++ b/device/bluetooth/floss/bluetooth_device_floss.cc
@@ -309,7 +309,7 @@
 void BluetoothDeviceFloss::ConnectionIncomplete() {
   UpdateConnectingState(
       ConnectingState::kIdle,
-      BluetoothDevice::ConnectErrorCode::ERROR_DEVICE_NOT_READY);
+      BluetoothDevice::ConnectErrorCode::ERROR_NON_AUTH_TIMEOUT);
 }
 
 #if BUILDFLAG(IS_CHROMEOS)
diff --git a/device/bluetooth/public/mojom/adapter.mojom b/device/bluetooth/public/mojom/adapter.mojom
index 906b8c83..9a8a44c 100644
--- a/device/bluetooth/public/mojom/adapter.mojom
+++ b/device/bluetooth/public/mojom/adapter.mojom
@@ -29,6 +29,7 @@
   NOT_CONNECTED,
   DOES_NOT_EXIST,
   INVALID_ARGS,
+  NON_AUTH_TIMEOUT,
 };
 
 union LocalCharacteristicReadResult {
diff --git a/device/bluetooth/public/mojom/connect_result_type_converter.h b/device/bluetooth/public/mojom/connect_result_type_converter.h
index ac3b79c..c8813ab 100644
--- a/device/bluetooth/public/mojom/connect_result_type_converter.h
+++ b/device/bluetooth/public/mojom/connect_result_type_converter.h
@@ -50,6 +50,8 @@
         return bluetooth::mojom::ConnectResult::DOES_NOT_EXIST;
       case device::BluetoothDevice::ConnectErrorCode::ERROR_INVALID_ARGS:
         return bluetooth::mojom::ConnectResult::INVALID_ARGS;
+      case device::BluetoothDevice::ConnectErrorCode::ERROR_NON_AUTH_TIMEOUT:
+        return bluetooth::mojom::ConnectResult::NON_AUTH_TIMEOUT;
       case device::BluetoothDevice::ConnectErrorCode::NUM_CONNECT_ERROR_CODES:
         NOTREACHED();
         return bluetooth::mojom::ConnectResult::FAILED;
diff --git a/device/fido/BUILD.gn b/device/fido/BUILD.gn
index ad7d05a..32f50f3 100644
--- a/device/fido/BUILD.gn
+++ b/device/fido/BUILD.gn
@@ -163,6 +163,8 @@
       "enclave/transact.h",
       "enclave/types.cc",
       "enclave/types.h",
+      "enclave/verify/claim.cc",
+      "enclave/verify/claim.h",
       "enclave/verify/verify.h",
       "fido_authenticator.cc",
       "fido_authenticator.h",
diff --git a/device/fido/enclave/verify/claim.cc b/device/fido/enclave/verify/claim.cc
new file mode 100644
index 0000000..4bf28fe
--- /dev/null
+++ b/device/fido/enclave/verify/claim.cc
@@ -0,0 +1,21 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "device/fido/enclave/verify/claim.h"
+
+namespace device::enclave {
+
+Subject::Subject() = default;
+Subject::Subject(std::string name, std::map<std::string, std::string> digest)
+    : name(std::move(name)), digest(std::move(digest)) {}
+Subject::~Subject() = default;
+
+ClaimEvidence::ClaimEvidence() = default;
+ClaimEvidence::ClaimEvidence(std::optional<std::string> role,
+                             std::string uri,
+                             std::vector<uint8_t> digest)
+    : role(std::move(role)), uri(std::move(uri)), digest(std::move(digest)) {}
+ClaimEvidence::~ClaimEvidence() = default;
+
+}  // namespace device::enclave
diff --git a/device/fido/enclave/verify/claim.h b/device/fido/enclave/verify/claim.h
new file mode 100644
index 0000000..6a96af46
--- /dev/null
+++ b/device/fido/enclave/verify/claim.h
@@ -0,0 +1,67 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef DEVICE_FIDO_ENCLAVE_VERIFY_CLAIM_H_
+#define DEVICE_FIDO_ENCLAVE_VERIFY_CLAIM_H_
+
+#include <map>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "base/time/time.h"
+
+namespace device::enclave {
+
+struct Subject {
+  Subject(std::string name, std::map<std::string, std::string> digest);
+  Subject();
+  ~Subject();
+
+  std::string name;
+  std::map<std::string, std::string> digest;
+};
+
+template <typename T>
+struct Statement {
+  std::string type;
+  std::string predicate_type;
+  std::vector<Subject> subject;
+  T predicate;
+};
+
+struct ClaimEvidence {
+  ClaimEvidence(std::optional<std::string> role,
+                std::string uri,
+                std::vector<uint8_t> digest);
+  ClaimEvidence();
+  ~ClaimEvidence();
+
+  std::optional<std::string> role;
+  std::string uri;
+  std::vector<uint8_t> digest;
+};
+
+struct ClaimValidity {
+  base::Time not_before;
+  base::Time not_after;
+};
+
+template <typename T>
+struct ClaimPredicate {
+  std::string claim_type;
+  std::optional<T> claim_spec;
+  std::string usage;
+  base::Time issued_on;
+  std::optional<ClaimValidity> validity;
+  std::vector<ClaimEvidence> evidence;
+};
+
+struct Claimless {};
+
+typedef Statement<ClaimPredicate<Claimless>> EndorsementStatement;
+
+}  // namespace device::enclave
+
+#endif  // DEVICE_FIDO_ENCLAVE_VERIFY_CLAIM_H_
diff --git a/docs/updater/functional_spec.md b/docs/updater/functional_spec.md
index f9d4e8a..74fd731 100644
--- a/docs/updater/functional_spec.md
+++ b/docs/updater/functional_spec.md
@@ -825,11 +825,13 @@
 The updater also checks for policy updates when the `RunPeriodicTasks` RPC is
 invoked at periodic intervals.
 
+The maximum size of the token is 4K (Windows only).
+
 #### Windows
 The enrollment token is searched in the order:
-* The `EnrollmentToken` REG_SZ value from
+* The `EnrollmentToken` REG_BINARY value from
   `HKLM\Software\Policies\{COMPANY_SHORTNAME}\CloudManagement`
-* The `CloudManagementEnrollmentToken` REG_SZ value from
+* The `CloudManagementEnrollmentToken` REG_BINARY value from
   `HKLM\Software\Policies\{COMPANY_SHORTNAME}\{BROWSER_NAME}`
 
 The `EnrollmentMandatory` REG_DWORD value is also read from
@@ -862,9 +864,9 @@
 
 DM token is stored at:
 ##### Windows
-- The `dmtoken` REG_SZ value at path:
+- The `dmtoken` REG_BINARY value at path:
   `HKLM\Software\WOW6432Node\{COMPANY_SHORTNAME}\Enrollment\`
-- The `dmtoken` REG_SZ value at path:
+- The `dmtoken` REG_BINARY value at path:
   `HKLM64\Software\{COMPANY_SHORTNAME}\{BROWSER_NAME}\Enrollment\`. This is
   for backward compatibility.
 
diff --git a/extensions/browser/BUILD.gn b/extensions/browser/BUILD.gn
index 676cfb7..aac6dfa 100644
--- a/extensions/browser/BUILD.gn
+++ b/extensions/browser/BUILD.gn
@@ -1049,6 +1049,7 @@
       "//chromeos:test_support",
       "//chromeos/ash/components/dbus:test_support",
       "//chromeos/ash/components/dbus/audio",
+      "//chromeos/ash/components/dbus/debug_daemon",
       "//chromeos/ash/components/dbus/media_analytics",
       "//chromeos/ash/components/dbus/media_analytics:media_perception_proto",
       "//chromeos/ash/components/dbus/shill",
diff --git a/extensions/browser/api/bluetooth/bluetooth_private_api.cc b/extensions/browser/api/bluetooth/bluetooth_private_api.cc
index 72d7269..15cc0dc 100644
--- a/extensions/browser/api/bluetooth/bluetooth_private_api.cc
+++ b/extensions/browser/api/bluetooth/bluetooth_private_api.cc
@@ -129,6 +129,8 @@
       return bt_private::ConnectResultType::kDoesNotExist;
     case device::BluetoothDevice::ERROR_INVALID_ARGS:
       return bt_private::ConnectResultType::kInvalidArgs;
+    case device::BluetoothDevice::ERROR_NON_AUTH_TIMEOUT:
+      return bt_private::ConnectResultType::kNonAuthTimeout;
     case device::BluetoothDevice::NUM_CONNECT_ERROR_CODES:
       NOTREACHED();
       break;
diff --git a/extensions/browser/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc b/extensions/browser/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc
index 4a5fde3..d5907352 100644
--- a/extensions/browser/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc
+++ b/extensions/browser/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc
@@ -237,6 +237,8 @@
     case BluetoothDevice::ERROR_INVALID_ARGS:
       return extensions::BluetoothLowEnergyEventRouter::
           kStatusErrorInvalidArguments;
+    case BluetoothDevice::ERROR_NON_AUTH_TIMEOUT:
+      return extensions::BluetoothLowEnergyEventRouter::kStatusErrorTimeout;
     case BluetoothDevice::NUM_CONNECT_ERROR_CODES:
       NOTREACHED();
       return extensions::BluetoothLowEnergyEventRouter::
diff --git a/extensions/browser/api/feedback_private/BUILD.gn b/extensions/browser/api/feedback_private/BUILD.gn
index 5aa7f77..c8f1acd 100644
--- a/extensions/browser/api/feedback_private/BUILD.gn
+++ b/extensions/browser/api/feedback_private/BUILD.gn
@@ -45,8 +45,12 @@
 
     deps += [
       "//ash/public/cpp",
+      "//chromeos/ash/components/cryptohome",
+      "//chromeos/ash/components/dbus/debug_daemon",
       "//chromeos/ash/services/assistant/public/cpp",
       "//chromeos/ash/services/assistant/public/mojom",
+      "//components/account_id",
+      "//components/user_manager",
     ]
   }
 }
diff --git a/extensions/browser/api/feedback_private/DEPS b/extensions/browser/api/feedback_private/DEPS
index 89fb4ef9..370f538f 100644
--- a/extensions/browser/api/feedback_private/DEPS
+++ b/extensions/browser/api/feedback_private/DEPS
@@ -1,4 +1,7 @@
 include_rules = [
-  "+components/feedback",
   "+ash/public/cpp",
+  "+components/account_id",
+  "+components/feedback",
+  "+components/user_manager",
+  "+third_party/cros_system_api"
 ]
diff --git a/extensions/browser/api/feedback_private/feedback_service.cc b/extensions/browser/api/feedback_private/feedback_service.cc
index ab004d7..3c9b17a 100644
--- a/extensions/browser/api/feedback_private/feedback_service.cc
+++ b/extensions/browser/api/feedback_private/feedback_service.cc
@@ -12,6 +12,7 @@
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
+#include "base/functional/callback_forward.h"
 #include "base/logging.h"
 #include "base/memory/ref_counted.h"
 #include "base/metrics/histogram_functions.h"
@@ -34,7 +35,11 @@
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 #include "ash/public/cpp/assistant/controller/assistant_controller.h"
+#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
 #include "chromeos/ash/services/assistant/public/cpp/assistant_service.h"
+#include "components/account_id/account_id.h"
+#include "components/user_manager/user_manager.h"
+#include "third_party/cros_system_api/dbus/debugd/dbus-constants.h"
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
 namespace extensions {
@@ -54,15 +59,11 @@
     FILE_PATH_LITERAL("bluetooth/log.bz2.old");
 constexpr base::FilePath::CharType kBluetoothQualityReportFilePath[] =
     FILE_PATH_LITERAL("bluetooth/bluetooth_quality_report");
-constexpr base::FilePath::CharType kWifiDebugLogsFilePath[] =
-    FILE_PATH_LITERAL("wifi/iwlwifi_firmware_dumps.tar.zst");
 
 constexpr char kBluetoothLogsAttachmentName[] = "bluetooth_logs.bz2";
 constexpr char kBluetoothLogsAttachmentNameOld[] = "bluetooth_logs.old.bz2";
 constexpr char kBluetoothQualityReportAttachmentName[] =
     "bluetooth_quality_report";
-constexpr char kWifiDebugLogsAttachmentName[] =
-    "iwlwifi_firmware_dumps.tar.zst";
 
 constexpr char kLacrosHistogramsFilename[] = "lacros_histograms.zip";
 
@@ -80,23 +81,22 @@
   }
 }
 
-void LoadAttachmentsIfRequested(
-    scoped_refptr<feedback::FeedbackData> feedback_data,
-    const base::FilePath& root_path,
-    bool send_bluetooth_logs,
-    bool send_wifi_debug_logs) {
-  if (send_bluetooth_logs) {
-    AddAttachment(feedback_data, root_path, kBluetoothLogsFilePath,
-                  kBluetoothLogsAttachmentName);
-    AddAttachment(feedback_data, root_path, kBluetoothLogsFilePathOld,
-                  kBluetoothLogsAttachmentNameOld);
-    AddAttachment(feedback_data, root_path, kBluetoothQualityReportFilePath,
-                  kBluetoothQualityReportAttachmentName);
-  }
+void AttachBluetoothLogs(scoped_refptr<feedback::FeedbackData> feedback_data,
+                         const base::FilePath& root_path) {
+  AddAttachment(feedback_data, root_path, kBluetoothLogsFilePath,
+                kBluetoothLogsAttachmentName);
+  AddAttachment(feedback_data, root_path, kBluetoothLogsFilePathOld,
+                kBluetoothLogsAttachmentNameOld);
+  AddAttachment(feedback_data, root_path, kBluetoothQualityReportFilePath,
+                kBluetoothQualityReportAttachmentName);
+}
 
-  if (send_wifi_debug_logs) {
-    AddAttachment(feedback_data, root_path, kWifiDebugLogsFilePath,
-                  kWifiDebugLogsAttachmentName);
+// A new case must be added for every new log type. Otherwise the code should
+// not compile.
+std::string_view GetAttachmentName(debugd::FeedbackBinaryLogType log_type) {
+  switch (log_type) {
+    case debugd::WIFI_FIRMWARE_DUMP:
+      return "iwlwifi_firmware_dumps.tar.zst";
   }
 }
 #endif
@@ -290,20 +290,46 @@
                            std::move(compressed_histograms));
   }
 
-  // If at least one attachment is requested, invoke LoadAttachmentsIfRequested
-  // to add all requested attachments in a separate thread to avoid blocking the
-  // UI thread.
-  if (params.send_bluetooth_logs || params.send_wifi_debug_logs) {
+  auto barrier_closure =
+      base::BarrierClosure((params.send_bluetooth_logs ? 1 : 0) +
+                               (params.send_wifi_debug_logs ? 1 : 0),
+                           base::BindOnce(&FeedbackService::OnAllLogsFetched,
+                                          this, params, feedback_data));
+  // If bluetooth logs are requested, invoke AttachBluetoothLogs to add
+  // them in a separate thread to avoid blocking the UI thread.
+  if (params.send_bluetooth_logs) {
     base::ThreadPool::PostTaskAndReply(
         FROM_HERE, {base::MayBlock()},
-        base::BindOnce(&LoadAttachmentsIfRequested, feedback_data,
-                       log_file_root_, params.send_bluetooth_logs,
-                       params.send_wifi_debug_logs),
-        base::BindOnce(&FeedbackService::OnAllLogsFetched, this, params,
-                       feedback_data));
-  } else {
-    OnAllLogsFetched(params, feedback_data);
+        base::BindOnce(&AttachBluetoothLogs, feedback_data, log_file_root_),
+        barrier_closure);
   }
+
+  if (params.send_wifi_debug_logs) {
+    const user_manager::User* user =
+        user_manager::UserManager::Get()->GetActiveUser();
+    const auto account_identifier =
+        cryptohome::CreateAccountIdentifierFromAccountId(
+            user ? user->GetAccountId() : EmptyAccountId());
+
+    binary_log_files_reader_.GetFeedbackBinaryLogs(
+        account_identifier, debugd::FeedbackBinaryLogType::WIFI_FIRMWARE_DUMP,
+        base::BindOnce(&FeedbackService::OnBinaryLogFilesFetched, this, params,
+                       feedback_data, barrier_closure));
+  }
+}
+
+void FeedbackService::OnBinaryLogFilesFetched(
+    const FeedbackParams& params,
+    scoped_refptr<feedback::FeedbackData> feedback_data,
+    base::RepeatingClosure barrier_closure_callback,
+    feedback::BinaryLogFilesReader::BinaryLogsResponse binary_logs_response) {
+  if (binary_logs_response) {
+    for (auto& item : *binary_logs_response) {
+      feedback_data->AddFile(GetAttachmentName(item.first).data(),
+                             std::move(item.second));
+    }
+  }
+  std::move(barrier_closure_callback).Run();
 }
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
diff --git a/extensions/browser/api/feedback_private/feedback_service.h b/extensions/browser/api/feedback_private/feedback_service.h
index ef8a43f..f9bbbfa40 100644
--- a/extensions/browser/api/feedback_private/feedback_service.h
+++ b/extensions/browser/api/feedback_private/feedback_service.h
@@ -5,6 +5,8 @@
 #ifndef EXTENSIONS_BROWSER_API_FEEDBACK_PRIVATE_FEEDBACK_SERVICE_H_
 #define EXTENSIONS_BROWSER_API_FEEDBACK_PRIVATE_FEEDBACK_SERVICE_H_
 
+#include <memory>
+
 #include "base/files/file_path.h"
 #include "base/functional/bind.h"
 #include "base/memory/raw_ptr.h"
@@ -16,6 +18,10 @@
 #include "components/feedback/system_logs/system_logs_fetcher.h"
 #include "extensions/browser/api/feedback_private/feedback_private_delegate.h"
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chromeos/ash/components/dbus/debug_daemon/binary_log_files_reader.h"
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+
 namespace feedback {
 class FeedbackData;
 }  // namespace feedback
@@ -94,27 +100,37 @@
       const FeedbackParams& params,
       scoped_refptr<feedback::FeedbackData> feedback_data,
       std::unique_ptr<system_logs::SystemLogsResponse> sys_info);
+  void OnAllLogsFetched(const FeedbackParams& params,
+                        scoped_refptr<feedback::FeedbackData> feedback_data);
+
 #if BUILDFLAG(IS_CHROMEOS_ASH)
   // Gets logs that aren't covered by FetchSystemInformation, but should be
   // included in the feedback report. These currently consist of the Intel Wi-Fi
   // debug logs (if they exist).
   void FetchExtraLogs(const FeedbackParams& params,
                       scoped_refptr<feedback::FeedbackData> feedback_data);
+  // Receive binary log files fetched from debugd.
+  void OnBinaryLogFilesFetched(
+      const FeedbackParams& params,
+      scoped_refptr<feedback::FeedbackData> feedback_data,
+      base::RepeatingClosure barrier_closure_callback,
+      feedback::BinaryLogFilesReader::BinaryLogsResponse binary_logs);
   void OnExtraLogsFetched(const FeedbackParams& params,
                           scoped_refptr<feedback::FeedbackData> feedback_data);
   void OnLacrosHistogramsFetched(
       const FeedbackParams& params,
       scoped_refptr<feedback::FeedbackData> feedback_data,
       const std::string& compressed_histograms);
-  // Root file path for log files. It can be overwritten for testing purpose.
-  base::FilePath log_file_root_{FILE_PATH_LITERAL("/var/log/")};
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
-  void OnAllLogsFetched(const FeedbackParams& params,
-                        scoped_refptr<feedback::FeedbackData> feedback_data);
 
   raw_ptr<content::BrowserContext, AcrossTasksDanglingUntriaged>
       browser_context_;
   raw_ptr<FeedbackPrivateDelegate, AcrossTasksDanglingUntriaged> delegate_;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  // Root file path for log files. It can be overwritten for testing purpose.
+  base::FilePath log_file_root_{FILE_PATH_LITERAL("/var/log/")};
+  feedback::BinaryLogFilesReader binary_log_files_reader_;
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 };
 
 }  // namespace extensions
diff --git a/extensions/browser/api/feedback_private/feedback_service_unittest.cc b/extensions/browser/api/feedback_private/feedback_service_unittest.cc
index 648d6be..a905201 100644
--- a/extensions/browser/api/feedback_private/feedback_service_unittest.cc
+++ b/extensions/browser/api/feedback_private/feedback_service_unittest.cc
@@ -25,6 +25,13 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
+#include "components/user_manager/fake_user_manager.h"
+#include "components/user_manager/scoped_user_manager.h"
+#include "components/user_manager/user_manager.h"
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+
 namespace extensions {
 
 using feedback::FeedbackData;
@@ -99,16 +106,26 @@
 };
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
-bool AttachmentExists(const std::string& name,
-                      const scoped_refptr<FeedbackData>& feedback_data) {
+const FeedbackCommon::AttachedFile* FindAttachment(
+    std::string_view name,
+    const scoped_refptr<FeedbackData>& feedback_data) {
   size_t num_attachments = feedback_data->attachments();
   for (size_t i = 0; i < num_attachments; i++) {
     const FeedbackCommon::AttachedFile* file = feedback_data->attachment(i);
-    if (!std::strcmp(name.c_str(), file->name.c_str())) {
-      return true;
+    if (file->name == name) {
+      return file;
     }
   }
-  return false;
+  return nullptr;
+}
+
+void VerifyAttachment(std::string_view name,
+                      std::string_view data,
+                      const scoped_refptr<FeedbackData>& feedback_data) {
+  const auto* attachment = FindAttachment(name, feedback_data);
+  ASSERT_TRUE(attachment);
+  EXPECT_EQ(name, attachment->name);
+  EXPECT_EQ(data, attachment->data);
 }
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
@@ -126,6 +143,11 @@
         test_shared_loader_factory_);
     feedback_data_ = base::MakeRefCounted<FeedbackData>(
         mock_uploader_->AsWeakPtr(), nullptr);
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    auto fake_user_manager = std::make_unique<user_manager::FakeUserManager>();
+    scoped_user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
+        std::move(fake_user_manager));
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
   }
 
   ~FeedbackServiceTest() override = default;
@@ -185,18 +207,27 @@
     EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
     EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
 
+    if (send_wifi_debug_logs) {
+      ash::DebugDaemonClient::InitializeFake();
+    }
     auto feedback_service = base::MakeRefCounted<FeedbackService>(
         browser_context(), mock_delegate.get());
     feedback_service->SetLogFilesRootPathForTesting(scoped_temp_dir_.GetPath());
 
     RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
+    if (ash::DebugDaemonClient::Get()) {
+      ash::DebugDaemonClient::Shutdown();
+    }
     EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
 
     // Verify the attachment is added if and only if send_wifi_debug_logs is
     // true.
-    EXPECT_EQ(
-        send_wifi_debug_logs,
-        AttachmentExists("iwlwifi_firmware_dumps.tar.zst", feedback_data_));
+    constexpr char kWifiDumpName[] = "iwlwifi_firmware_dumps.tar.zst";
+    if (send_wifi_debug_logs) {
+      VerifyAttachment(kWifiDumpName, "TestData", feedback_data_);
+    } else {
+      EXPECT_FALSE(FindAttachment(kWifiDumpName, feedback_data_));
+    }
   }
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
@@ -209,6 +240,10 @@
     task_environment()->RunUntilIdle();
   }
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+
   base::ScopedTempDir scoped_temp_dir_;
   network::TestURLLoaderFactory test_url_loader_factory_;
   scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
diff --git a/extensions/common/api/bluetooth_private.idl b/extensions/common/api/bluetooth_private.idl
index 59ab4d89..33084d1 100644
--- a/extensions/common/api/bluetooth_private.idl
+++ b/extensions/common/api/bluetooth_private.idl
@@ -58,7 +58,8 @@
     alreadyExists,
     notConnected,
     doesNotExist,
-    invalidArgs
+    invalidArgs,
+    nonAuthTimeout
   };
 
   // Valid pairing responses.
diff --git a/extensions/common/mojom/api_permission_id.mojom b/extensions/common/mojom/api_permission_id.mojom
index 5b72acba..a09ad78 100644
--- a/extensions/common/mojom/api_permission_id.mojom
+++ b/extensions/common/mojom/api_permission_id.mojom
@@ -282,6 +282,7 @@
   kEnterpriseKioskInput = 255,
   kOdfsConfigPrivate = 256,
   kChromeOSManagementAudio = 257,
+  kChromeOSDiagnosticsNetworkInfoForMlab = 258,
 
   // Add new entries at the end of the enum and be sure to update the
   // "ExtensionPermission3" enum in
diff --git a/infra/config/generated/testing/variants.pyl b/infra/config/generated/testing/variants.pyl
index fcd166a..3a46b1e 100644
--- a/infra/config/generated/testing/variants.pyl
+++ b/infra/config/generated/testing/variants.pyl
@@ -267,32 +267,32 @@
   },
   'LACROS_VERSION_SKEW_CANARY': {
     'identifier': 'Lacros version skew testing ash canary',
-    'description': 'Run with ash-chrome version 125.0.6415.0',
+    'description': 'Run with ash-chrome version 125.0.6416.0',
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome',
     ],
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v125.0.6415.0',
-          'revision': 'version:125.0.6415.0',
+          'location': 'lacros_version_skew_tests_v125.0.6416.0',
+          'revision': 'version:125.0.6416.0',
         },
       ],
     },
   },
   'LACROS_VERSION_SKEW_DEV': {
     'identifier': 'Lacros version skew testing ash dev',
-    'description': 'Run with ash-chrome version 125.0.6398.0',
+    'description': 'Run with ash-chrome version 125.0.6411.0',
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome',
     ],
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v125.0.6398.0',
-          'revision': 'version:125.0.6398.0',
+          'location': 'lacros_version_skew_tests_v125.0.6411.0',
+          'revision': 'version:125.0.6411.0',
         },
       ],
     },
diff --git a/infra/config/targets/lacros-version-skew-variants.json b/infra/config/targets/lacros-version-skew-variants.json
index ca06615..25b411b 100644
--- a/infra/config/targets/lacros-version-skew-variants.json
+++ b/infra/config/targets/lacros-version-skew-variants.json
@@ -1,32 +1,32 @@
 {
   "LACROS_VERSION_SKEW_CANARY": {
     "args": [
-      "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+      "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
     ],
-    "description": "Run with ash-chrome version 125.0.6415.0",
+    "description": "Run with ash-chrome version 125.0.6416.0",
     "identifier": "Lacros version skew testing ash canary",
     "swarming": {
       "cipd_packages": [
         {
           "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-          "location": "lacros_version_skew_tests_v125.0.6415.0",
-          "revision": "version:125.0.6415.0"
+          "location": "lacros_version_skew_tests_v125.0.6416.0",
+          "revision": "version:125.0.6416.0"
         }
       ]
     }
   },
   "LACROS_VERSION_SKEW_DEV": {
     "args": [
-      "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+      "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
     ],
-    "description": "Run with ash-chrome version 125.0.6398.0",
+    "description": "Run with ash-chrome version 125.0.6411.0",
     "identifier": "Lacros version skew testing ash dev",
     "swarming": {
       "cipd_packages": [
         {
           "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-          "location": "lacros_version_skew_tests_v125.0.6398.0",
-          "revision": "version:125.0.6398.0"
+          "location": "lacros_version_skew_tests_v125.0.6411.0",
+          "revision": "version:125.0.6411.0"
         }
       ]
     }
diff --git a/internal b/internal
index 3dff879..f50f32a 160000
--- a/internal
+++ b/internal
@@ -1 +1 @@
-Subproject commit 3dff8792f0d51d1790b512505334cdc668e54606
+Subproject commit f50f32ae0208beb1302fec22582ab6c8d36ba5eb
diff --git a/ios/chrome/browser/ui/autofill/BUILD.gn b/ios/chrome/browser/ui/autofill/BUILD.gn
index daa8be6..b16cb09a6 100644
--- a/ios/chrome/browser/ui/autofill/BUILD.gn
+++ b/ios/chrome/browser/ui/autofill/BUILD.gn
@@ -197,6 +197,7 @@
     "//build:branding_buildflags",
     "//components/autofill/core/common:features",
     "//components/autofill/ios/browser:autofill_test_bundle_data",
+    "//components/autofill/ios/common",
     "//components/strings",
     "//components/sync/base:features",
     "//ios/chrome/app/strings",
diff --git a/ios/chrome/browser/ui/autofill/authentication/BUILD.gn b/ios/chrome/browser/ui/autofill/authentication/BUILD.gn
index 06a379a1..d94947e5 100644
--- a/ios/chrome/browser/ui/autofill/authentication/BUILD.gn
+++ b/ios/chrome/browser/ui/autofill/authentication/BUILD.gn
@@ -41,6 +41,7 @@
     "//ios/chrome/browser/shared/ui/table_view:table_view",
     "//ios/chrome/browser/shared/ui/table_view:utils",
     "//ios/chrome/browser/ui/autofill/cells:cells",
+    "//ios/chrome/common/ui/colors:colors",
     "//ui/base:base",
   ]
   frameworks = [ "UIKit.framework" ]
@@ -178,3 +179,19 @@
   sources = [ "card_unmask_authentication_selection_mutator_bridge_target.h" ]
   public_deps = [ "//ios/chrome/browser/autofill/model/authentication:card_unmask_challenge_option_ios" ]
 }
+
+source_set("eg2_tests") {
+  configs += [ "//build/config/ios:xctest_config" ]
+  testonly = true
+  sources = [ "card_unmask_authentication_egtest.mm" ]
+  deps = [
+    "//components/autofill/core/browser:test_support",
+    "//components/strings:components_strings_grit",
+    "//ios/chrome/app/strings",
+    "//ios/chrome/browser/ui/autofill:eg_test_support+eg2",
+    "//ios/chrome/test/earl_grey:eg_test_support+eg2",
+    "//ios/testing/earl_grey:eg_test_support+eg2",
+    "//net:test_support",
+  ]
+  frameworks = [ "UIKit.framework" ]
+}
diff --git a/ios/chrome/browser/ui/autofill/authentication/card_unmask_authentication_egtest.mm b/ios/chrome/browser/ui/autofill/authentication/card_unmask_authentication_egtest.mm
new file mode 100644
index 0000000..092bb20
--- /dev/null
+++ b/ios/chrome/browser/ui/autofill/authentication/card_unmask_authentication_egtest.mm
@@ -0,0 +1,275 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+#import <XCTest/XCTest.h>
+
+#import "base/strings/sys_string_conversions.h"
+#import "components/autofill/core/common/autofill_payments_features.h"
+#import "components/strings/grit/components_strings.h"
+#import "ios/chrome/browser/ui/autofill/autofill_app_interface.h"
+#import "ios/chrome/grit/ios_strings.h"
+#import "ios/chrome/test/earl_grey/chrome_actions.h"
+#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
+#import "ios/chrome/test/earl_grey/chrome_matchers.h"
+#import "ios/chrome/test/earl_grey/chrome_test_case.h"
+#import "ios/testing/earl_grey/earl_grey_test.h"
+#import "ios/testing/earl_grey/matchers.h"
+#import "net/test/embedded_test_server/default_handlers.h"
+#import "ui/base/l10n/l10n_util.h"
+#import "ui/base/l10n/l10n_util_mac.h"
+
+namespace {
+
+// The test page url.
+const char kCreditCardUrl[] = "/credit_card.html";
+
+// A string on the credit_card.html page used to know when the page has loaded.
+const char kAutofillTestString[] = "Autofill Test";
+
+// The name of the card name form input.
+const char kFormCardName[] = "CCName";
+
+}  // namespace
+
+// The url to intercept in order to inject card unmask responses. These tests
+// do not make requests to the real server.
+NSString* const kUnmaskCardRequestUrl =
+    @"https://payments.google.com/payments/apis-secure/creditcardservice/"
+    @"getrealpan?s7e_suffix=chromewallet";
+
+// The fake response from the payment server when OTP, email and CVC card unmask
+// options are available.
+NSString* const kUnmaskCardResponseSuccessOtpAndEmailAndCvc =
+    @"{\"context_token\":\"__fake_context_token__\",\"idv_challenge_options\":["
+    @"{\"sms_otp_challenge_option\":{\"challenge_id\":"
+    @"\"JGQ1YTkxM2ZjLWY4YTAtMTFlZS1hMmFhLWZmYjYwNWVjODcwMwo=\",\"masked_phone_"
+    @"number\":\"*******1234\",\"otp_length\":6}},{\"email_otp_challenge_"
+    @"option\":{\"challenge_id\":"
+    @"\"JDNhNTdlMzVhLWY4YTEtMTFlZS1hOTUwLWZiNzY3ZWM4ZWY3ZAo=\",\"masked_email_"
+    @"address\":\"a***b@gmail.com\",\"otp_length\":6}},{\"cvc_challenge_"
+    @"option\":{\"challenge_id\":\"hardcoded_3CSC_challenge_id\",\"cvc_"
+    @"length\":3,\"cvc_position\":\"CVC_POSITION_BACK\"}}]}";
+
+// The masked phone number associated with the OTP card unmask option.
+NSString* const kUnmaskOptionMaskedPhoneNumber = @"*******1234";
+
+// The masked email associated with the email card unmask option.
+NSString* const kUnmaskOptionMaskedEmailAddress = @"a***b@gmail.com";
+
+// The length of the cvc challenge as shown in the challenge message.
+NSString* const kUnmaskOptionCvcLength = @"3";
+
+// Matcher for a navigation bar with the "Verification" title.
+id<GREYMatcher> CardUnmaskPromptNavigationBarTitle() {
+  return chrome_test_util::NavigationBarTitleWithAccessibilityLabelId(
+      IDS_AUTOFILL_CARD_UNMASK_PROMPT_NAVIGATION_TITLE_VERIFICATION);
+}
+
+// Matcher for the text message challenge option label.
+id<GREYMatcher> CardUnmaskTextMessageChallengeOptionLabel() {
+  return chrome_test_util::StaticTextWithAccessibilityLabelId(
+      IDS_AUTOFILL_AUTHENTICATION_MODE_GET_TEXT_MESSAGE);
+}
+
+// Matcher for the CVC challenge option label.
+id<GREYMatcher> CardUnmaskCvcChallengeOptionLabel() {
+  return chrome_test_util::StaticTextWithAccessibilityLabelId(
+      IDS_AUTOFILL_AUTHENTICATION_MODE_SECURITY_CODE);
+}
+
+// Matcher for the "Send" button.
+id<GREYMatcher> CardUnmaskAuthenticationSelectionSendButton() {
+  return grey_allOf(
+      chrome_test_util::ButtonWithAccessibilityLabelId(
+          IDS_AUTOFILL_CARD_UNMASK_AUTHENTICATION_SELECTION_DIALOG_OK_BUTTON_LABEL_SEND),
+      grey_not(grey_accessibilityTrait(UIAccessibilityTraitNotEnabled)), nil);
+}
+
+// Matcher for the "Cancel" button.
+id<GREYMatcher> CardUnmaskAuthenticationSelectionCancelButton() {
+  return grey_allOf(
+      chrome_test_util::CancelButton(),
+      grey_not(grey_accessibilityTrait(UIAccessibilityTraitNotEnabled)), nil);
+}
+
+@interface CardUnmaskAuthenticationSelectionEgtest : ChromeTestCase
+@end
+
+@implementation CardUnmaskAuthenticationSelectionEgtest {
+  NSString* _enrolledCardNameAndLastFour;
+}
+
+#pragma mark - Setup
+
+- (AppLaunchConfiguration)appConfigurationForTestCase {
+  AppLaunchConfiguration config;
+  config.features_enabled.push_back(
+      autofill::features::kAutofillEnableVirtualCards);
+  return config;
+}
+
+- (void)setUp {
+  [super setUp];
+  [AutofillAppInterface setUpSaveCardInfobarEGTestHelper];
+  _enrolledCardNameAndLastFour =
+      [AutofillAppInterface saveMaskedCreditCardEnrolledInVirtualCard];
+  [self setUpServer];
+  [self setUpTestPage];
+}
+
+- (void)setUpServer {
+  net::test_server::RegisterDefaultHandlers(self.testServer);
+  GREYAssertTrue(self.testServer->Start(), @"Failed to start test server.");
+}
+
+- (void)setUpTestPage {
+  [ChromeEarlGrey loadURL:self.testServer->GetURL(kCreditCardUrl)];
+  [ChromeEarlGrey waitForWebStateContainingText:kAutofillTestString];
+
+  [AutofillAppInterface considerCreditCardFormSecureForTesting];
+}
+
+- (void)tearDown {
+  [AutofillAppInterface clearAllServerDataForTesting];
+  [AutofillAppInterface tearDownSaveCardInfobarEGTestHelper];
+  [super tearDown];
+}
+
+- (void)showAuthenticationSelection {
+  // Tap on the card name field in the web content.
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
+      performAction:chrome_test_util::TapWebElementWithId(kFormCardName)];
+
+  // Wait for the payments bottom sheet to appear.
+  id<GREYMatcher> paymentsBottomSheetVirtualCard = grey_accessibilityID(
+      [NSString stringWithFormat:@"%@ %@", _enrolledCardNameAndLastFour,
+                                 @"Virtual card"]);
+  [ChromeEarlGrey
+      waitForUIElementToAppearWithMatcher:paymentsBottomSheetVirtualCard];
+  [[EarlGrey selectElementWithMatcher:paymentsBottomSheetVirtualCard]
+      performAction:grey_tap()];
+  [[EarlGrey selectElementWithMatcher:
+                 chrome_test_util::StaticTextWithAccessibilityLabelId(
+                     IDS_IOS_PAYMENT_BOTTOM_SHEET_CONTINUE)]
+      performAction:grey_tap()];
+
+  // Wait for the progress dialog to appear.
+  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:
+                      chrome_test_util::StaticTextWithAccessibilityLabelId(
+                          IDS_AUTOFILL_CARD_UNMASK_PROGRESS_DIALOG_TITLE)];
+
+  // Inject the card unmask response with card unmask options.
+  [AutofillAppInterface
+      setPaymentsResponse:kUnmaskCardResponseSuccessOtpAndEmailAndCvc
+               forRequest:kUnmaskCardRequestUrl
+            withErrorCode:net::HTTP_OK];
+
+  // Wait for the card unmask authentication selection to appear.
+  [ChromeEarlGrey
+      waitForUIElementToAppearWithMatcher:CardUnmaskPromptNavigationBarTitle()];
+}
+
+- (void)testCardUnmaskAuthenticationSelectionIsShownForVirtualCard {
+  [self showAuthenticationSelection];
+
+  // Verify that the card unmask prompt was shown.
+  [[EarlGrey
+      selectElementWithMatcher:
+          chrome_test_util::StaticTextWithAccessibilityLabelId(
+              IDS_AUTOFILL_CARD_AUTH_SELECTION_DIALOG_TITLE_MULTIPLE_OPTIONS)]
+      assertWithMatcher:grey_sufficientlyVisible()];
+  [[EarlGrey
+      selectElementWithMatcher:
+          chrome_test_util::StaticTextWithAccessibilityLabelId(
+              IDS_AUTOFILL_CARD_UNMASK_AUTHENTICATION_SELECTION_DIALOG_ISSUER_CONFIRMATION_TEXT)]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Verify that the card unmask options are shown starting with text message
+  // (SMS OTP).
+  [[EarlGrey
+      selectElementWithMatcher:CardUnmaskTextMessageChallengeOptionLabel()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::
+                                          StaticTextWithAccessibilityLabel(
+                                              kUnmaskOptionMaskedPhoneNumber)]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Verify that the email OTP unmask option is shown.
+  [[EarlGrey selectElementWithMatcher:
+                 chrome_test_util::StaticTextWithAccessibilityLabelId(
+                     IDS_AUTOFILL_AUTHENTICATION_MODE_GET_EMAIL)]
+      assertWithMatcher:grey_sufficientlyVisible()];
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::
+                                          StaticTextWithAccessibilityLabel(
+                                              kUnmaskOptionMaskedEmailAddress)]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Verify that the CVC unmask option is shown.
+  [[EarlGrey selectElementWithMatcher:CardUnmaskCvcChallengeOptionLabel()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+  [[EarlGrey
+      selectElementWithMatcher:
+          chrome_test_util::StaticTextWithAccessibilityLabel(l10n_util::GetNSStringF(
+              IDS_AUTOFILL_CARD_UNMASK_AUTHENTICATION_SELECTION_DIALOG_CVC_CHALLENGE_INFO,
+              base::SysNSStringToUTF16(kUnmaskOptionCvcLength),
+              l10n_util::GetStringUTF16(
+                  IDS_AUTOFILL_CARD_UNMASK_PROMPT_SECURITY_CODE_POSITION_BACK_OF_CARD)))]
+      assertWithMatcher:grey_sufficientlyVisible()];
+}
+
+- (void)testCardUnmaskAuthenticationSelectionCancel {
+  [self showAuthenticationSelection];
+
+  // Tap the cancel button.
+  [[EarlGrey
+      selectElementWithMatcher:CardUnmaskAuthenticationSelectionCancelButton()]
+      performAction:grey_tap()];
+
+  // Expect the card unmask authentication selection view to disappear.
+  [ChromeEarlGrey waitForUIElementToDisappearWithMatcher:
+                      CardUnmaskPromptNavigationBarTitle()];
+}
+
+- (void)testCardUnmaskAuthenticationSelectionAcceptanceButtonLabel {
+  [self showAuthenticationSelection];
+
+  // Verify selecting text message sets the acceptance button label to "Send".
+  [[EarlGrey
+      selectElementWithMatcher:CardUnmaskTextMessageChallengeOptionLabel()]
+      performAction:grey_tap()];
+  [[EarlGrey
+      selectElementWithMatcher:CardUnmaskAuthenticationSelectionSendButton()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Verify selecting CVC sets the acceptance button label to "Continue".
+  [[EarlGrey selectElementWithMatcher:CardUnmaskCvcChallengeOptionLabel()]
+      performAction:grey_tap()];
+  [[EarlGrey
+      selectElementWithMatcher:
+          grey_allOf(
+              chrome_test_util::ButtonWithAccessibilityLabelId(
+                  IDS_AUTOFILL_CARD_UNMASK_AUTHENTICATION_SELECTION_DIALOG_OK_BUTTON_LABEL_CONTINUE),
+              grey_not(grey_accessibilityTrait(UIAccessibilityTraitNotEnabled)),
+              nil)] assertWithMatcher:grey_sufficientlyVisible()];
+}
+
+- (void)testCardUnmaskAuthenticationSelectionShowsActivityIndicatorView {
+  [self showAuthenticationSelection];
+
+  // Select the text message otp challenge option.
+  [[EarlGrey
+      selectElementWithMatcher:CardUnmaskTextMessageChallengeOptionLabel()]
+      performAction:grey_tap()];
+  [[EarlGrey
+      selectElementWithMatcher:CardUnmaskAuthenticationSelectionSendButton()]
+      performAction:grey_tap()];
+
+  // Verify the activity indicator has been set.
+  [[EarlGrey
+      selectElementWithMatcher:grey_kindOfClassName(@"UIActivityIndicatorView")]
+      assertWithMatcher:grey_sufficientlyVisible()];
+}
+
+@end
diff --git a/ios/chrome/browser/ui/autofill/authentication/card_unmask_authentication_selection_mediator.mm b/ios/chrome/browser/ui/autofill/authentication/card_unmask_authentication_selection_mediator.mm
index d808d6f..abe051c 100644
--- a/ios/chrome/browser/ui/autofill/authentication/card_unmask_authentication_selection_mediator.mm
+++ b/ios/chrome/browser/ui/autofill/authentication/card_unmask_authentication_selection_mediator.mm
@@ -41,7 +41,15 @@
 }
 
 CardUnmaskAuthenticationSelectionMediator::
-    ~CardUnmaskAuthenticationSelectionMediator() = default;
+    ~CardUnmaskAuthenticationSelectionMediator() {
+  if (!was_dismissed_ && model_controller_) {
+    // Our coordinator is stopping (e.g. closing Chromium). We must call
+    // OnDialogClosed on the model_controller_ before this mediator is
+    // destroyed.
+    model_controller_->OnDialogClosed(
+        /*user_closed_dialog=*/true, /*server_success=*/false);
+  }
+}
 
 // Implementation of CardUnmaskAuthenticationSelectionMutatorBridgeTarget
 // follows:
diff --git a/ios/chrome/browser/ui/autofill/authentication/otp_input_dialog_view_controller.mm b/ios/chrome/browser/ui/autofill/authentication/otp_input_dialog_view_controller.mm
index 0ab0abc..5498eb3 100644
--- a/ios/chrome/browser/ui/autofill/authentication/otp_input_dialog_view_controller.mm
+++ b/ios/chrome/browser/ui/autofill/authentication/otp_input_dialog_view_controller.mm
@@ -8,16 +8,19 @@
 #import "components/strings/grit/components_strings.h"
 #import "ios/chrome/browser/shared/ui/list_model/list_model.h"
 #import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item.h"
+#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
 #import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
 #import "ios/chrome/browser/ui/autofill/authentication/otp_input_dialog_content.h"
 #import "ios/chrome/browser/ui/autofill/authentication/otp_input_dialog_mutator.h"
 #import "ios/chrome/browser/ui/autofill/cells/card_unmask_header_item.h"
+#import "ios/chrome/common/ui/colors/semantic_color_names.h"
 #import "ui/base/l10n/l10n_util.h"
 
 namespace {
 
 typedef NS_ENUM(NSInteger, SectionIdentifier) {
   SectionIdentifierContent = kSectionIdentifierEnumZero,
+  SectionIdentifierError,
 };
 
 typedef NS_ENUM(NSInteger, ItemIdentifier) {
@@ -37,6 +40,7 @@
   UITableViewDiffableDataSource<NSNumber*, NSNumber*>* _dataSource;
   BOOL _contentSet;
   NSString* _inputValue;
+  NSString* _errorTitle;
 }
 
 - (instancetype)init {
@@ -53,25 +57,48 @@
       initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
                            target:self
                            action:@selector(didTapCancelButton)];
-  self.navigationItem.rightBarButtonItem =
-      [[UIBarButtonItem alloc] initWithTitle:_content.confirmButtonLabel
-                                       style:UIBarButtonItemStyleDone
-                                      target:self
-                                      action:@selector(didTapConfirmButton)];
-  // Enable the confirm button only after a valid OTP has been entered.
-  self.navigationItem.rightBarButtonItem.enabled = NO;
-  self.tableView.allowsSelection = NO;
+  self.navigationItem.rightBarButtonItem = [self createConfirmButton];
   [self loadModel];
 }
 
 #pragma mark - UITableViewDelegate
 
+- (CGFloat)tableView:(UITableView*)tableView
+    heightForHeaderInSection:(NSInteger)section {
+  SectionIdentifier sectionIdentifier = static_cast<SectionIdentifier>(
+      [_dataSource sectionIdentifierForIndex:section].integerValue);
+  switch (sectionIdentifier) {
+    case SectionIdentifierContent:
+      return UITableViewAutomaticDimension;
+    case SectionIdentifierError:
+      return ChromeTableViewHeightForHeaderInSection(sectionIdentifier);
+  }
+}
+
 - (UIView*)tableView:(UITableView*)tableView
     viewForHeaderInSection:(NSInteger)section {
-  CardUnmaskHeaderView* view =
-      DequeueTableViewHeaderFooter<CardUnmaskHeaderView>(self.tableView);
-  view.titleLabel.text = _content.windowTitle;
-  return view;
+  SectionIdentifier sectionIdentifier = static_cast<SectionIdentifier>(
+      [_dataSource sectionIdentifierForIndex:section].integerValue);
+  switch (sectionIdentifier) {
+    case SectionIdentifierContent: {
+      CardUnmaskHeaderView* view =
+          DequeueTableViewHeaderFooter<CardUnmaskHeaderView>(self.tableView);
+      view.titleLabel.text = _content.windowTitle;
+      return view;
+    }
+    case SectionIdentifierError: {
+      if (!_errorTitle) {
+        return nil;
+      }
+      TableViewTextHeaderFooterView* errorMessage =
+          DequeueTableViewHeaderFooter<TableViewTextHeaderFooterView>(
+              self.tableView);
+      [errorMessage setSubtitle:_errorTitle
+                      withColor:[UIColor colorNamed:kRedColor]];
+      [errorMessage setForceIndents:YES];
+      return errorMessage;
+    }
+  }
 }
 
 #pragma mark - PaymentsSuggestionBottomSheetConsumer
@@ -88,13 +115,20 @@
 }
 
 - (void)showPendingState {
-  // TODO(crbug.com/303715678): Handle pending state (after the confirm button
-  // is clicked).
+  UIActivityIndicatorView* activityIndicator = [[UIActivityIndicatorView alloc]
+      initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
+  UIBarButtonItem* pendingButton =
+      [[UIBarButtonItem alloc] initWithCustomView:activityIndicator];
+  self.navigationItem.rightBarButtonItem = pendingButton;
+  [activityIndicator startAnimating];
+  [self.tableView setUserInteractionEnabled:NO];
 }
 
 - (void)showInvalidState:(NSString*)invalidLabelText {
-  // TODO(crbug.com/303715678): Handle error state (after the confirm button is
-  // clicked and server returns a result).
+  self.navigationItem.rightBarButtonItem = [self createConfirmButton];
+  _errorTitle = invalidLabelText;
+  [self.tableView setUserInteractionEnabled:YES];
+  [self reloadModel];
 }
 
 #pragma mark - Private
@@ -104,6 +138,7 @@
   CHECK(_contentSet);
   RegisterTableViewHeaderFooter<CardUnmaskHeaderView>(self.tableView);
   RegisterTableViewCell<TableViewTextEditCell>(self.tableView);
+  RegisterTableViewHeaderFooter<TableViewTextHeaderFooterView>(self.tableView);
   __weak __typeof(self) weakSelf = self;
   _dataSource = [[UITableViewDiffableDataSource alloc]
       initWithTableView:self.tableView
@@ -121,7 +156,15 @@
   [snapshot appendSectionsWithIdentifiers:@[ @(SectionIdentifierContent) ]];
   [snapshot appendItemsWithIdentifiers:@[ @(ItemTypeTextField) ]
              intoSectionWithIdentifier:@(SectionIdentifierContent)];
-  [_dataSource applySnapshot:snapshot animatingDifferences:NO];
+  [snapshot appendSectionsWithIdentifiers:@[ @(SectionIdentifierError) ]];
+  [_dataSource applySnapshot:snapshot animatingDifferences:YES];
+}
+
+- (void)reloadModel {
+  NSDiffableDataSourceSnapshot* snapshot = [_dataSource snapshot];
+  [snapshot reconfigureItemsWithIdentifiers:@[ @(ItemTypeTextField) ]];
+  [snapshot reloadSectionsWithIdentifiers:@[ @(SectionIdentifierError) ]];
+  [_dataSource applySnapshot:snapshot animatingDifferences:YES];
 }
 
 // Returns the appropriate cell for the table view.
@@ -131,7 +174,14 @@
   TableViewTextEditCell* cell =
       DequeueTableViewCell<TableViewTextEditCell>(self.tableView);
   [cell setIdentifyingIcon:nil];
-  [cell setIcon:TableViewTextEditItemIconTypeEdit];
+  if (_errorTitle) {
+    [cell setIcon:TableViewTextEditItemIconTypeError];
+    cell.textField.text = _inputValue;
+    cell.textField.textColor = [UIColor colorNamed:kRedColor];
+  } else {
+    [cell setIcon:TableViewTextEditItemIconTypeEdit];
+    cell.textField.textColor = [UIColor colorNamed:kTextPrimaryColor];
+  }
   cell.textField.placeholder = _content.textFieldPlaceholder;
   [cell.textField addTarget:self
                      action:@selector(textFieldDidChange:)
@@ -143,6 +193,12 @@
 }
 
 - (void)textFieldDidChange:(UITextField*)textField {
+  // Reset error state.
+  if (_errorTitle) {
+    _errorTitle = nil;
+    [self reloadModel];
+  }
+
   _inputValue = textField.text;
   [self didChangeOtpInputText];
 }
@@ -163,4 +219,15 @@
   [_mutator onOtpInputChanges:_inputValue];
 }
 
+- (UIBarButtonItem*)createConfirmButton {
+  UIBarButtonItem* confirmButton =
+      [[UIBarButtonItem alloc] initWithTitle:_content.confirmButtonLabel
+                                       style:UIBarButtonItemStyleDone
+                                      target:self
+                                      action:@selector(didTapConfirmButton)];
+  // Enable the confirm button only after a valid OTP has been entered.
+  confirmButton.enabled = NO;
+  return confirmButton;
+}
+
 @end
diff --git a/ios/chrome/browser/ui/autofill/save_card_infobar_egtest.mm b/ios/chrome/browser/ui/autofill/save_card_infobar_egtest.mm
index dcb7674..26e31c0 100644
--- a/ios/chrome/browser/ui/autofill/save_card_infobar_egtest.mm
+++ b/ios/chrome/browser/ui/autofill/save_card_infobar_egtest.mm
@@ -5,8 +5,10 @@
 #import <memory>
 
 #import "base/test/ios/wait_util.h"
+#import "base/time/time.h"
 #import "build/branding_buildflags.h"
 #import "components/autofill/core/common/autofill_features.h"
+#import "components/autofill/ios/common/features.h"
 #import "components/strings/grit/components_strings.h"
 #import "ios/chrome/browser/metrics/model/metrics_app_interface.h"
 #import "ios/chrome/browser/ui/autofill/autofill_app_interface.h"
@@ -104,12 +106,8 @@
 // Some tests are not compatible with explicit save prompts for addresses.
 - (AppLaunchConfiguration)appConfigurationForTestCase {
   AppLaunchConfiguration config;
-  if ([self isRunningTest:@selector(testUserData_LocalSave_UserAccepts)] ||
-      [self
-          isRunningTest:@selector(testOfferLocalSave_FullData_RequestFails)] ||
-      [self isRunningTest:@selector(testUserData_LocalSave_UserDeclines)] ||
-      [self isRunningTest:@selector
-            (testOfferLocalSave_FullData_PaymentsDeclines)]) {
+  if ([self isRunningTest:@selector(testStickySavePromptJourney)]) {
+    config.features_enabled.push_back(kAutofillStickyInfobarIos);
   }
   return config;
 }
@@ -605,4 +603,81 @@
       @"Save card infobar should not show.");
 }
 
+// Tests the sticky credit card prompt journey where the prompt remains there
+// when navigating without an explicit user gesture, and then the prompt is
+// dismissed when navigating with a user gesture. Test with the credit card save
+// prompt but the type of credit card prompt doesn't matter in this test case.
+- (void)testStickySavePromptJourney {
+  const GURL testPageURL =
+      web::test::HttpServer::MakeUrl(kCreditCardUploadForm);
+
+  [ChromeEarlGrey loadURL:testPageURL];
+
+  // Set up the Google Payments server response.
+  [AutofillAppInterface setPaymentsResponse:kResponseGetUploadDetailsFailure
+                                 forRequest:kURLGetUploadDetailsRequest
+                              withErrorCode:net::HTTP_OK];
+
+  [AutofillAppInterface resetEventWaiterForEvents:@[
+    @(CreditCardSaveManagerObserverEvent::kOnDecideToRequestUploadSaveCalled),
+    @(CreditCardSaveManagerObserverEvent::
+          kOnReceivedGetUploadDetailsResponseCalled),
+    @(CreditCardSaveManagerObserverEvent::kOnOfferLocalSaveCalled)
+  ]
+                                          timeout:kWaitForDownloadTimeout];
+  [self fillAndSubmitForm];
+  GREYAssertTrue([AutofillAppInterface waitForEvents],
+                 @"Event was not triggered");
+
+  // Wait until the save card infobar becomes visible.
+  GREYAssert(
+      [self waitForUIElementToAppearWithMatcher:LocalBannerLabelsMatcher()],
+      @"Save card infobar failed to show.");
+
+  [AutofillAppInterface resetEventWaiterForEvents:@[
+    @(CreditCardSaveManagerObserverEvent::kOnStrikeChangeCompleteCalled)
+  ]
+                                          timeout:kWaitForDownloadTimeout];
+
+  {
+    // Reloading page from script shouldn't dismiss the infobar.
+    NSString* script = @"location.reload();";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+  {
+    // Assigning url from script to the page aka open an url shouldn't dismiss
+    // the infobar.
+    NSString* script = @"window.location.assign(window.location.href);";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+  {
+    // Pushing new history entry without reloading content shouldn't dismiss the
+    // infobar.
+    NSString* script = @"history.pushState({}, '', 'destination2.html');";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+  {
+    // Replacing history entry without reloading content shouldn't dismiss the
+    // infobar.
+    NSString* script = @"history.replaceState({}, '', 'destination3.html');";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+
+  // Wait some time for things to settle.
+  base::test::ios::SpinRunLoopWithMinDelay(base::Milliseconds(200));
+
+  // Verify that the prompt is still there after the non-user initiated
+  // navigations.
+  [[EarlGrey selectElementWithMatcher:LocalBannerLabelsMatcher()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Navigate with an emulated user gesture.
+  [ChromeEarlGrey loadURL:testPageURL];
+
+  // Wait until the save card infobar disappears.
+  GREYAssertTrue(
+      [self waitForUIElementToDisappearWithMatcher:LocalBannerLabelsMatcher()],
+      @"Save card infobar failed to disappear.");
+}
+
 @end
diff --git a/ios/chrome/browser/ui/autofill/save_profile_egtest.mm b/ios/chrome/browser/ui/autofill/save_profile_egtest.mm
index a3f1594c..a963c2b 100644
--- a/ios/chrome/browser/ui/autofill/save_profile_egtest.mm
+++ b/ios/chrome/browser/ui/autofill/save_profile_egtest.mm
@@ -7,7 +7,9 @@
 #import "base/strings/sys_string_conversions.h"
 #import "base/strings/utf_string_conversions.h"
 #import "base/test/ios/wait_util.h"
+#import "base/time/time.h"
 #import "components/autofill/core/common/autofill_features.h"
+#import "components/autofill/ios/common/features.h"
 #import "components/strings/grit/components_strings.h"
 #import "ios/chrome/browser/signin/model/fake_system_identity.h"
 #import "ios/chrome/browser/ui/authentication/signin_earl_grey.h"
@@ -114,14 +116,18 @@
   config.features_disabled.push_back(
       autofill::features::test::kAutofillServerCommunication);
 
+  if ([self isRunningTest:@selector(testStickySavePromptJourney)]) {
+    config.features_enabled.push_back(kAutofillStickyInfobarIos);
+  }
+
   return config;
 }
 
 #pragma mark - Test helper methods
 
 // Fills the president profile in the form by clicking on the button, submits
-// the form and accepts the save address banner.
-- (void)fillPresidentProfileAndShowSaveModal {
+// the form to the save address profile infobar.
+- (void)fillPresidentProfileAndShowSaveInfobar {
   GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
   [ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];
 
@@ -132,6 +138,10 @@
   [ChromeEarlGrey tapWebStateElementWithID:@"fill_profile_president"];
   [ChromeEarlGrey tapWebStateElementWithID:@"submit_profile"];
   [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
+}
+
+- (void)fillPresidentProfileAndShowSaveModal {
+  [self fillPresidentProfileAndShowSaveInfobar];
 
   // Accept the banner.
   [[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
@@ -357,4 +367,50 @@
   [SigninEarlGrey signOut];
 }
 
+// Tests the sticky address prompt journey where the prompt remains there when
+// navigating without an explicit user gesture, and then the prompt is dismissed
+// when navigating with a user gesture. Test with the address save prompt but
+// the type of address prompt doesn't matter in this test case.
+- (void)testStickySavePromptJourney {
+  [self fillPresidentProfileAndShowSaveInfobar];
+
+  {
+    // Reloading page from script shouldn't dismiss the infobar.
+    NSString* script = @"location.reload();";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+  {
+    // Assigning url from script to the page aka open an url shouldn't dismiss
+    // the infobar.
+    NSString* script = @"window.location.assign(window.location.href);";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+  {
+    // Pushing new history entry without reloading content shouldn't dismiss the
+    // infobar.
+    NSString* script = @"history.pushState({}, '', 'destination2.html');";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+  {
+    // Replacing history entry without reloading content shouldn't dismiss the
+    // infobar.
+    NSString* script = @"history.replaceState({}, '', 'destination3.html');";
+    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
+  }
+
+  // Wait some time for things to settle.
+  base::test::ios::SpinRunLoopWithMinDelay(base::Milliseconds(200));
+
+  // Verify that the prompt is still there after the non-user initiated
+  // navigations.
+  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
+                                          kInfobarBannerViewIdentifier)]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Navigate with an emulated user gesture and verify that dismisses the
+  // prompt.
+  [ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];
+  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
+}
+
 @end
diff --git a/ios/chrome/test/earl_grey2/BUILD.gn b/ios/chrome/test/earl_grey2/BUILD.gn
index 62b5a65..feee13e5 100644
--- a/ios/chrome/test/earl_grey2/BUILD.gn
+++ b/ios/chrome/test/earl_grey2/BUILD.gn
@@ -186,6 +186,7 @@
     "//ios/chrome/browser/plus_addresses/ui:eg2_tests",
     "//ios/chrome/browser/qr_scanner/ui_bundled:eg2_tests",
     "//ios/chrome/browser/settings/model/sync/utils:eg2_tests",
+    "//ios/chrome/browser/ui/autofill/authentication:eg2_tests",
     "//ios/chrome/browser/ui/autofill/bottom_sheet:eg2_tests",
     "//ios/chrome/browser/ui/autofill/form_input_accessory:eg2_tests",
     "//ios/chrome/browser/ui/bring_android_tabs:eg2_tests",
diff --git a/ios_internal b/ios_internal
index b52f647..a5aea78 160000
--- a/ios_internal
+++ b/ios_internal
@@ -1 +1 @@
-Subproject commit b52f647176b2a69101f532235ff01e26a03708c2
+Subproject commit a5aea7848cb458595e3fc715475154bc54ca53cd
diff --git a/ipc/BUILD.gn b/ipc/BUILD.gn
index 5c85e84..bd8c193 100644
--- a/ipc/BUILD.gn
+++ b/ipc/BUILD.gn
@@ -185,6 +185,12 @@
   ]
 }
 
+mojom("test_mojom") {
+  testonly = true
+  sources = [ "ipc_channel_mojo_unittest.test-mojom" ]
+  public_deps = [ "//mojo/public/mojom/base" ]
+}
+
 mojom_component("mojom") {
   output_prefix = "ipc_mojom"
   macro_prefix = "IPC_MOJOM"
@@ -307,6 +313,7 @@
       ":protobuf_support",
       ":run_all_unittests",
       ":test_interfaces",
+      ":test_mojom",
       ":test_proto",
       ":test_support",
       "//base",
diff --git a/ipc/ipc_channel_mojo_unittest.cc b/ipc/ipc_channel_mojo_unittest.cc
index 893ebeb..2ecb28f5 100644
--- a/ipc/ipc_channel_mojo_unittest.cc
+++ b/ipc/ipc_channel_mojo_unittest.cc
@@ -34,12 +34,14 @@
 #include "base/synchronization/waitable_event.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/test/test_io_thread.h"
 #include "base/test/test_shared_memory_util.h"
 #include "base/test/test_timeouts.h"
 #include "base/threading/thread.h"
 #include "build/build_config.h"
+#include "ipc/ipc_channel_mojo_unittest.test-mojom.h"
 #include "ipc/ipc_message.h"
 #include "ipc/ipc_message_utils.h"
 #include "ipc/ipc_mojo_handle_attachment.h"
@@ -53,6 +55,7 @@
 #include "ipc/urgent_message_observer.h"
 #include "mojo/public/cpp/bindings/associated_receiver.h"
 #include "mojo/public/cpp/bindings/associated_remote.h"
+#include "mojo/public/cpp/bindings/features.h"
 #include "mojo/public/cpp/bindings/lib/validation_errors.h"
 #include "mojo/public/cpp/bindings/pending_associated_receiver.h"
 #include "mojo/public/cpp/bindings/self_owned_associated_receiver.h"
@@ -66,6 +69,7 @@
 #include "ipc/ipc_platform_file_attachment_posix.h"
 #endif
 
+namespace ipc_channel_mojo_unittest {
 namespace {
 
 void SendString(IPC::Sender* sender, const std::string& str) {
@@ -1384,6 +1388,75 @@
   DestroyProxy();
 }
 
+class ListenerWithClumsyBinder : public IPC::Listener {
+ public:
+  ListenerWithClumsyBinder() = default;
+  ~ListenerWithClumsyBinder() override = default;
+
+  void RunUntilClientQuit() { run_loop_.Run(); }
+
+ private:
+  // IPC::Listener:
+  bool OnMessageReceived(const IPC::Message& message) override {
+    run_loop_.Quit();
+    return true;
+  }
+
+  void OnAssociatedInterfaceRequest(
+      const std::string& interface_name,
+      mojo::ScopedInterfaceEndpointHandle handle) override {
+    // Ignore and drop the endpoint so it's closed.
+  }
+
+  base::RunLoop run_loop_;
+};
+
+TEST_F(IPCChannelProxyMojoTest, DropAssociatedReceiverWithSyncCallInFlight) {
+  // Regression test for https://crbug.com/331636067. Verifies that endpoint
+  // lifetime is properly managed when associated endpoints are serialized into
+  // a message that gets dropped before transmission.
+
+  Init("SyncCallToDroppedReceiver");
+  ListenerWithClumsyBinder listener;
+  CreateProxy(&listener);
+  RunProxy();
+  listener.RunUntilClientQuit();
+  EXPECT_TRUE(WaitForClientShutdown());
+  DestroyProxy();
+}
+
+DEFINE_IPC_CHANNEL_MOJO_TEST_CLIENT_WITH_CUSTOM_FIXTURE(
+    SyncCallToDroppedReceiver,
+    ChannelProxyClient) {
+  // Force-enable the fix, since ipc_tests doesn't initialize FeatureList.
+  const base::test::ScopedFeatureList kFeatures(
+      mojo::features::kMojoFixAssociatedHandleLeak);
+
+  DummyListener listener;
+  CreateProxy(&listener);
+  RunProxy();
+
+  mojo::AssociatedRemote<mojom::Binder> binder;
+  proxy()->GetRemoteAssociatedInterface(
+      binder.BindNewEndpointAndPassReceiver());
+
+  // Wait for disconnection to be observed. This way we know any subsequent
+  // outgoing messages on `binder` will not be sent.
+  base::RunLoop loop;
+  binder.set_disconnect_handler(loop.QuitClosure());
+  loop.Run();
+
+  // Send another endpoint over. This receiver will be dropped, and the remote
+  // should be properly notified of peer closure to terminate the sync call. The
+  // call should return false (because no reply), but shouldn't hang.
+  mojo::AssociatedRemote<mojom::Binder> another_binder;
+  binder->Bind(another_binder.BindNewEndpointAndPassReceiver());
+  EXPECT_FALSE(another_binder->Ping());
+
+  SendString(proxy(), "ok bye");
+  DestroyProxy();
+}
+
 // TODO(https://crbug.com/1500560): Disabled for flaky behavior of forced
 // process termination. Will be re-enabled with a fix.
 TEST_F(IPCChannelProxyMojoTest, DISABLED_SyncAssociatedInterfacePipeError) {
@@ -2061,3 +2134,4 @@
 }
 
 }  // namespace
+}  // namespace ipc_channel_mojo_unittest
diff --git a/ipc/ipc_channel_mojo_unittest.test-mojom b/ipc/ipc_channel_mojo_unittest.test-mojom
new file mode 100644
index 0000000..7df7018d
--- /dev/null
+++ b/ipc/ipc_channel_mojo_unittest.test-mojom
@@ -0,0 +1,12 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module ipc_channel_mojo_unittest.mojom;
+
+import "mojo/public/mojom/base/generic_pending_associated_receiver.mojom";
+
+interface Binder {
+  Bind(mojo_base.mojom.GenericPendingAssociatedReceiver receiver);
+  [Sync] Ping() => ();
+};
diff --git a/ipc/ipc_mojo_bootstrap.cc b/ipc/ipc_mojo_bootstrap.cc
index 951c629..56c023e 100644
--- a/ipc/ipc_mojo_bootstrap.cc
+++ b/ipc/ipc_mojo_bootstrap.cc
@@ -41,6 +41,7 @@
 #include "mojo/public/cpp/bindings/associated_group.h"
 #include "mojo/public/cpp/bindings/associated_group_controller.h"
 #include "mojo/public/cpp/bindings/connector.h"
+#include "mojo/public/cpp/bindings/features.h"
 #include "mojo/public/cpp/bindings/interface_endpoint_client.h"
 #include "mojo/public/cpp/bindings/interface_endpoint_controller.h"
 #include "mojo/public/cpp/bindings/interface_id.h"
@@ -407,6 +408,22 @@
       control_message_proxy_.NotifyPeerEndpointClosed(id, reason);
   }
 
+  void NotifyLocalEndpointOfPeerClosure(mojo::InterfaceId id) override {
+    if (!base::FeatureList::IsEnabled(
+            mojo::features::kMojoFixAssociatedHandleLeak)) {
+      return;
+    }
+
+    if (!task_runner_->RunsTasksInCurrentSequence()) {
+      task_runner_->PostTask(
+          FROM_HERE, base::BindOnce(&ChannelAssociatedGroupController::
+                                        NotifyLocalEndpointOfPeerClosure,
+                                    base::WrapRefCounted(this), id));
+      return;
+    }
+    OnPeerAssociatedEndpointClosed(id, std::nullopt);
+  }
+
   mojo::InterfaceEndpointController* AttachEndpointClient(
       const mojo::ScopedInterfaceEndpointHandle& handle,
       mojo::InterfaceEndpointClient* client,
diff --git a/media/video/renderable_gpu_memory_buffer_video_frame_pool.cc b/media/video/renderable_gpu_memory_buffer_video_frame_pool.cc
index 245ea07c..a7ac0f10 100644
--- a/media/video/renderable_gpu_memory_buffer_video_frame_pool.cc
+++ b/media/video/renderable_gpu_memory_buffer_video_frame_pool.cc
@@ -17,12 +17,14 @@
 #include "base/task/sequenced_task_runner.h"
 #include "build/build_config.h"
 #include "cc/base/math_util.h"
+#include "components/viz/common/resources/shared_image_format_utils.h"
 #include "gpu/GLES2/gl2extchromium.h"
 #include "gpu/command_buffer/client/client_shared_image.h"
 #include "gpu/command_buffer/client/gpu_memory_buffer_manager.h"
 #include "gpu/command_buffer/client/shared_image_interface.h"
 #include "gpu/command_buffer/common/gpu_memory_buffer_support.h"
 #include "gpu/command_buffer/common/shared_image_usage.h"
+#include "media/base/format_utils.h"
 #include "media/base/media_switches.h"
 #include "media/base/video_frame.h"
 
@@ -157,8 +159,9 @@
       format_(format),
       coded_size_(coded_size),
       color_space_(color_space) {
-  // Currently only support ARGB and NV12.
-  CHECK(format == PIXEL_FORMAT_ARGB || format == PIXEL_FORMAT_NV12);
+  // Currently only support ARGB, ABGR and NV12.
+  CHECK(format == PIXEL_FORMAT_ARGB || format == PIXEL_FORMAT_ABGR ||
+        format == PIXEL_FORMAT_NV12);
 }
 
 FrameResources::~FrameResources() {
@@ -172,22 +175,12 @@
   }
 }
 
-gfx::BufferFormat GetBufferFormatForVideoPixelFormat(VideoPixelFormat format) {
-  switch (format) {
-    case PIXEL_FORMAT_ARGB:
-      return gfx::BufferFormat::RGBA_8888;
-    case PIXEL_FORMAT_NV12:
-      return gfx::BufferFormat::YUV_420_BIPLANAR;
-    default:
-      NOTREACHED_NORETURN();
-  }
-}
-
 gfx::Size GetBufferSizeInPixelsForVideoPixelFormat(
     VideoPixelFormat format,
     const gfx::Size& coded_size) {
   switch (format) {
     case PIXEL_FORMAT_ARGB:
+    case PIXEL_FORMAT_ABGR:
       return coded_size;
     case PIXEL_FORMAT_NV12:
       // Align number of rows to 2, because it's required by YUV_420_BIPLANAR
@@ -214,7 +207,7 @@
       ;
 
   const gfx::BufferFormat buffer_format =
-      GetBufferFormatForVideoPixelFormat(format_);
+      VideoPixelFormatToGfxBufferFormat(format_).value();
 
   const gfx::Size buffer_size_in_pixels =
       GetBufferSizeInPixelsForVideoPixelFormat(format_, coded_size_);
@@ -288,11 +281,14 @@
       }
       return true;
     }
+    case PIXEL_FORMAT_ABGR:
     case PIXEL_FORMAT_ARGB: {
+      const viz::SharedImageFormat image_format =
+          viz::GetSinglePlaneSharedImageFormat(buffer_format);
       shared_images_[0] = context->CreateSharedImage(
-          gpu_memory_buffer_.get(), viz::SinglePlaneFormat::kRGBA_8888,
-          color_space_, kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType,
-          kSharedImageUsage, mailbox_holders_[0].sync_token);
+          gpu_memory_buffer_.get(), image_format, color_space_,
+          kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType, kSharedImageUsage,
+          mailbox_holders_[0].sync_token);
       if (shared_images_[0]) {
         mailbox_holders_[0].mailbox = shared_images_[0]->mailbox();
         mailbox_holders_[0].texture_target =
diff --git a/media/video/renderable_gpu_memory_buffer_video_frame_pool_unittest.cc b/media/video/renderable_gpu_memory_buffer_video_frame_pool_unittest.cc
index d45f6fed..5c15e0ff 100644
--- a/media/video/renderable_gpu_memory_buffer_video_frame_pool_unittest.cc
+++ b/media/video/renderable_gpu_memory_buffer_video_frame_pool_unittest.cc
@@ -15,6 +15,7 @@
 #include "components/viz/test/test_context_provider.h"
 #include "gpu/command_buffer/client/client_shared_image.h"
 #include "gpu/config/gpu_finch_features.h"
+#include "media/base/format_utils.h"
 #include "media/base/media_switches.h"
 #include "media/base/video_frame.h"
 #include "media/video/fake_gpu_memory_buffer.h"
@@ -32,24 +33,13 @@
     case media::PIXEL_FORMAT_NV12:
       return gfx::ColorSpace::CreateREC709();
     case media::PIXEL_FORMAT_ARGB:
+    case media::PIXEL_FORMAT_ABGR:
       return gfx::ColorSpace::CreateSRGB();
     default:
       NOTREACHED_NORETURN();
   }
 }
 
-gfx::BufferFormat GetBufferFormatForVideoPixelFormat(
-    media::VideoPixelFormat format) {
-  switch (format) {
-    case media::PIXEL_FORMAT_ARGB:
-      return gfx::BufferFormat::RGBA_8888;
-    case media::PIXEL_FORMAT_NV12:
-      return gfx::BufferFormat::YUV_420_BIPLANAR;
-    default:
-      NOTREACHED_NORETURN();
-  }
-}
-
 class FakeContext : public RenderableGpuMemoryBufferVideoFramePool::Context {
  public:
   FakeContext()
@@ -159,6 +149,12 @@
       }
       case PIXEL_FORMAT_ARGB: {
         EXPECT_CALL(*context,
+                    DoCreateSharedImage(viz::SinglePlaneFormat::kBGRA_8888, _,
+                                        _, _, _, _, _));
+        break;
+      }
+      case PIXEL_FORMAT_ABGR: {
+        EXPECT_CALL(*context,
                     DoCreateSharedImage(viz::SinglePlaneFormat::kRGBA_8888, _,
                                         _, _, _, _, _));
         break;
@@ -174,6 +170,7 @@
       case PIXEL_FORMAT_NV12: {
         return nv12_multi_plane_ ? 1 : 2;
       }
+      case PIXEL_FORMAT_ABGR:
       case PIXEL_FORMAT_ARGB: {
         return 1;
       }
@@ -192,7 +189,8 @@
 TEST_P(RenderableGpuMemoryBufferVideoFramePoolTest, SimpleLifetimes) {
   base::test::SingleThreadTaskEnvironment task_environment;
   const gfx::Size size0(128, 256);
-  const gfx::BufferFormat format = GetBufferFormatForVideoPixelFormat(format_);
+  const gfx::BufferFormat format =
+      VideoPixelFormatToGfxBufferFormat(format_).value();
   const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);
 
   base::WeakPtr<FakeContext> context;
@@ -252,7 +250,8 @@
 TEST_P(RenderableGpuMemoryBufferVideoFramePoolTest, FrameFreedAfterPool) {
   base::test::SingleThreadTaskEnvironment task_environment;
   const gfx::Size size0(128, 256);
-  const gfx::BufferFormat format = GetBufferFormatForVideoPixelFormat(format_);
+  const gfx::BufferFormat format =
+      VideoPixelFormatToGfxBufferFormat(format_).value();
   const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);
 
   base::WeakPtr<FakeContext> context;
@@ -317,7 +316,8 @@
   base::test::TaskEnvironment task_environment{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
   const gfx::Size size0(128, 256);
-  const gfx::BufferFormat format = GetBufferFormatForVideoPixelFormat(format_);
+  const gfx::BufferFormat format =
+      VideoPixelFormatToGfxBufferFormat(format_).value();
   const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);
 
   // Create a pool and several frames on the main thread.
@@ -384,7 +384,8 @@
 
 TEST_P(RenderableGpuMemoryBufferVideoFramePoolTest, RespectSizeAndColorSpace) {
   base::test::SingleThreadTaskEnvironment task_environment;
-  const gfx::BufferFormat format = GetBufferFormatForVideoPixelFormat(format_);
+  const gfx::BufferFormat format =
+      VideoPixelFormatToGfxBufferFormat(format_).value();
   const gfx::Size size0(128, 256);
   const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);
   const gfx::Size size1(256, 256);
@@ -469,7 +470,8 @@
         testing::Bool(),
 #endif
         testing::Values(media::VideoPixelFormat::PIXEL_FORMAT_NV12,
-                        media::VideoPixelFormat::PIXEL_FORMAT_ARGB)));
+                        media::VideoPixelFormat::PIXEL_FORMAT_ARGB,
+                        media::VideoPixelFormat::PIXEL_FORMAT_ABGR)));
 
 }  // namespace
 
diff --git a/mojo/public/cpp/bindings/associated_group_controller.h b/mojo/public/cpp/bindings/associated_group_controller.h
index f2b65497..7603ea3 100644
--- a/mojo/public/cpp/bindings/associated_group_controller.h
+++ b/mojo/public/cpp/bindings/associated_group_controller.h
@@ -49,6 +49,13 @@
       InterfaceId id,
       const std::optional<DisconnectReason>& reason) = 0;
 
+  // Notifies the controller that the peer of interface `id` has been closed.
+  // Normally this notification comes from a remote client on the underlying
+  // pipe, but in some cases the remote client may never have been made aware of
+  // the new associated interface and will not be able to send such a
+  // notification.
+  virtual void NotifyLocalEndpointOfPeerClosure(InterfaceId id) = 0;
+
   // Attaches a client to the specified endpoint to send and receive messages.
   // The returned object is still owned by the controller. It must only be used
   // on the same sequence as this call, and only before the client is detached
diff --git a/mojo/public/cpp/bindings/features.cc b/mojo/public/cpp/bindings/features.cc
index 7f2950b..ec6ff28b 100644
--- a/mojo/public/cpp/bindings/features.cc
+++ b/mojo/public/cpp/bindings/features.cc
@@ -41,5 +41,12 @@
 #endif
 );
 
+// Enables a bugfix for https://crbug.com/331636067. This is a very old bug, and
+// this flag will be used to understand the stability and performance impact of
+// the fix, if any.
+BASE_FEATURE(kMojoFixAssociatedHandleLeak,
+             "MojoFixAssociatedHandleLeak",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 }  // namespace features
 }  // namespace mojo
diff --git a/mojo/public/cpp/bindings/features.h b/mojo/public/cpp/bindings/features.h
index 7ffc8bfc..c402f74 100644
--- a/mojo/public/cpp/bindings/features.h
+++ b/mojo/public/cpp/bindings/features.h
@@ -19,6 +19,9 @@
 COMPONENT_EXPORT(MOJO_CPP_BINDINGS_BASE)
 BASE_DECLARE_FEATURE(kMojoPredictiveAllocation);
 
+COMPONENT_EXPORT(MOJO_CPP_BINDINGS_BASE)
+BASE_DECLARE_FEATURE(kMojoFixAssociatedHandleLeak);
+
 }  // namespace features
 }  // namespace mojo
 
diff --git a/mojo/public/cpp/bindings/lib/interface_endpoint_client.cc b/mojo/public/cpp/bindings/lib/interface_endpoint_client.cc
index 8a676d0..a32052f7 100644
--- a/mojo/public/cpp/bindings/lib/interface_endpoint_client.cc
+++ b/mojo/public/cpp/bindings/lib/interface_endpoint_client.cc
@@ -598,8 +598,10 @@
   // to work properly.
   message->SerializeHandles(handle_.group_controller());
 
-  if (encountered_error_)
+  if (encountered_error_) {
+    message->NotifyPeerClosureForSerializedHandles(handle_.group_controller());
     return false;
+  }
 
   InitControllerIfNecessary();
 
@@ -609,8 +611,10 @@
 #endif
 
   message->set_heap_profiler_tag(interface_name_);
-  if (!controller_->SendMessage(message))
+  if (!controller_->SendMessage(message)) {
+    message->NotifyPeerClosureForSerializedHandles(handle_.group_controller());
     return false;
+  }
 
   if (!is_control_message && idle_handler_)
     ++num_unacked_messages_;
@@ -630,8 +634,10 @@
   // Please see comments in Accept().
   message->SerializeHandles(handle_.group_controller());
 
-  if (encountered_error_)
+  if (encountered_error_) {
+    message->NotifyPeerClosureForSerializedHandles(handle_.group_controller());
     return false;
+  }
 
   InitControllerIfNecessary();
 
@@ -653,8 +659,10 @@
   const bool exclusive_wait =
       message->has_flag(Message::kFlagNoInterrupt) ||
       !SyncCallRestrictions::AreSyncCallInterruptsEnabled();
-  if (!controller_->SendMessage(message))
+  if (!controller_->SendMessage(message)) {
+    message->NotifyPeerClosureForSerializedHandles(handle_.group_controller());
     return false;
+  }
 
   if (!is_control_message && idle_handler_)
     ++num_unacked_messages_;
diff --git a/mojo/public/cpp/bindings/lib/message.cc b/mojo/public/cpp/bindings/lib/message.cc
index d78b74f..88587b54 100644
--- a/mojo/public/cpp/bindings/lib/message.cc
+++ b/mojo/public/cpp/bindings/lib/message.cc
@@ -588,6 +588,19 @@
   return result;
 }
 
+void Message::NotifyPeerClosureForSerializedHandles(
+    AssociatedGroupController* group_controller) {
+  const uint32_t num_ids = payload_num_interface_ids();
+  if (num_ids == 0) {
+    return;
+  }
+
+  const uint32_t* ids = header_v2()->payload_interface_ids.Get()->storage();
+  for (uint32_t i = 0; i < num_ids; ++i) {
+    group_controller->NotifyLocalEndpointOfPeerClosure(ids[i]);
+  }
+}
+
 void Message::SerializeIfNecessary() {
   MojoResult rv = MojoSerializeMessage(handle_->value(), nullptr);
   if (rv == MOJO_RESULT_FAILED_PRECONDITION)
diff --git a/mojo/public/cpp/bindings/lib/multiplex_router.cc b/mojo/public/cpp/bindings/lib/multiplex_router.cc
index 90913f4..5bf35af9 100644
--- a/mojo/public/cpp/bindings/lib/multiplex_router.cc
+++ b/mojo/public/cpp/bindings/lib/multiplex_router.cc
@@ -10,6 +10,7 @@
 
 #include "base/containers/contains.h"
 #include "base/containers/flat_set.h"
+#include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/location.h"
 #include "base/memory/ptr_util.h"
@@ -19,6 +20,7 @@
 #include "base/synchronization/waitable_event.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/types/pass_key.h"
+#include "mojo/public/cpp/bindings/features.h"
 #include "mojo/public/cpp/bindings/interface_endpoint_client.h"
 #include "mojo/public/cpp/bindings/interface_endpoint_controller.h"
 #include "mojo/public/cpp/bindings/lib/may_auto_lock.h"
@@ -512,6 +514,24 @@
   ProcessTasks(NO_DIRECT_CLIENT_CALLS, nullptr);
 }
 
+void MultiplexRouter::NotifyLocalEndpointOfPeerClosure(InterfaceId id) {
+  if (!base::FeatureList::IsEnabled(features::kMojoFixAssociatedHandleLeak)) {
+    return;
+  }
+
+  if (!task_runner_->RunsTasksInCurrentSequence()) {
+    task_runner_->PostTask(
+        FROM_HERE,
+        base::BindOnce(&MultiplexRouter::NotifyLocalEndpointOfPeerClosure,
+                       base::WrapRefCounted(this), id));
+    return;
+  }
+  OnPeerAssociatedEndpointClosed(id, std::nullopt);
+
+  MayAutoLock locker(&lock_);
+  ProcessTasks(NO_DIRECT_CLIENT_CALLS, nullptr);
+}
+
 InterfaceEndpointController* MultiplexRouter::AttachEndpointClient(
     const ScopedInterfaceEndpointHandle& handle,
     InterfaceEndpointClient* client,
diff --git a/mojo/public/cpp/bindings/lib/multiplex_router.h b/mojo/public/cpp/bindings/lib/multiplex_router.h
index 2744818..bfe7c22 100644
--- a/mojo/public/cpp/bindings/lib/multiplex_router.h
+++ b/mojo/public/cpp/bindings/lib/multiplex_router.h
@@ -132,6 +132,7 @@
   void CloseEndpointHandle(
       InterfaceId id,
       const std::optional<DisconnectReason>& reason) override;
+  void NotifyLocalEndpointOfPeerClosure(InterfaceId id) override;
   InterfaceEndpointController* AttachEndpointClient(
       const ScopedInterfaceEndpointHandle& handle,
       InterfaceEndpointClient* endpoint_client,
diff --git a/mojo/public/cpp/bindings/message.h b/mojo/public/cpp/bindings/message.h
index 0faff211b..c365d08 100644
--- a/mojo/public/cpp/bindings/message.h
+++ b/mojo/public/cpp/bindings/message.h
@@ -257,6 +257,14 @@
   bool DeserializeAssociatedEndpointHandles(
       AssociatedGroupController* group_controller);
 
+  // If this message contains serialized associated interface endponits but is
+  // going to be destroyed without being sent across a pipe, this notifies any
+  // relevant local peer endpoints about peer closure. Must be called on any
+  // unsent Message that is going to be destroyed after calling
+  // SerializeHandles().
+  void NotifyPeerClosureForSerializedHandles(
+      AssociatedGroupController* group_controller);
+
   // If this Message has an unserialized message context attached, force it to
   // be serialized immediately. Otherwise this does nothing.
   void SerializeIfNecessary();
diff --git a/mojo/public/cpp/bindings/tests/BUILD.gn b/mojo/public/cpp/bindings/tests/BUILD.gn
index cfb6c98..763053a 100644
--- a/mojo/public/cpp/bindings/tests/BUILD.gn
+++ b/mojo/public/cpp/bindings/tests/BUILD.gn
@@ -169,6 +169,7 @@
 mojom("test_mojom") {
   testonly = true
   sources = [
+    "associated_interface_unittest.test-mojom",
     "binder_map_unittest.test-mojom",
     "connection_group_unittest.test-mojom",
     "default_construct_unittest.test-mojom",
diff --git a/mojo/public/cpp/bindings/tests/associated_interface_unittest.cc b/mojo/public/cpp/bindings/tests/associated_interface_unittest.cc
index 4e57b94..1abbc81 100644
--- a/mojo/public/cpp/bindings/tests/associated_interface_unittest.cc
+++ b/mojo/public/cpp/bindings/tests/associated_interface_unittest.cc
@@ -21,16 +21,19 @@
 #include "base/task/single_thread_task_runner.h"
 #include "base/task/thread_pool.h"
 #include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/threading/thread.h"
 #include "mojo/public/cpp/bindings/associated_receiver.h"
 #include "mojo/public/cpp/bindings/associated_remote.h"
+#include "mojo/public/cpp/bindings/features.h"
 #include "mojo/public/cpp/bindings/lib/multiplex_router.h"
 #include "mojo/public/cpp/bindings/pending_associated_receiver.h"
 #include "mojo/public/cpp/bindings/pending_associated_remote.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/bindings/shared_associated_remote.h"
+#include "mojo/public/cpp/bindings/tests/associated_interface_unittest.test-mojom.h"
 #include "mojo/public/cpp/bindings/unique_associated_receiver_set.h"
 #include "mojo/public/cpp/system/functions.h"
 #include "mojo/public/interfaces/bindings/tests/ping_service.mojom.h"
@@ -39,6 +42,7 @@
 
 namespace mojo {
 namespace test {
+namespace associated_interface_unittest {
 namespace {
 
 using mojo::internal::MultiplexRouter;
@@ -1170,6 +1174,54 @@
   }
 }
 
+class ClumsyBinderImpl : public mojom::ClumsyBinder {
+ public:
+  explicit ClumsyBinderImpl(PendingReceiver<mojom::ClumsyBinder> receiver)
+      : receiver_(this, std::move(receiver)) {}
+  ~ClumsyBinderImpl() override = default;
+
+  // mojom::ClumsyBinder:
+  void DropAssociatedBinder(
+      PendingAssociatedReceiver<mojom::AssociatedBinder> receiver) override {
+    // Nothing to do but drop the receiver so it's closed.
+  }
+
+ private:
+  Receiver<mojom::ClumsyBinder> receiver_;
+};
+
+TEST_F(AssociatedInterfaceTest, CloseSerializedAssociatedEndpoints) {
+  // Regression test for https://crbug.com/331636067. Verifies that endpoint
+  // lifetime is properly managed when associated endpoints are serialized into
+  // a message that gets dropped before transmission.
+
+  // Force-enable the feature since this test requires it to pass.
+  base::test::ScopedFeatureList kFeatures{
+      features::kMojoFixAssociatedHandleLeak};
+
+  Remote<mojom::ClumsyBinder> binder;
+  ClumsyBinderImpl binder_impl(binder.BindNewPipeAndPassReceiver());
+
+  AssociatedRemote<mojom::AssociatedBinder> associated_binder;
+  binder->DropAssociatedBinder(
+      associated_binder.BindNewEndpointAndPassReceiver());
+
+  // Wait for disconnection to be observed. This way we know any subsequent
+  // outgoing messages on `associated_binder` will not be sent.
+  base::RunLoop loop1;
+  associated_binder.set_disconnect_handler(loop1.QuitClosure());
+  loop1.Run();
+
+  // Send another endpoint over. This receiver will be dropped, and the remote
+  // should be properly notified of peer closure to terminate this loop.
+  base::RunLoop loop2;
+  AssociatedRemote<mojom::AssociatedBinder> another_binder;
+  associated_binder->Bind(another_binder.BindNewEndpointAndPassReceiver());
+  another_binder.set_disconnect_handler(loop2.QuitClosure());
+  loop2.Run();
+}
+
 }  // namespace
+}  // namespace associated_interface_unittest
 }  // namespace test
 }  // namespace mojo
diff --git a/mojo/public/cpp/bindings/tests/associated_interface_unittest.test-mojom b/mojo/public/cpp/bindings/tests/associated_interface_unittest.test-mojom
new file mode 100644
index 0000000..f609c718e
--- /dev/null
+++ b/mojo/public/cpp/bindings/tests/associated_interface_unittest.test-mojom
@@ -0,0 +1,15 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module mojo.test.associated_interface_unittest.mojom;
+
+import "mojo/public/mojom/base/generic_pending_associated_receiver.mojom";
+
+interface ClumsyBinder {
+  DropAssociatedBinder(pending_associated_receiver<AssociatedBinder> receiver);
+};
+
+interface AssociatedBinder {
+  Bind(mojo_base.mojom.GenericPendingAssociatedReceiver receiver);
+};
diff --git a/testing/buildbot/chromium.chromiumos.json b/testing/buildbot/chromium.chromiumos.json
index 2198fb5a..7f73b47 100644
--- a/testing/buildbot/chromium.chromiumos.json
+++ b/testing/buildbot/chromium.chromiumos.json
@@ -5484,9 +5484,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5496,8 +5496,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -5514,9 +5514,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5526,8 +5526,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -5640,9 +5640,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5652,8 +5652,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -5670,9 +5670,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5682,8 +5682,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/chromium.coverage.json b/testing/buildbot/chromium.coverage.json
index 1adb55e7..2c87605 100644
--- a/testing/buildbot/chromium.coverage.json
+++ b/testing/buildbot/chromium.coverage.json
@@ -19715,9 +19715,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -19727,8 +19727,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -19744,9 +19744,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -19756,8 +19756,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -19865,9 +19865,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -19877,8 +19877,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -19894,9 +19894,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -19906,8 +19906,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index 747c8ec..b910a0a 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -41738,9 +41738,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -41749,8 +41749,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -41767,9 +41767,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -41778,8 +41778,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -41888,9 +41888,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -41899,8 +41899,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -41917,9 +41917,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -41928,8 +41928,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -43237,9 +43237,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -43249,8 +43249,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -43267,9 +43267,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -43279,8 +43279,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -43393,9 +43393,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -43405,8 +43405,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -43423,9 +43423,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -43435,8 +43435,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -44718,9 +44718,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -44729,8 +44729,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -44747,9 +44747,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -44758,8 +44758,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -44868,9 +44868,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -44879,8 +44879,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -44897,9 +44897,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -44908,8 +44908,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/chromium.memory.json b/testing/buildbot/chromium.memory.json
index 2b35774..7951973 100644
--- a/testing/buildbot/chromium.memory.json
+++ b/testing/buildbot/chromium.memory.json
@@ -15765,12 +15765,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -15780,8 +15780,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -15798,12 +15798,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -15813,8 +15813,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
@@ -15941,12 +15941,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 125.0.6415.0",
+        "description": "Run with ash-chrome version 125.0.6416.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -15956,8 +15956,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6415.0",
-              "revision": "version:125.0.6415.0"
+              "location": "lacros_version_skew_tests_v125.0.6416.0",
+              "revision": "version:125.0.6416.0"
             }
           ],
           "dimensions": {
@@ -15974,12 +15974,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.filter;../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 125.0.6398.0",
+        "description": "Run with ash-chrome version 125.0.6411.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -15989,8 +15989,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v125.0.6398.0",
-              "revision": "version:125.0.6398.0"
+              "location": "lacros_version_skew_tests_v125.0.6411.0",
+              "revision": "version:125.0.6411.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index fcd166a..3a46b1e 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -267,32 +267,32 @@
   },
   'LACROS_VERSION_SKEW_CANARY': {
     'identifier': 'Lacros version skew testing ash canary',
-    'description': 'Run with ash-chrome version 125.0.6415.0',
+    'description': 'Run with ash-chrome version 125.0.6416.0',
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6415.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6416.0/test_ash_chrome',
     ],
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v125.0.6415.0',
-          'revision': 'version:125.0.6415.0',
+          'location': 'lacros_version_skew_tests_v125.0.6416.0',
+          'revision': 'version:125.0.6416.0',
         },
       ],
     },
   },
   'LACROS_VERSION_SKEW_DEV': {
     'identifier': 'Lacros version skew testing ash dev',
-    'description': 'Run with ash-chrome version 125.0.6398.0',
+    'description': 'Run with ash-chrome version 125.0.6411.0',
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6398.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v125.0.6411.0/test_ash_chrome',
     ],
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v125.0.6398.0',
-          'revision': 'version:125.0.6398.0',
+          'location': 'lacros_version_skew_tests_v125.0.6411.0',
+          'revision': 'version:125.0.6411.0',
         },
       ],
     },
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index a8507bc..2446c0b 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -11775,6 +11775,26 @@
             ]
         }
     ],
+    "MojoFixAssociatedHandleLeak": [
+        {
+            "platforms": [
+                "android",
+                "chromeos",
+                "chromeos_lacros",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "MojoFixAssociatedHandleLeak"
+                    ]
+                }
+            ]
+        }
+    ],
     "MojoInlineMessagePayloads": [
         {
             "platforms": [
@@ -11891,6 +11911,22 @@
             ]
         }
     ],
+    "MultiCalendarSupport": [
+        {
+            "platforms": [
+                "chromeos",
+                "chromeos_lacros"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "MultiCalendarSupport"
+                    ]
+                }
+            ]
+        }
+    ],
     "MutationEvents": [
         {
             "platforms": [
@@ -15433,26 +15469,6 @@
             ]
         }
     ],
-    "ProtectedAudiencesMultiBid": [
-        {
-            "platforms": [
-                "android",
-                "chromeos",
-                "chromeos_lacros",
-                "linux",
-                "mac",
-                "windows"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled",
-                    "enable_features": [
-                        "FledgeMultiBid"
-                    ]
-                }
-            ]
-        }
-    ],
     "ProtectedAudiencesReportingTimeout": [
         {
             "platforms": [
diff --git a/third_party/angle b/third_party/angle
index 67fc293a..e229afa 160000
--- a/third_party/angle
+++ b/third_party/angle
@@ -1 +1 @@
-Subproject commit 67fc293ab0b33e82cc151286f22d55a0781e9e86
+Subproject commit e229afada1d2af012b7ec55395d10f53f1ebfe60
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 697c95bc..505438c 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -2172,9 +2172,9 @@
              "SharedStorageAPIM118",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
-BASE_FEATURE(kSharedStorageAPIM124,
-             "SharedStorageAPIM124",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+BASE_FEATURE(kSharedStorageAPIM125,
+             "SharedStorageAPIM125",
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 BASE_FEATURE(kSharedStorageAPIEnableWALForDatabase,
              "SharedStorageAPIEnableWALForDatabase",
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index bf9780c6..6d17853 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -1420,10 +1420,10 @@
 // shipped.
 BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kSharedStorageAPIM118);
 
-// Additional Shared Storage API features shipped in M124.
+// Additional Shared Storage API features shipped in M125.
 // TODO(crbug.com/1218540): Merge this flag with `kSharedStorageAPI` once
 // shipped.
-BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kSharedStorageAPIM124);
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kSharedStorageAPIM125);
 
 // Enables WAL (write-ahead-logging) mode for the Shared Storage API SQLite
 // database backend.
diff --git a/third_party/blink/public/common/page/v8_compile_hints_histograms.h b/third_party/blink/public/common/page/v8_compile_hints_histograms.h
index ab5d797..d59b4b43 100644
--- a/third_party/blink/public/common/page/v8_compile_hints_histograms.h
+++ b/third_party/blink/public/common/page/v8_compile_hints_histograms.h
@@ -13,9 +13,6 @@
 inline constexpr const char* kLocalCompileHintsGeneratedHistogram =
     "WebCore.Scripts.V8LocalCompileHintsGenerated";
 
-inline constexpr const char* kLocalCompileHintsObsoletedByCodeCacheHistogram =
-    "WebCore.Scripts.V8LocalCompileHintsObsoletedByCodeCache";
-
 // These values are persisted to logs. Entries should not be renumbered and
 // numeric values should never be reused.
 enum class Status {
diff --git a/third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom b/third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom
index 7c0c288..618b270 100644
--- a/third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom
+++ b/third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom
@@ -72,6 +72,7 @@
   CONNECT_ALREADY_CONNECTED,
   CONNECT_ALREADY_EXISTS,
   CONNECT_NOT_CONNECTED,
+  CONNECT_NON_AUTH_TIMEOUT,
   // NotFoundError:
   NO_BLUETOOTH_ADAPTER,
   CHOSEN_DEVICE_VANISHED,
diff --git a/third_party/blink/renderer/bindings/core/v8/script_streamer.cc b/third_party/blink/renderer/bindings/core/v8/script_streamer.cc
index 00a980a..f70de92 100644
--- a/third_party/blink/renderer/bindings/core/v8/script_streamer.cc
+++ b/third_party/blink/renderer/bindings/core/v8/script_streamer.cc
@@ -639,8 +639,6 @@
     }
   }
 
-  V8CodeCache::RecordCacheGetStatistics(script_resource_->CacheHandler());
-
   // Here we can't call Check on the cache handler because it requires the
   // script source, which would require having already loaded the script. It is
   // OK at this point to disable streaming even though we might end up rejecting
@@ -1120,8 +1118,6 @@
   scoped_refptr<CachedMetadata> metadata =
       big_buffer ? CachedMetadata::CreateFromSerializedData(*big_buffer)
                  : nullptr;
-
-  V8CodeCache::RecordCacheGetStatistics(metadata.get(), encoding);
   std::unique_ptr<v8_compile_hints::CompileHintsForStreaming> result =
       std::move(builder).Build(
           (metadata && V8CodeCache::HasHotCompileHints(*metadata, encoding))
@@ -1350,8 +1346,6 @@
   }
   if (HasCodeCache(cached_metadata, encoding_.GetName())) {
     SuppressStreaming(NotStreamingReason::kHasCodeCacheBackground);
-    V8CodeCache::RecordCacheGetStatistics(
-        V8CodeCache::GetMetadataType::kCodeCache);
     return false;
   }
   compile_hints_ = BuildCompileHintsForStreaming(
diff --git a/third_party/blink/renderer/bindings/core/v8/v8_code_cache.cc b/third_party/blink/renderer/bindings/core/v8/v8_code_cache.cc
index 07f06a8d..5a8f747 100644
--- a/third_party/blink/renderer/bindings/core/v8/v8_code_cache.cc
+++ b/third_party/blink/renderer/bindings/core/v8/v8_code_cache.cc
@@ -84,76 +84,6 @@
   kFull = 1,
 };
 
-V8CodeCache::GetMetadataType ReadGetMetadataType(
-    const CachedMetadataHandler* cache_handler) {
-  // Check the metadata types in the same preference order they're checked in
-  // the code: code cache, local compile hints, timestamp. That way we get the
-  // right sample in case several metadata types are set.
-  uint32_t code_cache_tag = V8CodeCache::TagForCodeCache(cache_handler);
-  if (cache_handler
-          ->GetCachedMetadata(code_cache_tag,
-                              CachedMetadataHandler::kAllowUnchecked)
-          .get()) {
-    return V8CodeCache::GetMetadataType::kCodeCache;
-  }
-  scoped_refptr<CachedMetadata> cached_metadata =
-      cache_handler->GetCachedMetadata(
-          V8CodeCache::TagForCompileHints(cache_handler),
-          CachedMetadataHandler::kAllowUnchecked);
-  if (cached_metadata) {
-    return TimestampIsRecent(cached_metadata.get())
-               ? V8CodeCache::GetMetadataType::
-                     kLocalCompileHintsWithHotTimestamp
-               : V8CodeCache::GetMetadataType::
-                     kLocalCompileHintsWithColdTimestamp;
-  }
-  cached_metadata = cache_handler->GetCachedMetadata(
-      V8CodeCache::TagForTimeStamp(cache_handler),
-      CachedMetadataHandler::kAllowUnchecked);
-  if (cached_metadata) {
-    return TimestampIsRecent(cached_metadata.get())
-               ? V8CodeCache::GetMetadataType::kHotTimestamp
-               : V8CodeCache::GetMetadataType::kColdTimestamp;
-  }
-  return V8CodeCache::GetMetadataType::kNone;
-}
-
-V8CodeCache::GetMetadataType ReadGetMetadataType(
-    const CachedMetadata* cached_metadata,
-    const String& encoding) {
-  if (!cached_metadata) {
-    return V8CodeCache::GetMetadataType::kNone;
-  }
-
-  // Check the metadata types in the same preference order they're checked in
-  // the code: code cache, local compile hints, timestamp. That way we get the
-  // right sample in case several metadata types are set.
-  if (cached_metadata->DataTypeID() == CacheTag(kCacheTagCode, encoding)) {
-    return V8CodeCache::GetMetadataType::kCodeCache;
-  }
-
-  if (cached_metadata->DataTypeID() ==
-      CacheTag(kCacheTagCompileHints, encoding)) {
-    return TimestampIsRecent(cached_metadata)
-               ? V8CodeCache::GetMetadataType::
-                     kLocalCompileHintsWithHotTimestamp
-               : V8CodeCache::GetMetadataType::
-                     kLocalCompileHintsWithColdTimestamp;
-  }
-
-  if (cached_metadata->DataTypeID() == CacheTag(kCacheTagTimeStamp, encoding)) {
-    return TimestampIsRecent(cached_metadata)
-               ? V8CodeCache::GetMetadataType::kHotTimestamp
-               : V8CodeCache::GetMetadataType::kColdTimestamp;
-  }
-  return V8CodeCache::GetMetadataType::kNone;
-}
-
-constexpr const char* kCacheGetHistogram =
-    "WebCore.Scripts.V8CodeCacheMetadata.Get";
-constexpr const char* kCacheSetHistogram =
-    "WebCore.Scripts.V8CodeCacheMetadata.Set";
-
 }  // namespace
 
 // Check previously stored timestamp (either from the code cache or compile
@@ -342,10 +272,6 @@
                            no_cache_reason);
   }
 
-  // By recording statistics at this point we exclude scripts for which we're
-  // not going to generate metadata.
-  RecordCacheGetStatistics(cache_handler);
-
   if (HasCodeCache(cache_handler) &&
       no_code_cache_compile_options !=
           v8::ScriptCompiler::kProduceCompileHints) {
@@ -462,8 +388,6 @@
       std::unique_ptr<v8::ScriptCompiler::CachedData> cached_data(
           v8::ScriptCompiler::CreateCodeCache(unbound_script));
       if (cached_data) {
-        V8CodeCache::RecordCacheSetStatistics(
-            V8CodeCache::SetMetadataType::kCodeCache);
         const uint8_t* data = cached_data->data;
         int length = cached_data->length;
         cache_handler->ClearCachedMetadata(
@@ -534,7 +458,6 @@
 // Store a timestamp to the cache as hint.
 void V8CodeCache::SetCacheTimeStamp(CodeCacheHost* code_cache_host,
                                     CachedMetadataHandler* cache_handler) {
-  RecordCacheSetStatistics(V8CodeCache::SetMetadataType::kTimestamp);
   uint64_t now_ms = GetTimestamp();
   cache_handler->ClearCachedMetadata(code_cache_host,
                                      CachedMetadataHandler::kClearLocally);
@@ -621,27 +544,4 @@
   return cached_metadata;
 }
 
-void V8CodeCache::RecordCacheGetStatistics(
-    const CachedMetadataHandler* cache_handler) {
-  base::UmaHistogramEnumeration(kCacheGetHistogram,
-                                ReadGetMetadataType(cache_handler));
-}
-
-void V8CodeCache::RecordCacheGetStatistics(
-    const CachedMetadata* cached_metadata,
-    const String& encoding) {
-  base::UmaHistogramEnumeration(kCacheGetHistogram,
-                                ReadGetMetadataType(cached_metadata, encoding));
-}
-
-void V8CodeCache::RecordCacheGetStatistics(
-    V8CodeCache::GetMetadataType metadata_type) {
-  base::UmaHistogramEnumeration(kCacheGetHistogram, metadata_type);
-}
-
-void V8CodeCache::RecordCacheSetStatistics(
-    V8CodeCache::SetMetadataType metadata_type) {
-  base::UmaHistogramEnumeration(kCacheSetHistogram, metadata_type);
-}
-
 }  // namespace blink
diff --git a/third_party/blink/renderer/bindings/core/v8/v8_code_cache.h b/third_party/blink/renderer/bindings/core/v8/v8_code_cache.h
index 2edb39d..6c0f4e72 100644
--- a/third_party/blink/renderer/bindings/core/v8/v8_code_cache.h
+++ b/third_party/blink/renderer/bindings/core/v8/v8_code_cache.h
@@ -127,36 +127,6 @@
       const KURL& source_url,
       const WTF::TextEncoding&,
       OpaqueMode);
-
-  // These values are persisted to logs. Entries should not be renumbered and
-  // numeric values should never be reused.
-  enum class GetMetadataType {
-    kNone = 0,
-    kHotTimestamp = 1,
-    kColdTimestamp = 2,
-    kLocalCompileHintsWithHotTimestamp = 3,
-    kLocalCompileHintsWithColdTimestamp = 4,
-    kCodeCache = 5,
-    kMaxValue = kCodeCache
-  };
-
-  // These values are persisted to logs. Entries should not be renumbered and
-  // numeric values should never be reused.
-  enum class SetMetadataType {
-    kTimestamp = 0,
-    kLocalCompileHintsAtFMP = 1,
-    kLocalCompileHintsAtInteractive = 2,
-    kCodeCache = 3,
-    kMaxValue = kCodeCache
-  };
-
-  static void RecordCacheGetStatistics(
-      const CachedMetadataHandler* cache_handler);
-  static void RecordCacheGetStatistics(const CachedMetadata* cached_metadata,
-                                       const String& encoding);
-  static void RecordCacheGetStatistics(GetMetadataType metadata_type);
-
-  static void RecordCacheSetStatistics(SetMetadataType metadata_type);
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/bindings/core/v8/v8_local_compile_hints_producer.cc b/third_party/blink/renderer/bindings/core/v8/v8_local_compile_hints_producer.cc
index d892e91..f08cff4 100644
--- a/third_party/blink/renderer/bindings/core/v8/v8_local_compile_hints_producer.cc
+++ b/third_party/blink/renderer/bindings/core/v8/v8_local_compile_hints_producer.cc
@@ -62,23 +62,6 @@
       continue;
     }
 
-    if (V8CodeCache::HasCodeCache(cache_handler,
-                                  CachedMetadataHandler::kAllowUnchecked)) {
-      // We're trying to set compile hints even though the code cache exists
-      // already. This can happen if the user navigated around on the website
-      // and the script became so hot that a code cache was created.
-      base::UmaHistogramBoolean(kLocalCompileHintsObsoletedByCodeCacheHistogram,
-                                true);
-      return;
-    }
-    base::UmaHistogramBoolean(kLocalCompileHintsObsoletedByCodeCacheHistogram,
-                              false);
-
-    V8CodeCache::RecordCacheSetStatistics(
-        final_data
-            ? V8CodeCache::SetMetadataType::kLocalCompileHintsAtInteractive
-            : V8CodeCache::SetMetadataType::kLocalCompileHintsAtFMP);
-
     uint64_t timestamp = V8CodeCache::GetTimestamp();
     std::unique_ptr<v8::ScriptCompiler::CachedData> data(
         CreateCompileHintsCachedDataForScript(compile_hints, timestamp));
diff --git a/third_party/blink/renderer/modules/bluetooth/bluetooth_error.cc b/third_party/blink/renderer/modules/bluetooth/bluetooth_error.cc
index cedf850..159ceb36 100644
--- a/third_party/blink/renderer/modules/bluetooth/bluetooth_error.cc
+++ b/third_party/blink/renderer/modules/bluetooth/bluetooth_error.cc
@@ -118,6 +118,8 @@
                 "Connection Error: Already exists.");
       MAP_ERROR(CONNECT_NOT_CONNECTED, DOMExceptionCode::kInvalidStateError,
                 "Connection Error: Not connected.");
+      MAP_ERROR(CONNECT_NON_AUTH_TIMEOUT, DOMExceptionCode::kInvalidStateError,
+                "Connection Error: Non-authentication timeout.");
 
       // NetworkErrors:
       MAP_ERROR(CONNECT_ALREADY_IN_PROGRESS, DOMExceptionCode::kNetworkError,
diff --git a/third_party/blink/renderer/modules/shared_storage/shared_storage.idl b/third_party/blink/renderer/modules/shared_storage/shared_storage.idl
index 9065a623..01af65bd 100644
--- a/third_party/blink/renderer/modules/shared_storage/shared_storage.idl
+++ b/third_party/blink/renderer/modules/shared_storage/shared_storage.idl
@@ -72,7 +72,7 @@
   ] Promise<any> run(DOMString name, optional SharedStorageRunOperationMethodOptions options);
 
   [
-    RuntimeEnabled=SharedStorageAPIM124,
+    RuntimeEnabled=SharedStorageAPIM125,
     CallWith=ScriptState,
     RaisesException,
     MeasureAs=SharedStorageAPI_CreateWorklet_Method
diff --git a/third_party/blink/renderer/modules/shared_storage/shared_storage_worklet.idl b/third_party/blink/renderer/modules/shared_storage/shared_storage_worklet.idl
index b222dfd..9cafcfb 100644
--- a/third_party/blink/renderer/modules/shared_storage/shared_storage_worklet.idl
+++ b/third_party/blink/renderer/modules/shared_storage/shared_storage_worklet.idl
@@ -15,7 +15,7 @@
   ] Promise<undefined> addModule(USVString moduleURL, optional WorkletOptions options = {});
 
   [
-    RuntimeEnabled=SharedStorageAPIM124,
+    RuntimeEnabled=SharedStorageAPIM125,
     Exposed=Window,
     CallWith=ScriptState,
     RaisesException,
@@ -25,7 +25,7 @@
                                  optional SharedStorageRunOperationMethodOptions options);
 
   [
-    RuntimeEnabled=SharedStorageAPIM124,
+    RuntimeEnabled=SharedStorageAPIM125,
     Exposed=Window,
     CallWith=ScriptState,
     RaisesException,
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index c543b07..f5fa6f9 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -3523,7 +3523,7 @@
       public: true,
     },
     {
-      name: "SharedStorageAPIM124",
+      name: "SharedStorageAPIM125",
       base_feature: "none",
       public: true,
     },
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 32222d5..8383368 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -1861,7 +1861,7 @@
       "external/wpt/shared-storage",
       "http/tests/inspector-protocol/shared-storage"
     ],
-    "args": ["--enable-features=SharedStorageAPI,FencedFrames:implementation_type/mparch,PrivacySandboxAdsAPIsOverride,FencedFramesAPIChanges,FencedFramesDefaultMode,SharedStorageAPIM118,SharedStorageAPIM124,SharedStorageAPIEnableWALForDatabase,FencedFramesEnforceFocus",
+    "args": ["--enable-features=SharedStorageAPI,FencedFrames:implementation_type/mparch,PrivacySandboxAdsAPIsOverride,FencedFramesAPIChanges,FencedFramesDefaultMode,SharedStorageAPIM118,SharedStorageAPIM125,SharedStorageAPIEnableWALForDatabase,FencedFramesEnforceFocus",
              "--disable-threaded-compositing", "--disable-threaded-animation"],
     "expires": "Jul 31, 2024"
   },
@@ -1874,7 +1874,7 @@
     "exclusive_tests": [
       "external/wpt/shared-storage-selecturl-limit/"
     ],
-    "args": ["--enable-features=SharedStorageAPI,FencedFrames:implementation_type/mparch,FencedFramesAPIChanges,FencedFramesDefaultMode,FencedFramesEnforceFocus,PrivacySandboxAdsAPIsOverride,SharedStorageSelectURLLimit:SharedStorageSelectURLBitBudgetPerPageLoad/9,SharedStorageAPIM118,SharedStorageAPIM124,SharedStorageAPIEnableWALForDatabase",
+    "args": ["--enable-features=SharedStorageAPI,FencedFrames:implementation_type/mparch,FencedFramesAPIChanges,FencedFramesDefaultMode,FencedFramesEnforceFocus,PrivacySandboxAdsAPIsOverride,SharedStorageSelectURLLimit:SharedStorageSelectURLBitBudgetPerPageLoad/9,SharedStorageAPIM118,SharedStorageAPIM125,SharedStorageAPIEnableWALForDatabase",
              "--disable-threaded-compositing", "--disable-threaded-animation"],
     "expires": "Jul 31, 2024"
   },
diff --git a/third_party/blink/web_tests/external/wpt/compute-pressure/observe_return_type.https.any.js b/third_party/blink/web_tests/external/wpt/compute-pressure/observe_return_type.https.any.js
new file mode 100644
index 0000000..b24878ab
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/compute-pressure/observe_return_type.https.any.js
@@ -0,0 +1,18 @@
+// META: script=/resources/test-only-api.js
+// META: script=resources/pressure-helpers.js
+// META: global=window,dedicatedworker,sharedworker
+
+'use strict';
+
+// Regression test for https://issues.chromium.org/issues/333957909
+// Make sure that observe() always returns a Promise.
+pressure_test(async (t, mockPressureService) => {
+  const observer = new PressureObserver(() => {});
+  t.add_cleanup(() => observer.disconnect());
+
+  for (let i = 0; i < 2; i++) {
+    const promise = observer.observe('cpu');
+    assert_class_string(promise, 'Promise');
+    await promise;
+  }
+}, 'PressureObserver.observe() is idempotent');
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
index 97da054..fe8b439 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
@@ -9472,6 +9472,7 @@
     method append
     method clear
     method constructor
+    method createWorklet
     method delete
     method get
     method run
@@ -9481,6 +9482,8 @@
     attribute @@toStringTag
     method addModule
     method constructor
+    method run
+    method selectURL
 interface SharedWorker : EventTarget
     attribute @@toStringTag
     getter onerror
diff --git a/third_party/chromium-variations b/third_party/chromium-variations
index 9bbacc0..5951e69 160000
--- a/third_party/chromium-variations
+++ b/third_party/chromium-variations
@@ -1 +1 @@
-Subproject commit 9bbacc037e69792d5f5c70a389ce780e35b71f29
+Subproject commit 5951e69a2333e36fbd7912bd160f4686347e6c5f
diff --git a/third_party/closure_compiler/externs/bluetooth_private.js b/third_party/closure_compiler/externs/bluetooth_private.js
index 46801ebd..b54db7d 100644
--- a/third_party/closure_compiler/externs/bluetooth_private.js
+++ b/third_party/closure_compiler/externs/bluetooth_private.js
@@ -54,6 +54,7 @@
   NOT_CONNECTED: 'notConnected',
   DOES_NOT_EXIST: 'doesNotExist',
   INVALID_ARGS: 'invalidArgs',
+  NON_AUTH_TIMEOUT: 'nonAuthTimeout',
 };
 
 /**
diff --git a/third_party/dawn b/third_party/dawn
index 5864d1b..37f756f 160000
--- a/third_party/dawn
+++ b/third_party/dawn
@@ -1 +1 @@
-Subproject commit 5864d1bef534d0e54f64d489c865db7f76deb2fe
+Subproject commit 37f756f60fb233f9fa1d622c3fe277816baab4cc
diff --git a/third_party/depot_tools b/third_party/depot_tools
index 8f6d774..609288a 160000
--- a/third_party/depot_tools
+++ b/third_party/depot_tools
@@ -1 +1 @@
-Subproject commit 8f6d774a8d8ddcd5a4dc6e8aac06a35576c2b113
+Subproject commit 609288a46b37758cbbfd82aefc01359631aec81f
diff --git a/third_party/devtools-frontend-internal b/third_party/devtools-frontend-internal
index 5674bb7..1c8f358 160000
--- a/third_party/devtools-frontend-internal
+++ b/third_party/devtools-frontend-internal
@@ -1 +1 @@
-Subproject commit 5674bb7b0c13888a0222377953c082b1abf53fbd
+Subproject commit 1c8f3583f1e10d91097074d41aba0b8f78837ae1
diff --git a/third_party/googletest/src b/third_party/googletest/src
index b1a777f..5197b1a 160000
--- a/third_party/googletest/src
+++ b/third_party/googletest/src
@@ -1 +1 @@
-Subproject commit b1a777f31913f8a047f43b2a5f823e736e7f5082
+Subproject commit 5197b1a8e6a1ef9f214f4aa537b0be17cbf91946
diff --git a/third_party/lit/v3_0/BUILD.gn b/third_party/lit/v3_0/BUILD.gn
index 2b160ac7..f1173706 100644
--- a/third_party/lit/v3_0/BUILD.gn
+++ b/third_party/lit/v3_0/BUILD.gn
@@ -23,6 +23,7 @@
     "//chrome/test/data/webui/cr_elements:build_ts",
     "//chrome/test/data/webui/extensions:build_ts",
     "//chrome/test/data/webui/history:build_ts",
+    "//chrome/test/data/webui/welcome:build_ts",
     "//ui/webui/resources/cr_components/customize_color_scheme_mode:build_ts",
     "//ui/webui/resources/cr_components/help_bubble:build_ts",
     "//ui/webui/resources/cr_components/history_clusters:build_ts",
diff --git a/third_party/pdfium b/third_party/pdfium
index 2c66e07..fde20e1 160000
--- a/third_party/pdfium
+++ b/third_party/pdfium
@@ -1 +1 @@
-Subproject commit 2c66e07e9c3b52feee720c03aa11984895f09803
+Subproject commit fde20e170bebdde902d38dd577a0543e11b6d4d4
diff --git a/third_party/skia b/third_party/skia
index b40fdf1..293de35 160000
--- a/third_party/skia
+++ b/third_party/skia
@@ -1 +1 @@
-Subproject commit b40fdf12342f60a32930e1cc5758380d0c786756
+Subproject commit 293de35a9d1e046bf919f32ec6eb693f82ee4df9
diff --git a/third_party/webrtc b/third_party/webrtc
index 83a1c92..f2cdbc9 160000
--- a/third_party/webrtc
+++ b/third_party/webrtc
@@ -1 +1 @@
-Subproject commit 83a1c92bb409cb16dfa2cb60f7f3064db388aca6
+Subproject commit f2cdbc9b0717e67821826f7ff93ce7fe0e9485c3
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index a32b88c..5fbaa7f 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -12256,6 +12256,36 @@
   <int value="8" label="Overflow"/>
 </enum>
 
+<enum name="GameControlsButtonOptionsMenuFunction">
+  <int value="0" label="Select single button type"/>
+  <int value="1" label="Select joystick type"/>
+  <int value="2" label="Edit label is focused"/>
+  <int value="3" label="Key assigned successfully"/>
+  <int value="4" label="Press done button"/>
+  <int value="5" label="Press delete button"/>
+</enum>
+
+<enum name="GameControlsEditDeleteMenuFunction">
+  <int value="0" label="Press edit button"/>
+  <int value="1" label="Press delete button"/>
+</enum>
+
+<enum name="GameControlsEditingListFunction">
+  <int value="0" label="Add"/>
+  <int value="1" label="Done"/>
+  <int value="2" label="Hover list item"/>
+  <int value="3" label="Press list item"/>
+  <int value="4" label="Edit label is focused"/>
+  <int value="5" label="Key assigned successfully"/>
+</enum>
+
+<enum name="GameControlsMappingSource">
+  <int value="0" label="Empty"/>
+  <int value="1" label="Default mapping, including position change"/>
+  <int value="2" label="User added mapping only"/>
+  <int value="3" label="Default and user-added mapping"/>
+</enum>
+
 <enum name="GameDashboardFunction">
   <int value="0" label="Feedback"/>
   <int value="1" label="Help"/>
@@ -19753,6 +19783,7 @@
   <int value="-614223913"
       label="ClickToCallContextMenuForSelectedText:enabled"/>
   <int value="-613596048" label="new-canvas-2d-api"/>
+  <int value="-613595118" label="StarterPackIPH:enabled"/>
   <int value="-613072171" label="ElementCapture:enabled"/>
   <int value="-612860466" label="Projector:disabled"/>
   <int value="-612633819" label="NotificationScrollBar:disabled"/>
@@ -23379,6 +23410,7 @@
   <int value="1009976778" label="SidePanel:disabled"/>
   <int value="1010832751" label="VideoToolboxAv1Decoding:disabled"/>
   <int value="1011491959" label="PageInfoCookiesSubpage:enabled"/>
+  <int value="1012449492" label="StarterPackIPH:disabled"/>
   <int value="1012643576" label="CommerceDeveloper:enabled"/>
   <int value="1012916096" label="AutocorrectToggle:enabled"/>
   <int value="1012942422" label="HorizontalTabSwitcherAndroid:disabled"/>
diff --git a/tools/metrics/histograms/metadata/arc/histograms.xml b/tools/metrics/histograms/metadata/arc/histograms.xml
index ed327b7b..7faf9f7 100644
--- a/tools/metrics/histograms/metadata/arc/histograms.xml
+++ b/tools/metrics/histograms/metadata/arc/histograms.xml
@@ -1135,6 +1135,59 @@
   </token>
 </histogram>
 
+<histogram name="Arc.GameControls.ButtonOptionsMenuFunctionTriggered"
+    enum="GameControlsButtonOptionsMenuFunction" expires_after="2025-04-07">
+  <owner>cuicuiruan@google.com</owner>
+  <owner>pjlee@google.com</owner>
+  <owner>arc-gaming@google.com</owner>
+  <summary>
+    Records how often a specific function within the game controls button
+    options menu is triggered.
+  </summary>
+</histogram>
+
+<histogram name="Arc.GameControls.EditDeleteMenuFuctionTriggered"
+    enum="GameControlsEditDeleteMenuFunction" expires_after="2025-04-07">
+  <owner>cuicuiruan@google.com</owner>
+  <owner>pjlee@google.com</owner>
+  <owner>arc-gaming@google.com</owner>
+  <summary>
+    Records how often a specific function within the game controls edit/delete
+    menu is triggered.
+  </summary>
+</histogram>
+
+<histogram name="Arc.GameControls.EditingListFunctionTriggered"
+    enum="GameControlsEditingListFunction" expires_after="2025-04-07">
+  <owner>cuicuiruan@google.com</owner>
+  <owner>pjlee@google.com</owner>
+  <owner>arc-gaming@google.com</owner>
+  <summary>
+    Records how often a specific function within the game controls editing list
+    is triggered.
+  </summary>
+</histogram>
+
+<histogram
+    name="Arc.GameControls.{FeatureOrHint}ToggleWithMappingSource.{OnOrOff}"
+    enum="GameControlsMappingSource" expires_after="2025-04-07">
+  <owner>cuicuiruan@google.com</owner>
+  <owner>pjlee@google.com</owner>
+  <owner>arc-gaming@google.com</owner>
+  <summary>
+    Records the game controls feature or hint is toggled on or off with current
+    mapping source.
+  </summary>
+  <token key="FeatureOrHint">
+    <variant name="Feature"/>
+    <variant name="Hint"/>
+  </token>
+  <token key="OnOrOff">
+    <variant name="Off"/>
+    <variant name="On"/>
+  </token>
+</histogram>
+
 <histogram name="Arc.GhostWindowViewType" units="ArcGhostWindowViewType"
     expires_after="2024-09-01">
   <owner>sstan@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/ash/enums.xml b/tools/metrics/histograms/metadata/ash/enums.xml
index 1b179d4..0964237 100644
--- a/tools/metrics/histograms/metadata/ash/enums.xml
+++ b/tools/metrics/histograms/metadata/ash/enums.xml
@@ -1110,6 +1110,7 @@
   <int value="20" label="Stop screen recording keyboard shortcut"/>
   <int value="21" label="Game dashboard stop recording button"/>
   <int value="22" label="Game toolbar stop recording button"/>
+  <int value="23" label="Game dashboard stop recording for tablet mode"/>
 </enum>
 
 <enum name="EolIncentiveButtonType">
@@ -1281,6 +1282,12 @@
   <int value="6" label="IME tray"/>
 </enum>
 
+<enum name="MahiQuestionSource">
+  <int value="0" label="menu view"/>
+  <int value="1" label="panel view"/>
+  <int value="2" label="retry button"/>
+</enum>
+
 <enum name="MantaStatusCode">
   <int value="0" label="kOk"/>
   <int value="1" label="kGenericError"/>
diff --git a/tools/metrics/histograms/metadata/ash/histograms.xml b/tools/metrics/histograms/metadata/ash/histograms.xml
index 8e365a27e..b5f208b 100644
--- a/tools/metrics/histograms/metadata/ash/histograms.xml
+++ b/tools/metrics/histograms/metadata/ash/histograms.xml
@@ -4833,6 +4833,36 @@
   </summary>
 </histogram>
 
+<histogram name="Ash.Mahi.QuestionAnswer.LoadingTime" units="ms"
+    expires_after="2025-04-10">
+  <owner>leandre@chromium.org</owner>
+  <owner>cros-status-area-eng@google.com</owner>
+  <summary>
+    Record the time it takes to answer the question inside the Mahi panel.
+    Emitted when an answer text is fully loaded and shows up.
+  </summary>
+</histogram>
+
+<histogram name="Ash.Mahi.QuestionSource" enum="MahiQuestionSource"
+    expires_after="2025-02-01">
+  <owner>andrewxu@chromium.org</owner>
+  <owner>cros-status-area-eng@google.com</owner>
+  <summary>
+    Record the sources of questions sent to the Mahi backend. Emitted when a
+    question is posted.
+  </summary>
+</histogram>
+
+<histogram name="Ash.Mahi.Summary.LoadingTime" units="ms"
+    expires_after="2025-04-10">
+  <owner>leandre@chromium.org</owner>
+  <owner>cros-status-area-eng@google.com</owner>
+  <summary>
+    Record the time it takes to load the summary inside the Mahi panel. Emitted
+    when summary text is fully loaded and shows up.
+  </summary>
+</histogram>
+
 <histogram name="Ash.Mahi.UserJourneyTime" units="ms"
     expires_after="2025-04-10">
   <owner>leandre@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/bluetooth/enums.xml b/tools/metrics/histograms/metadata/bluetooth/enums.xml
index a0d1a93..794c2ad 100644
--- a/tools/metrics/histograms/metadata/bluetooth/enums.xml
+++ b/tools/metrics/histograms/metadata/bluetooth/enums.xml
@@ -60,6 +60,8 @@
   <int value="8" label="Authentication canceled"/>
   <int value="9" label="Authentication rejected"/>
   <int value="10" label="Connection in progress"/>
+  <int value="11" label="Device not found"/>
+  <int value="12" label="Bluetooth disabled"/>
 </enum>
 
 <enum name="BluetoothConnectToServiceError">
@@ -84,6 +86,7 @@
   <int value="11" label="Not Connected"/>
   <int value="12" label="Does Not Exist"/>
   <int value="13" label="Invalid Arguments"/>
+  <int value="14" label="Non-authorization Timeout"/>
 </enum>
 
 <enum name="BluetoothDeviceConnectToServiceFailureReason">
@@ -1397,6 +1400,7 @@
   <int value="13" label="Not Connected"/>
   <int value="14" label="Does Not Exist"/>
   <int value="15" label="Invalid Arguments"/>
+  <int value="16" label="Non-auth Timeout"/>
 </enum>
 
 <enum name="WebBluetoothGATTOperationOutcome">
diff --git a/tools/metrics/histograms/metadata/extensions/enums.xml b/tools/metrics/histograms/metadata/extensions/enums.xml
index 8757140..acfba71 100644
--- a/tools/metrics/histograms/metadata/extensions/enums.xml
+++ b/tools/metrics/histograms/metadata/extensions/enums.xml
@@ -3109,6 +3109,7 @@
   <int value="255" label="kEnterpriseKioskInput"/>
   <int value="256" label="kOdfsConfigPrivate"/>
   <int value="257" label="kChromeOSManagementAudio"/>
+  <int value="258" label="kChromeOSDiagnosticsNetworkInfoForMlab"/>
 </enum>
 
 <enum name="ExtensionPolicyReinstallReason">
diff --git a/tools/metrics/histograms/metadata/others/histograms.xml b/tools/metrics/histograms/metadata/others/histograms.xml
index fcbf9e8..8272bce 100644
--- a/tools/metrics/histograms/metadata/others/histograms.xml
+++ b/tools/metrics/histograms/metadata/others/histograms.xml
@@ -12066,7 +12066,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.CloseAction" enum="WebUITabStripCloseActions"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>collinbaker@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
@@ -12077,7 +12077,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.CloseTabAction"
-    enum="WebUITabStripCloseTabActions" expires_after="2024-09-15">
+    enum="WebUITabStripCloseTabActions" expires_after="2024-10-02">
   <owner>johntlee@chromium.org</owner>
   <owner>dpapad@chromium.org</owner>
   <summary>
@@ -12088,7 +12088,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.LoadCompletedTime" units="ms"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -12102,7 +12102,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.LoadDocumentTime" units="ms"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -12116,7 +12116,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.OpenAction" enum="WebUITabStripOpenActions"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>collinbaker@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
@@ -12127,7 +12127,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.OpenDuration" units="ms"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>collinbaker@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
@@ -12138,7 +12138,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.TabActivation" units="ms"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>robliao@chromium.org</owner>
   <owner>johntlee@chromium.org</owner>
   <summary>
@@ -12148,7 +12148,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.TabCreation" units="ms"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>robliao@chromium.org</owner>
   <owner>johntlee@chromium.org</owner>
   <summary>
@@ -12157,7 +12157,7 @@
 </histogram>
 
 <histogram name="WebUITabStrip.TabDataReceived" units="ms"
-    expires_after="2024-05-15">
+    expires_after="2024-10-02">
   <owner>robliao@chromium.org</owner>
   <owner>johntlee@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/safe_browsing/enums.xml b/tools/metrics/histograms/metadata/safe_browsing/enums.xml
index f3ca53ce..d4ede5d 100644
--- a/tools/metrics/histograms/metadata/safe_browsing/enums.xml
+++ b/tools/metrics/histograms/metadata/safe_browsing/enums.xml
@@ -148,6 +148,18 @@
   <int value="1" label="Unavailable"/>
 </enum>
 
+<enum name="ClientReportPersistDownloadReportResult">
+  <int value="0" label="kPersistTaskPosted"/>
+  <int value="1" label="kSerializationError"/>
+  <int value="2" label="kEmptyReport"/>
+</enum>
+
+<enum name="ClientReportPersisterWriteResult">
+  <int value="0" label="kSuccess"/>
+  <int value="1" label="kFailedCreateDirectory"/>
+  <int value="2" label="kFailedWriteFile"/>
+</enum>
+
 <enum name="ClientSafeBrowsingReportType">
   <int value="0" label="Unknown"/>
   <int value="1" label="URL phishing"/>
@@ -170,6 +182,8 @@
   <int value="22" label="Phishy site interactions"/>
   <int value="23" label="Safe Browsing warning shown to user"/>
   <int value="24" label="Abusive notification permission accepted"/>
+  <int value="25" label="dangerous download auto deleted"/>
+  <int value="26" label="dangerous download canceled on profile closure"/>
 </enum>
 
 <enum name="SafeBrowsingAllowlistAsyncMatch">
diff --git a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
index fb58a63..b48f96f 100644
--- a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
+++ b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
@@ -516,6 +516,46 @@
   </token>
 </histogram>
 
+<histogram
+    name="SafeBrowsing.ClientSafeBrowsingReport.PersistDownloadReportResult"
+    enum="ClientReportPersistDownloadReportResult" expires_after="2024-10-12">
+  <owner>xinghuilu@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records the result of the call to persisting a download report on disk.
+    Logged each time a download report needs to be persisted at shutdown.
+  </summary>
+</histogram>
+
+<histogram
+    name="SafeBrowsing.ClientSafeBrowsingReport.PersisterReadReportSuccessful"
+    enum="BooleanSuccess" expires_after="2024-10-12">
+  <owner>xinghuilu@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records whether reading a persisted report is successful. Logged each time a
+    persisted report is found on startup.
+  </summary>
+</histogram>
+
+<histogram
+    name="SafeBrowsing.ClientSafeBrowsingReport.PersisterReportCountOnStartup"
+    units="count" expires_after="2024-10-12">
+  <owner>xinghuilu@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>Record the number of persisted report on startup.</summary>
+</histogram>
+
+<histogram name="SafeBrowsing.ClientSafeBrowsingReport.PersisterWriteResult"
+    enum="ClientReportPersisterWriteResult" expires_after="2024-10-12">
+  <owner>xinghuilu@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records the result of persisting a report on disk. Logged each time the
+    persister write a report on disk.
+  </summary>
+</histogram>
+
 <histogram name="SafeBrowsing.ClientSafeBrowsingReport.ReportType"
     enum="ClientSafeBrowsingReportType" expires_after="2024-09-29">
   <owner>xinghuilu@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/web_core/enums.xml b/tools/metrics/histograms/metadata/web_core/enums.xml
index b014a1f5..cfa8f7d2 100644
--- a/tools/metrics/histograms/metadata/web_core/enums.xml
+++ b/tools/metrics/histograms/metadata/web_core/enums.xml
@@ -400,22 +400,6 @@
   <int value="1" label="Has client"/>
 </enum>
 
-<enum name="V8CodeCacheGetMetadataType">
-  <int value="0" label="None"/>
-  <int value="1" label="Hot timestamp"/>
-  <int value="2" label="Cold timestamp"/>
-  <int value="3" label="Local compile hints with hot timestamp"/>
-  <int value="4" label="Local compile hints with cold timestamp"/>
-  <int value="5" label="Code cache"/>
-</enum>
-
-<enum name="V8CodeCacheSetMetadataType">
-  <int value="0" label="Timestamp"/>
-  <int value="1" label="Local compile hints at FMP"/>
-  <int value="2" label="Local compile hints at interactive"/>
-  <int value="3" label="Code cache"/>
-</enum>
-
 <enum name="V8CompileHintsModelQuality">
   <int value="0" label="No model"/>
   <int value="1" label="Bad model"/>
diff --git a/tools/metrics/histograms/metadata/web_core/histograms.xml b/tools/metrics/histograms/metadata/web_core/histograms.xml
index 5866c68..262d303 100644
--- a/tools/metrics/histograms/metadata/web_core/histograms.xml
+++ b/tools/metrics/histograms/metadata/web_core/histograms.xml
@@ -793,27 +793,6 @@
   <summary>Whether a parsing blocking script was streamed or not.</summary>
 </histogram>
 
-<histogram name="WebCore.Scripts.V8CodeCacheMetadata.Get"
-    enum="V8CodeCacheGetMetadataType" expires_after="2024-09-15">
-  <owner>marja@chromium.org</owner>
-  <owner>v8-runtime@google.com</owner>
-  <summary>
-    What type of cache metadata we retrieved for a script. Recorded when we are
-    about to compile a script.
-  </summary>
-</histogram>
-
-<histogram name="WebCore.Scripts.V8CodeCacheMetadata.Set"
-    enum="V8CodeCacheSetMetadataType" expires_after="2024-09-15">
-  <owner>marja@chromium.org</owner>
-  <owner>v8-runtime@google.com</owner>
-  <summary>
-    What type of cache metadata we set for a script. If we set multiple metadata
-    types for the same script, multiple samples are recorded. Recorded when we
-    set the metadata.
-  </summary>
-</histogram>
-
 <histogram name="WebCore.Scripts.V8CompileHintsStatus"
     enum="V8CompileHintsStatus" expires_after="2024-09-15">
   <owner>marja@chromium.org</owner>
@@ -847,16 +826,6 @@
   </summary>
 </histogram>
 
-<histogram name="WebCore.Scripts.V8LocalCompileHintsObsoletedByCodeCache"
-    enum="BooleanYesNo" expires_after="2024-09-15">
-  <owner>marja@chromium.org</owner>
-  <owner>v8-runtime@google.com</owner>
-  <summary>
-    Whether there already was a code cache when we tried to set local compile
-    hints. Recorded when we set compile hints.
-  </summary>
-</histogram>
-
 </histograms>
 
 </histogram-configuration>
diff --git a/tools/perf/contrib/shared_storage/shared_storage.py b/tools/perf/contrib/shared_storage/shared_storage.py
index 4058660..954c2f1 100644
--- a/tools/perf/contrib/shared_storage/shared_storage.py
+++ b/tools/perf/contrib/shared_storage/shared_storage.py
@@ -17,7 +17,7 @@
 # Features to enable via command line.
 _ENABLED_FEATURES = [
     'SharedStorageAPI:ExposeDebugMessageForSettingsStatus/true',
-    'SharedStorageAPIM118', 'SharedStorageAPIM124',
+    'SharedStorageAPIM118', 'SharedStorageAPIM125',
     'SharedStorageAPIEnableWALForDatabase',
     'FencedFrames:implementation_type/mparch', 'FencedFramesDefaultMode',
     'PrivacySandboxAdsAPIsOverride', 'DefaultAllowPrivacySandboxAttestations'