diff --git a/DEPS b/DEPS
index d98d459..862fec78 100644
--- a/DEPS
+++ b/DEPS
@@ -288,7 +288,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': '4b0f6f0649d2ed2c214cc093d6a86d4ce0009e52',
+  'angle_revision': '4c60a308592af56dc745d8d1ba994e89ebb053db',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -296,11 +296,11 @@
   # 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': '20b8b48e460b742e34075cf5dd682e25dfea4dc3',
+  'pdfium_revision': '3cd0a262ce17171069d69eb839a0ab9f284c329c',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # and whatever else without interference from each other.
-  'boringssl_revision': '574fd72ebc0a15c4e54066e368c7834e6f86205d',
+  'boringssl_revision': '7db3433bd4466b20ade77494cd3bb03396441aef',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Fuchsia sdk
   # 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 freetype
   # and whatever else without interference from each other.
-  'freetype_revision': 'f219996754c4df75b758983e18cabb401e5b93a1',
+  'freetype_revision': 'ee1310ab5ca2897b760258b94f3d9230335cc2c0',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling freetype
   # and whatever else without interference from each other.
@@ -348,11 +348,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': 'c4c972c5f0bebc45d077172aa42d7ba98f3abbbb',
+  'catapult_revision': '580dbb72d8984e972a02874d06ace7b13295798a',
   # 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': '91db0dddd20500f927076d7309ab54d3b3909344',
+  'chromium_variations_revision': '8010e7fb8e1d64f2c914da216cedf6e3cdfad093',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling CrossBench
   # and whatever else without interference from each other.
@@ -364,7 +364,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling fuzztest
   # and whatever else without interference from each other.
-  'fuzztest_revision': '87fffb7eaf974e55ec736f0100dcd9bdf3f91469',
+  'fuzztest_revision': 'ae6208fc45a09da94d9c0925e26cd9bbca92154b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling domato
   # and whatever else without interference from each other.
@@ -400,7 +400,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.
-  'quiche_revision': '981c424462d9e5210dc843e92b325c93d3bee4e9',
+  'quiche_revision': 'cd46f4983659501081e81ac45237ec723d00f15e',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ink
   # and whatever else without interference from each other.
@@ -1439,7 +1439,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    'bbbe2cd7740a857fb0ba49ec0df3bc6131a604e8',
+    'ff16da349347e90984ba1bc206cafccad280e3c6',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -1598,7 +1598,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'SgrqBlWCI5LkTzOF-U0o3PpLs27z1EHeYf15q-exbVEC',
+          'version': 'fo4-wfWpq9bmst8F64v9PNRzBj6NBcDIlW9IT490PPkC',
       },
     ],
     'condition': 'checkout_android and non_git_source',
@@ -1691,7 +1691,7 @@
       'packages': [
           {
                'package': 'chromium/third_party/android_build_tools/lint',
-               'version': 'Q2AdRKMr8eMrKgLZ9CwGTRx4BwGOL2wc6Ad942McdDUC',
+               'version': '_77LT8DN3c2A3RPC4ctJk-kOk2K_QEuS3_aQty7g384C',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -1934,7 +1934,7 @@
 
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + 'a912cd245b093ea57a187ca66665ca98090a82fd',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + 'b576ab3b78a9d19c33060c821d4f11643397fa30',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -2436,7 +2436,7 @@
     Var('pdfium_git') + '/pdfium.git' + '@' +  Var('pdfium_revision'),
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + '0b3d3cc75b911d55cb138252231d2c929e312dba',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + '38f6220c882b08ed8bd8cbb47cff50cfa790e41b',
 
   'src/base/tracing/test/data': {
     'bucket': 'perfetto',
@@ -2752,7 +2752,7 @@
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@8d2a13cc437488e3dafc128371e0fb78cd1a216b',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@ee1a15d510c031acaff02138a922ccdb6f85c2a7',
   'src/third_party/glslang/src': '{chromium_git}/external/github.com/KhronosGroup/glslang@e43514866f7e0f8265c677039d2fe773c892d44b',
   'src/third_party/spirv-cross/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Cross@b8fcf307f1f347089e3c46eb4451d27f32ebc8d3',
   'src/third_party/spirv-headers/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Headers@a380cd25433092dbce9a455a3feb1242138febee',
@@ -2761,7 +2761,7 @@
   'src/third_party/vulkan-loader/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Loader@369f59ad598b60d6ed9f553af651c5cccd20234c',
   'src/third_party/vulkan-tools/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Tools@315964ad5aabd5b148a484e5fbea8a365c8d1eb3',
   'src/third_party/vulkan-utility-libraries/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Utility-Libraries@5a88b6042edb8f03eefc8de73bd73a899989373f',
-  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@ea05bf71f2948d64f7d36651ffcad59216401095',
+  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@5cceb78082833556789a64f3237b04df7a826d93',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + '56300b29fbfcc693ee6609ddad3fdd5b7a449a21',
@@ -2806,7 +2806,7 @@
     Var('chromium_git') + '/webpagereplay.git' + '@' + Var('webpagereplay_revision'),
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '83861d5649c83d1df82fafa622f469ac272f21aa',
+    Var('webrtc_git') + '/src.git' + '@' + 'da8a535ad4e41fbb587b38346d324a2892ba4183',
 
   # 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.
@@ -3071,7 +3071,7 @@
       'packages': [
           {
               'package': 'chromium/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework',
-              'version': 'version:2@4.0.0.cr1',
+              'version': 'version:2@4.1.1.cr1',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -3148,7 +3148,7 @@
       'packages': [
           {
               'package': 'chromium/third_party/android_deps/libs/com_google_android_gms_play_services_basement',
-              'version': 'version:2@18.4.0.cr1',
+              'version': 'version:2@18.5.0.cr1',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -3236,7 +3236,7 @@
       'packages': [
           {
               'package': 'chromium/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials',
-              'version': 'version:2@16.0.0-alpha02.cr1',
+              'version': 'version:2@16.0.0-alpha04.cr1',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -4634,7 +4634,7 @@
 
   'src/ios_internal':  {
       'url': Var('chrome_git') + '/chrome/ios_internal.git' + '@' +
-        '7c7a00774ea89513f74dcb2f89bf976d889ffb87',
+        '53efd8418bb70bf06616f417a7090f9d3b659343',
       'condition': 'checkout_ios and checkout_src_internal',
   },
 
@@ -4842,19 +4842,19 @@
 
   'src/third_party/widevine/cdm/linux': {
       'url': Var('chrome_git') + '/chrome/deps/widevine/cdm/linux.git' + '@' +
-        '91eaf08b6d45a593d8fa9a3aaff2ff0f6a97b422',
+        '8a12afc6ad470fac67ecb97bc9acf4bdbf9285e7',
       'condition': 'checkout_linux and checkout_src_internal',
   },
 
   'src/third_party/widevine/cdm/mac': {
       'url': Var('chrome_git') + '/chrome/deps/widevine/cdm/mac.git' + '@' +
-        'a0b43dc94b6519a2f0e4163801748e234fdfdaff',
+        '8c2898cf5e27669beeb7fc432a30f953f2541106',
       'condition': 'checkout_mac and checkout_src_internal',
     },
 
   'src/third_party/widevine/cdm/win': {
       'url': Var('chrome_git') + '/chrome/deps/widevine/cdm/win.git' + '@' +
-        '7c1534e91a8bfdc4af28f8b7d6900343a845a8af',
+        '33d580b25178a85837950972b985f555c6d65fa9',
       'condition': 'checkout_win and checkout_src_internal',
   },
 
diff --git a/WATCHLISTS b/WATCHLISTS
index fc37c23..568588a 100644
--- a/WATCHLISTS
+++ b/WATCHLISTS
@@ -750,7 +750,8 @@
                   '|services/resource_coordinator/',
     },
     'chrome_intelligence': {
-      'filepath': 'history_clusters'\
+      'filepath': 'contextual_cueing'\
+                  '|history_clusters'\
                   '|history_embeddings'\
                   '|image_service'\
                   '|ios/chrome/browser/text_selection'\
diff --git a/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java b/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
index 2362f67..f2d56dd 100644
--- a/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
+++ b/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
@@ -1030,6 +1030,18 @@
                 "SimpleCachePrioritizedCaching",
                 "When enabled, main frame navigation resources will be prioritized in Simple"
                         + " Cache."),
+        Flag.baseFeature(
+                CcFeatures.PREVENT_DUPLICATE_IMAGE_DECODES,
+                "De-duplicate and share image decode requests between raster tasks "
+                        + "and javascript image decode requests."),
+        Flag.baseFeature(
+                CcFeatures.SEND_EXPLICIT_DECODE_REQUESTS_IMMEDIATELY,
+                "Forward javascript image decode requests to cc right away, "
+                        + "rather than bundling them into the next compositor commit."),
+        Flag.baseFeature(
+                BlinkFeatures.SPECULATIVE_IMAGE_DECODES,
+                "Start decoding in-viewport images as soon as they have loaded, "
+                        + "rather than waiting for them to appear in a raster task."),
         // Add new commandline switches and features above. The final entry should have a
         // trailing comma for cleaner diffs.
     };
diff --git a/ash/accessibility/magnifier/magnifier_glass.cc b/ash/accessibility/magnifier/magnifier_glass.cc
index 3f8dd873..a68634b 100644
--- a/ash/accessibility/magnifier/magnifier_glass.cc
+++ b/ash/accessibility/magnifier/magnifier_glass.cc
@@ -196,7 +196,6 @@
   zoom_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
   zoom_layer_->SetBounds(window_bounds);
   zoom_layer_->SetBackgroundZoom(params_.scale, kZoomInset);
-  zoom_layer_->SetFillsBoundsOpaquely(false);
   root_layer->Add(zoom_layer_.get());
 
   // Create a rounded rect clip, so that only we see a circle of the zoomed
diff --git a/ash/app_list/views/app_list_bubble_view.cc b/ash/app_list/views/app_list_bubble_view.cc
index b01ac85..a15d0a9 100644
--- a/ash/app_list/views/app_list_bubble_view.cc
+++ b/ash/app_list/views/app_list_bubble_view.cc
@@ -105,7 +105,6 @@
   SeparatorWithLayer() {
     SetPaintToLayer(ui::LAYER_SOLID_COLOR);
     // Color is set in OnThemeChanged().
-    layer()->SetFillsBoundsOpaquely(false);
   }
   SeparatorWithLayer(const SeparatorWithLayer&) = delete;
   SeparatorWithLayer& operator=(const SeparatorWithLayer&) = delete;
diff --git a/ash/app_list/views/app_list_folder_view.cc b/ash/app_list/views/app_list_folder_view.cc
index d5aafe2..f93f27bf 100644
--- a/ash/app_list/views/app_list_folder_view.cc
+++ b/ash/app_list/views/app_list_folder_view.cc
@@ -692,7 +692,6 @@
         ColorProvider::kBackgroundBlurSigma);
     animating_background_->layer()->SetBackdropFilterQuality(
         ColorProvider::kBackgroundBlurQuality);
-    animating_background_->layer()->SetFillsBoundsOpaquely(false);
   }
 
   animating_background_->SetVisible(false);
diff --git a/ash/app_list/views/app_list_item_view.cc b/ash/app_list/views/app_list_item_view.cc
index 5067ac9..c68bc9c2 100644
--- a/ash/app_list/views/app_list_item_view.cc
+++ b/ash/app_list/views/app_list_item_view.cc
@@ -663,7 +663,6 @@
 
   icon_background_ = AddChildView(std::make_unique<views::View>());
   icon_background_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
-  icon_background_->layer()->SetFillsBoundsOpaquely(false);
   icon_background_->SetCanProcessEventsWithinSubtree(false);
   icon_background_->SetVisible(is_folder_);
 
diff --git a/ash/app_list/views/assistant/assistant_page_view.cc b/ash/app_list/views/assistant/assistant_page_view.cc
index e9098ed..9e7ee7f 100644
--- a/ash/app_list/views/assistant/assistant_page_view.cc
+++ b/ash/app_list/views/assistant/assistant_page_view.cc
@@ -413,7 +413,6 @@
 void AssistantPageView::InitLayout() {
   // Use a solid color layer. The color is set in OnThemeChanged().
   SetPaintToLayer(ui::LAYER_SOLID_COLOR);
-  layer()->SetFillsBoundsOpaquely(!chromeos::features::IsSystemBlurEnabled());
 
   view_shadow_ = std::make_unique<views::ViewShadow>(this, kShadowElevation);
   view_shadow_->SetRoundedCornerRadius(
diff --git a/ash/app_list/views/continue_task_view.cc b/ash/app_list/views/continue_task_view.cc
index 9deb803d..f4a947d 100644
--- a/ash/app_list/views/continue_task_view.cc
+++ b/ash/app_list/views/continue_task_view.cc
@@ -368,8 +368,12 @@
       return TaskResultType::kLocalFile;
     case AppListSearchResultType::kZeroStateDrive:
       return TaskResultType::kDriveFile;
+    case AppListSearchResultType::kZeroStateHelpApp:
+      return TaskResultType::kHelpApp;
+    case AppListSearchResultType::kDesksAdminTemplate:
+      return TaskResultType::kDesksAdminTemplate;
     default:
-      NOTREACHED();
+      return TaskResultType::kUnknown;
   }
 }
 
diff --git a/ash/app_list/views/continue_task_view.h b/ash/app_list/views/continue_task_view.h
index b2b90d5..836abc6 100644
--- a/ash/app_list/views/continue_task_view.h
+++ b/ash/app_list/views/continue_task_view.h
@@ -48,7 +48,9 @@
     kLocalFile = 0,
     kDriveFile = 1,
     kUnknown = 2,
-    kMaxValue = kUnknown,
+    kHelpApp = 3,
+    kDesksAdminTemplate = 4,
+    kMaxValue = kDesksAdminTemplate,
   };
 
   ContinueTaskView(AppListViewDelegate* view_delegate, bool tablet_mode);
diff --git a/ash/app_list/views/top_icon_animation_view.cc b/ash/app_list/views/top_icon_animation_view.cc
index 33026cb8..1fb7bd3 100644
--- a/ash/app_list/views/top_icon_animation_view.cc
+++ b/ash/app_list/views/top_icon_animation_view.cc
@@ -47,7 +47,6 @@
     icon_background_ = AddChildView(std::make_unique<views::View>());
     if (item_in_folder_icon_) {
       icon_background_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
-      icon_background_->layer()->SetFillsBoundsOpaquely(false);
     } else {
       const int background_diameter =
           app_list_config->GetShortcutBackgroundContainerDimension();
diff --git a/ash/birch/birch_coral_provider.cc b/ash/birch/birch_coral_provider.cc
index 9fc4934..6585663 100644
--- a/ash/birch/birch_coral_provider.cc
+++ b/ash/birch/birch_coral_provider.cc
@@ -506,11 +506,13 @@
 
 void BirchCoralProvider::OnWindowParentChanged(aura::Window* window,
                                                aura::Window* parent) {
-  // When the last group is launched the `in_session_source_desk_` is reset and
-  // its windows are still being observed, we only need to removed the
-  // observation.
-  if (!in_session_source_desk_) {
-    windows_observation_.RemoveObservation(window);
+  // Reset the observations when `response_` or `in_session_source_desk_` are
+  // null. This can occur when launching the last group which resets the
+  // `in_session_source_desk_`.
+  // TODO(crbug.com/383770356): Still need to find out the reason why the window
+  // is still being observed when the `response_` has been reset.
+  if (!response_ || !in_session_source_desk_) {
+    Reset();
     return;
   }
 
@@ -543,9 +545,7 @@
   // Clear the in-session `response_` and reset the in-session source desk and
   // the app windows observation.
   if (response_ && response_->source() == CoralSource::kInSession) {
-    response_.reset();
-    in_session_source_desk_ = nullptr;
-    windows_observation_.RemoveAllObservations();
+    Reset();
   }
 }
 
@@ -553,8 +553,7 @@
     session_manager::SessionState state) {
   // Clear stale items on login.
   if (state == session_manager::SessionState::ACTIVE) {
-    response_.reset();
-    in_session_source_desk_ = nullptr;
+    Reset();
   }
 }
 
@@ -861,4 +860,10 @@
   }
 }
 
+void BirchCoralProvider::Reset() {
+  response_.reset();
+  in_session_source_desk_ = nullptr;
+  windows_observation_.RemoveAllObservations();
+}
+
 }  // namespace ash
diff --git a/ash/birch/birch_coral_provider.h b/ash/birch/birch_coral_provider.h
index 659594e..80ad7f16 100644
--- a/ash/birch/birch_coral_provider.h
+++ b/ash/birch/birch_coral_provider.h
@@ -157,6 +157,9 @@
   // current in-session `response_`.
   void RemoveEntity(std::string_view entity_identifier);
 
+  // Resets raw pointers and window observations when exiting Overview mode.
+  void Reset();
+
   // The request sent to the coral backend.
   CoralRequest request_;
 
diff --git a/ash/capture_mode/user_nudge_controller.cc b/ash/capture_mode/user_nudge_controller.cc
index b69e58a..454bb56 100644
--- a/ash/capture_mode/user_nudge_controller.cc
+++ b/ash/capture_mode/user_nudge_controller.cc
@@ -68,10 +68,8 @@
       DarkLightModeControllerImpl::Get()->IsDarkModeEnabled() ? SK_ColorWHITE
                                                               : SK_ColorBLACK;
   base_ring_.SetColor(ring_color);
-  base_ring_.SetFillsBoundsOpaquely(false);
   base_ring_.SetOpacity(0);
   ripple_ring_.SetColor(ring_color);
-  ripple_ring_.SetFillsBoundsOpaquely(false);
   ripple_ring_.SetOpacity(0);
 
   Reposition();
diff --git a/ash/constants/ash_switches.cc b/ash/constants/ash_switches.cc
index 1ddf300..a3aefff6 100644
--- a/ash/constants/ash_switches.cc
+++ b/ash/constants/ash_switches.cc
@@ -545,9 +545,9 @@
 const char kEnterpriseForceManualEnrollmentInTestBuilds[] =
     "enterprise-force-manual-enrollment-in-test-builds";
 
-// Whether to enable unified state determination.
+// Whether to enable state determination.
 const char kEnterpriseEnableUnifiedStateDetermination[] =
-    "enterprise-enable-unified-state-determination";
+    "enterprise-enable-state-determination";
 
 // Whether to enable forced enterprise re-enrollment.
 const char kEnterpriseEnableForcedReEnrollment[] =
diff --git a/ash/rotator/screen_rotation_animator.cc b/ash/rotator/screen_rotation_animator.cc
index 7d03ac84a..f319849 100644
--- a/ash/rotator/screen_rotation_animator.cc
+++ b/ash/rotator/screen_rotation_animator.cc
@@ -29,6 +29,7 @@
 #include "ui/compositor/layer_animator.h"
 #include "ui/compositor/layer_owner.h"
 #include "ui/compositor/layer_tree_owner.h"
+#include "ui/compositor/layer_type.h"
 #include "ui/compositor/scoped_animation_duration_scale_mode.h"
 #include "ui/display/display.h"
 #include "ui/display/manager/display_manager.h"
@@ -367,12 +368,10 @@
       GetScreenRotationContainer(root_window_)->layer()->size();
   std::unique_ptr<ui::Layer> copy_layer =
       CreateLayerFromCopyOutputResult(std::move(result), layer_size);
+  CHECK_EQ(copy_layer->type(), ui::LAYER_SOLID_COLOR);
   DCHECK_EQ(copy_layer->size(),
             GetScreenRotationContainer(root_window_)->layer()->size());
 
-  // TODO(crbug.com/40113966): This is a workaround and should be removed once
-  // the issue is fixed.
-  copy_layer->SetFillsBoundsOpaquely(false);
   return std::make_unique<ui::LayerTreeOwner>(std::move(copy_layer));
 }
 
diff --git a/ash/style/counter_expand_button.cc b/ash/style/counter_expand_button.cc
index 69cf4c7e..f7b9c90 100644
--- a/ash/style/counter_expand_button.cc
+++ b/ash/style/counter_expand_button.cc
@@ -74,7 +74,6 @@
   views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);
 
   SetPaintToLayer(ui::LAYER_SOLID_COLOR);
-  layer()->SetFillsBoundsOpaquely(false);
   layer()->SetRoundedCornerRadius(gfx::RoundedCornersF{kTrayItemCornerRadius});
   layer()->SetIsFastRoundedCorner(true);
 }
diff --git a/ash/system/bluetooth/bluetooth_device_list_item_battery_view.cc b/ash/system/bluetooth/bluetooth_device_list_item_battery_view.cc
index 64c810d..7ff0f03 100644
--- a/ash/system/bluetooth/bluetooth_device_list_item_battery_view.cc
+++ b/ash/system/bluetooth/bluetooth_device_list_item_battery_view.cc
@@ -92,8 +92,8 @@
       GetColorProvider()->GetColor(color_id));
   battery_image_info.charge_percent = new_battery_percentage;
 
-  icon_->SetImage(PowerStatus::GetBatteryImage(
-      battery_image_info, kUnifiedTraySubIconSize, GetColorProvider()));
+  icon_->SetImage(PowerStatus::GetBatteryImageModel(battery_image_info,
+                                                    kUnifiedTraySubIconSize));
 }
 
 bool BluetoothDeviceListItemBatteryView::ApproximatelyEqual(
diff --git a/ash/system/privacy/privacy_indicators_tray_item_view.cc b/ash/system/privacy/privacy_indicators_tray_item_view.cc
index 7927ce7..08d2bd3 100644
--- a/ash/system/privacy/privacy_indicators_tray_item_view.cc
+++ b/ash/system/privacy/privacy_indicators_tray_item_view.cc
@@ -165,7 +165,6 @@
   // Set up a solid color layer to paint the background color, then add a layer
   // to each child so that they are visible and can perform layer animation.
   SetPaintToLayer(ui::LAYER_SOLID_COLOR);
-  layer()->SetFillsBoundsOpaquely(false);
   layer()->SetRoundedCornerRadius(
       gfx::RoundedCornersF{kPrivacyIndicatorsViewExpandedShorterSideSize / 2});
   layer()->SetIsFastRoundedCorner(true);
@@ -173,7 +172,6 @@
   auto add_icon_to_container = [&container_view]() {
     auto icon = std::make_unique<views::ImageView>();
     icon->SetPaintToLayer();
-    icon->layer()->SetFillsBoundsOpaquely(false);
     icon->SetVisible(false);
     return container_view->AddChildView(std::move(icon));
   };
diff --git a/ash/system/privacy/privacy_indicators_tray_item_view_pixeltest.cc b/ash/system/privacy/privacy_indicators_tray_item_view_pixeltest.cc
index 131b874..3a5975b 100644
--- a/ash/system/privacy/privacy_indicators_tray_item_view_pixeltest.cc
+++ b/ash/system/privacy/privacy_indicators_tray_item_view_pixeltest.cc
@@ -75,7 +75,7 @@
   SimulateAnimateToFullyExpandedState(privacy_indicators_view);
   EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
       "full_view" + GetScreenshotNameSuffix(),
-      /*revision_number=*/1, notification_center_tray));
+      /*revision_number=*/2, notification_center_tray));
 
   SimulateAnimationEnded(privacy_indicators_view);
   EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
diff --git a/ash/system/tray/tray_background_view.cc b/ash/system/tray/tray_background_view.cc
index 269b241..dac5365f 100644
--- a/ash/system/tray/tray_background_view.cc
+++ b/ash/system/tray/tray_background_view.cc
@@ -298,7 +298,6 @@
   // Use layer color to provide background color. Note that children views
   // need to have their own layers to be visible.
   SetPaintToLayer(ui::LAYER_SOLID_COLOR);
-  layer()->SetFillsBoundsOpaquely(!chromeos::features::IsSystemBlurEnabled());
 
   // Start the tray items not visible, because visibility changes are animated.
   views::View::SetVisible(false);
diff --git a/ash/system/unified/power_button.cc b/ash/system/unified/power_button.cc
index 2d0c30d..6a87f7178 100644
--- a/ash/system/unified/power_button.cc
+++ b/ash/system/unified/power_button.cc
@@ -398,7 +398,6 @@
   background_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
   auto* background_layer = background_view_->layer();
   background_layer->SetRoundedCornerRadius(kAllRoundedCorners);
-  background_layer->SetFillsBoundsOpaquely(false);
   background_layer->SetIsFastRoundedCorner(true);
 
   set_context_menu_controller(context_menu_.get());
diff --git a/ash/system/video_conference/bubble/title_view.cc b/ash/system/video_conference/bubble/title_view.cc
index 20424f2..5a71b2f 100644
--- a/ash/system/video_conference/bubble/title_view.cc
+++ b/ash/system/video_conference/bubble/title_view.cc
@@ -107,7 +107,6 @@
   background_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
   auto* background_layer = background_view_->layer();
   background_layer->SetRoundedCornerRadius(gfx::RoundedCornersF(16));
-  background_layer->SetFillsBoundsOpaquely(false);
 
   AddChildView(std::make_unique<MicTestButtonContainer>(base::BindRepeating(
       &MicTestButton::OnMicTestButtonClicked, base::Unretained(this))));
diff --git a/ash/user_education/welcome_tour/welcome_tour_scrim.cc b/ash/user_education/welcome_tour/welcome_tour_scrim.cc
index e03ae7f..4bb3528 100644
--- a/ash/user_education/welcome_tour/welcome_tour_scrim.cc
+++ b/ash/user_education/welcome_tour/welcome_tour_scrim.cc
@@ -208,7 +208,6 @@
   // Invoked once to initialize `this` scrim.
   void Init() {
     // Configure static scrim layer properties.
-    layer_owner_.layer()->SetFillsBoundsOpaquely(false);
     layer_owner_.layer()->SetMaskLayer(mask_layer_owner_.layer());
     layer_owner_.layer()->SetName(WelcomeTourScrim::kLayerName);
 
diff --git a/ash/utility/layer_copy_animator.cc b/ash/utility/layer_copy_animator.cc
index a3d0eb5..1a14aaf0 100644
--- a/ash/utility/layer_copy_animator.cc
+++ b/ash/utility/layer_copy_animator.cc
@@ -10,6 +10,7 @@
 #include "ui/base/class_property.h"
 #include "ui/compositor/layer_animation_sequence.h"
 #include "ui/compositor/layer_animator.h"
+#include "ui/compositor/layer_type.h"
 
 DEFINE_UI_CLASS_PROPERTY_TYPE(ash::LayerCopyAnimator*)
 
@@ -114,7 +115,7 @@
 }
 
 void LayerCopyAnimator::RunAnimation() {
-  copied_layer_->SetFillsBoundsOpaquely(false);
+  CHECK_EQ(copied_layer_->type(), ui::LAYER_SOLID_COLOR);
 
   auto* parent_layer = window_->layer()->parent();
   parent_layer->Add(copied_layer_.get());
diff --git a/ash/wm/window_mini_view.cc b/ash/wm/window_mini_view.cc
index 7e0e5e8..23ed53e 100644
--- a/ash/wm/window_mini_view.cc
+++ b/ash/wm/window_mini_view.cc
@@ -136,9 +136,7 @@
     backdrop_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
 
     ui::Layer* layer = backdrop_view_->layer();
-
     layer->SetName("BackdropView");
-    layer->SetFillsBoundsOpaquely(false);
 
     const int corner_radius = window_util::GetMiniWindowRoundedCornerRadius();
     layer->SetRoundedCornerRadius(
diff --git a/ash/wm/window_mini_view_header_view.cc b/ash/wm/window_mini_view_header_view.cc
index 734d4e7..e15aa10 100644
--- a/ash/wm/window_mini_view_header_view.cc
+++ b/ash/wm/window_mini_view_header_view.cc
@@ -121,8 +121,9 @@
     icon_view_->layer()->SetFillsBoundsOpaquely(false);
   }
 
-  icon_view_->SetImage(gfx::ImageSkiaOperations::CreateResizedImage(
-      *icon, skia::ImageOperations::RESIZE_BEST, kIconSize));
+  icon_view_->SetImage(ui::ImageModel::FromImageSkia(
+      gfx::ImageSkiaOperations::CreateResizedImage(
+          *icon, skia::ImageOperations::RESIZE_BEST, kIconSize)));
 }
 
 void WindowMiniViewHeaderView::UpdateTitleLabel(aura::Window* window) {
diff --git a/base/android/pre_freeze_background_memory_trimmer.cc b/base/android/pre_freeze_background_memory_trimmer.cc
index 0053755..4ded905 100644
--- a/base/android/pre_freeze_background_memory_trimmer.cc
+++ b/base/android/pre_freeze_background_memory_trimmer.cc
@@ -74,7 +74,7 @@
 std::string GetSelfCompactionMetricName(std::string_view name,
                                         std::string_view suffix) {
   const char* process_type = GetProcessType();
-  return StrCat({"Memory.SelfCompact.", process_type, ".", name, ".", suffix});
+  return StrCat({"Memory.SelfCompact2.", process_type, ".", name, ".", suffix});
 }
 
 class PrivateMemoryFootprintMetric
@@ -517,8 +517,8 @@
     base::TimeTicks started_at) {
   TRACE_EVENT0("base", "StartSelfCompaction");
   metric->RecordBeforeMetrics();
-  SelfCompactionTask(std::move(task_runner), std::move(regions),
-                     std::move(metric), max_bytes, started_at);
+  MaybePostSelfCompactionTask(std::move(task_runner), std::move(regions),
+                              std::move(metric), max_bytes, started_at);
 }
 
 void PreFreezeBackgroundMemoryTrimmer::FinishSelfCompaction(
@@ -559,13 +559,17 @@
   TRACE_EVENT0("base", "CompactSelf");
   std::vector<debug::MappedMemoryRegion> regions;
 
-  std::string proc_maps;
-  if (!debug::ReadProcMaps(&proc_maps) || !ParseProcMaps(proc_maps, &regions)) {
-    return;
-  }
+  // We still start the task in the control group, in order to record metrics.
+  if (base::FeatureList::IsEnabled(kShouldFreezeSelf)) {
+    std::string proc_maps;
+    if (!debug::ReadProcMaps(&proc_maps) ||
+        !ParseProcMaps(proc_maps, &regions)) {
+      return;
+    }
 
-  if (regions.size() == 0) {
-    return;
+    if (regions.size() == 0) {
+      return;
+    }
   }
 
   auto started_at = base::TimeTicks::Now();
@@ -646,10 +650,6 @@
 
 // static
 void PreFreezeBackgroundMemoryTrimmer::OnSelfFreeze() {
-  if (!base::FeatureList::IsEnabled(kShouldFreezeSelf)) {
-    return;
-  }
-
   TRACE_EVENT0("base", "OnSelfFreeze");
 
   Instance().OnSelfFreezeInternal();
@@ -657,7 +657,9 @@
 
 void PreFreezeBackgroundMemoryTrimmer::OnSelfFreezeInternal() {
   base::AutoLock locker(lock_);
-  RunPreFreezeTasks();
+  if (base::FeatureList::IsEnabled(kShouldFreezeSelf)) {
+    RunPreFreezeTasks();
+  }
 
   base::ThreadPool::PostDelayedTask(
       FROM_HERE, {base::TaskPriority::BEST_EFFORT, MayBlock()},
diff --git a/base/android/pre_freeze_background_memory_trimmer_unittest.cc b/base/android/pre_freeze_background_memory_trimmer_unittest.cc
index 4057e551..0be0025d 100644
--- a/base/android/pre_freeze_background_memory_trimmer_unittest.cc
+++ b/base/android/pre_freeze_background_memory_trimmer_unittest.cc
@@ -849,7 +849,8 @@
   task_environment_.FastForwardBy(base::Seconds(60));
 
   // No metrics should have been recorded, since we cancelled self compaction.
-  EXPECT_EQ(histograms.GetTotalCountsForPrefix("Memory.SelfCompact").size(), 0);
+  EXPECT_EQ(histograms.GetTotalCountsForPrefix("Memory.SelfCompact2").size(),
+            0);
 
   for (size_t i = 1; i < 5; i++) {
     Unmap(addrs[i], i * base::GetPageSize());
@@ -885,7 +886,10 @@
           started_at),
       1, started_at);
 
-  for (size_t i = 0; i < 3; i++) {
+  // We should have 4 sections here, based on the sizes mapped above.
+  // |StartSelfCompaction| doesn't run right away, but rather schedules a task.
+  // So, we expect to have 4 tasks to run here.
+  for (size_t i = 0; i < 4; i++) {
     EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 1u);
     task_environment_.FastForwardBy(
         task_environment_.NextMainThreadPendingTaskDelay());
@@ -903,12 +907,12 @@
     for (const auto& timing :
          {"Before", "After", "After1s", "After10s", "After60s"}) {
       histograms.ExpectTotalCount(
-          StrCat({"Memory.SelfCompact.Browser.", name, ".", timing}), 1);
+          StrCat({"Memory.SelfCompact2.Browser.", name, ".", timing}), 1);
     }
     for (const auto& timing :
          {"BeforeAfter", "After1s", "After10s", "After60s"}) {
       const auto metric =
-          StrCat({"Memory.SelfCompact.Browser.", name, ".Diff.", timing});
+          StrCat({"Memory.SelfCompact2.Browser.", name, ".Diff.", timing});
       base::HistogramTester::CountsMap diff_metrics;
       diff_metrics[StrCat({metric, ".Increase"})] = 1;
       diff_metrics[StrCat({metric, ".Decrease"})] = 1;
@@ -919,7 +923,7 @@
 
   // We also check that no other histograms (other than the ones expected above)
   // were recorded.
-  EXPECT_EQ(histograms.GetTotalCountsForPrefix("Memory.SelfCompact").size(),
+  EXPECT_EQ(histograms.GetTotalCountsForPrefix("Memory.SelfCompact2").size(),
             45);
 
   for (size_t i = 1; i < 5; i++) {
@@ -928,4 +932,51 @@
     Unmap(addrs[i], len);
   }
 }
+
+// Test that we still record metrics even when the feature is disabled.
+TEST_F(PreFreezeSelfCompactionTest, Disabled) {
+  // Although we are not actually compacting anything, the self compaction
+  // code will exit out before metrics are recorded in the case where compaction
+  // is not supported.
+  if (!PreFreezeBackgroundMemoryTrimmer::SelfCompactionIsSupported()) {
+    GTEST_SKIP() << "No kernel support";
+  }
+
+  base::test::ScopedFeatureList feature_list_;
+  feature_list_.InitAndDisableFeature(kShouldFreezeSelf);
+
+  base::HistogramTester histograms;
+
+  PreFreezeBackgroundMemoryTrimmer::Instance().CompactSelf();
+
+  // Run metrics
+  task_environment_.FastForwardBy(base::Seconds(60));
+
+  // We check here for the names of each metric we expect to be recorded. We
+  // can't easily check for the exact values of these metrics unfortunately,
+  // since they depend on reading /proc/self/smaps_rollup.
+  for (const auto& name : {"Rss", "Pss", "PssAnon", "PssFile", "SwapPss"}) {
+    for (const auto& timing :
+         {"Before", "After", "After1s", "After10s", "After60s"}) {
+      histograms.ExpectTotalCount(
+          StrCat({"Memory.SelfCompact2.Browser.", name, ".", timing}), 1);
+    }
+    for (const auto& timing :
+         {"BeforeAfter", "After1s", "After10s", "After60s"}) {
+      const auto metric =
+          StrCat({"Memory.SelfCompact2.Browser.", name, ".Diff.", timing});
+      base::HistogramTester::CountsMap diff_metrics;
+      diff_metrics[StrCat({metric, ".Increase"})] = 1;
+      diff_metrics[StrCat({metric, ".Decrease"})] = 1;
+      EXPECT_THAT(histograms.GetTotalCountsForPrefix(metric),
+                  testing::IsSubsetOf(diff_metrics));
+    }
+  }
+
+  // We also check that no other histograms (other than the ones expected above)
+  // were recorded.
+  EXPECT_EQ(histograms.GetTotalCountsForPrefix("Memory.SelfCompact2").size(),
+            45);
+}
+
 }  // namespace base::android
diff --git a/base/task/thread_pool/task_tracker.cc b/base/task/thread_pool/task_tracker.cc
index cb1563a..0e779e003 100644
--- a/base/task/thread_pool/task_tracker.cc
+++ b/base/task/thread_pool/task_tracker.cc
@@ -98,30 +98,6 @@
 }
 #endif  //  BUILDFLAG(ENABLE_BASE_TRACING)
 
-auto EmitThreadPoolTraceEventMetadata(perfetto::EventContext& ctx,
-                                      const TaskTraits& traits,
-                                      TaskSource* task_source,
-                                      const SequenceToken& token) {
-#if BUILDFLAG(ENABLE_BASE_TRACING)
-  // Other parameters are included only when "scheduler" category is enabled.
-  const uint8_t* scheduler_category_enabled =
-      TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED("scheduler");
-
-  if (!*scheduler_category_enabled) {
-    return;
-  }
-  auto* task = ctx.event<perfetto::protos::pbzero::ChromeTrackEvent>()
-                   ->set_thread_pool_task();
-  task->set_task_priority(TaskPriorityToProto(traits.priority()));
-  task->set_execution_mode(ExecutionModeToProto(task_source->execution_mode()));
-  task->set_shutdown_behavior(
-      ShutdownBehaviorToProto(traits.shutdown_behavior()));
-  if (token.IsValid()) {
-    task->set_sequence_token(token.ToInternalValue());
-  }
-#endif  //  BUILDFLAG(ENABLE_BASE_TRACING)
-}
-
 // If this is greater than 0 on a given thread, it will ignore the DCHECK which
 // prevents posting BLOCK_SHUTDOWN tasks after shutdown. There are cases where
 // posting back to a BLOCK_SHUTDOWN sequence is a coincidence rather than part
@@ -656,6 +632,34 @@
   }
 }
 
+void TaskTracker::EmitThreadPoolTraceEventMetadata(perfetto::EventContext& ctx,
+                                                   const TaskTraits& traits,
+                                                   TaskSource* task_source,
+                                                   const SequenceToken& token) {
+#if BUILDFLAG(ENABLE_BASE_TRACING)
+  if (TRACE_EVENT_CATEGORY_ENABLED("scheduler.flow")) {
+    if (token.IsValid()) {
+      ctx.event()->add_flow_ids(reinterpret_cast<uint64_t>(this) ^
+                                static_cast<uint64_t>(token.ToInternalValue()));
+    }
+  }
+
+  // Other parameters are included only when "scheduler" category is enabled.
+  if (TRACE_EVENT_CATEGORY_ENABLED("scheduler")) {
+    auto* task = ctx.event<perfetto::protos::pbzero::ChromeTrackEvent>()
+                     ->set_thread_pool_task();
+    task->set_task_priority(TaskPriorityToProto(traits.priority()));
+    task->set_execution_mode(
+        ExecutionModeToProto(task_source->execution_mode()));
+    task->set_shutdown_behavior(
+        ShutdownBehaviorToProto(traits.shutdown_behavior()));
+    if (token.IsValid()) {
+      task->set_sequence_token(token.ToInternalValue());
+    }
+  }
+#endif  //  BUILDFLAG(ENABLE_BASE_TRACING)
+}
+
 NOINLINE void TaskTracker::RunContinueOnShutdown(Task& task,
                                                  const TaskTraits& traits,
                                                  TaskSource* task_source,
diff --git a/base/task/thread_pool/task_tracker.h b/base/task/thread_pool/task_tracker.h
index dcab998..50b62529 100644
--- a/base/task/thread_pool/task_tracker.h
+++ b/base/task/thread_pool/task_tracker.h
@@ -204,6 +204,13 @@
   // Invokes all |flush_callbacks_for_testing_| if any in a lock-safe manner.
   void InvokeFlushCallbacksForTesting();
 
+  // Adds ThreadPool related trace event metadata to the event `ctx`. Notably,
+  // records sequence information, as well as priority/execution mode.
+  void EmitThreadPoolTraceEventMetadata(perfetto::EventContext& ctx,
+                                        const TaskTraits& traits,
+                                        TaskSource* task_source,
+                                        const SequenceToken& token);
+
   // Dummy frames to allow identification of shutdown behavior in a stack trace.
   void RunContinueOnShutdown(Task& task,
                              const TaskTraits& traits,
diff --git a/base/trace_event/builtin_categories.h b/base/trace_event/builtin_categories.h
index dfa4928b..c428a5f8 100644
--- a/base/trace_event/builtin_categories.h
+++ b/base/trace_event/builtin_categories.h
@@ -166,6 +166,10 @@
     perfetto::Category("SiteEngagement"),
     perfetto::Category("safe_browsing"),
     perfetto::Category("scheduler"),
+    perfetto::Category("scheduler.flow").SetDescription(
+        "Includes flow events related to scheduling dependency. Notably, "
+        "records flows between tasks running in the thread pool on the same "
+        "sequence."),
     perfetto::Category("scheduler.long_tasks"),
     perfetto::Category("screenlock_monitor"),
     perfetto::Category("segmentation_platform"),
diff --git a/build/android/gradle/generate_gradle.py b/build/android/gradle/generate_gradle.py
index 5a30023..5743700b 100755
--- a/build/android/gradle/generate_gradle.py
+++ b/build/android/gradle/generate_gradle.py
@@ -460,6 +460,8 @@
   link_dir = os.path.dirname(link_path)
   relpath = os.path.relpath(target_path, link_dir)
   logging.debug('Creating symlink %s -> %s', link_path, relpath)
+  if not os.path.exists(link_dir):
+    os.makedirs(link_dir)
   os.symlink(relpath, link_path)
 
 
@@ -472,8 +474,6 @@
     symlink_dir = os.path.join(entry_output_dir, _JNI_LIBS_SUBDIR)
     shutil.rmtree(symlink_dir, True)
     abi_dir = os.path.join(symlink_dir, _ARMEABI_SUBDIR)
-    if not os.path.exists(abi_dir):
-      os.makedirs(abi_dir)
     for so_file in so_files:
       target_path = os.path.join(output_dir, so_file)
       symlinked_path = os.path.join(abi_dir, so_file)
diff --git a/build/android/gyp/check_for_missing_direct_deps.py b/build/android/gyp/check_for_missing_direct_deps.py
index 2d817e4..8bbf53b 100755
--- a/build/android/gyp/check_for_missing_direct_deps.py
+++ b/build/android/gyp/check_for_missing_direct_deps.py
@@ -298,7 +298,7 @@
       auto_add_deps=args.auto_add_deps)
   logging.info('Check completed.')
 
-  build_utils.Touch(args.stamp)
+  server_utils.MaybeTouch(args.stamp)
 
 
 if __name__ == '__main__':
diff --git a/build/android/gyp/lint.py b/build/android/gyp/lint.py
index 973bacc..161c3c1 100755
--- a/build/android/gyp/lint.py
+++ b/build/android/gyp/lint.py
@@ -546,7 +546,7 @@
            args.create_cache,
            warnings_as_errors=args.warnings_as_errors)
   logging.info('Creating stamp file')
-  build_utils.Touch(args.stamp)
+  server_utils.MaybeTouch(args.stamp)
 
 
 if __name__ == '__main__':
diff --git a/build/android/gyp/proguard.py b/build/android/gyp/proguard.py
index eca9fb9..c780560 100755
--- a/build/android/gyp/proguard.py
+++ b/build/android/gyp/proguard.py
@@ -60,7 +60,7 @@
     # We enforce that this class is removed via -checkdiscard.
     r'FastServiceLoader\.class:.*Could not inline ServiceLoader\.load',
     # Happens on internal builds. It's a real failure, but happens in dead code.
-    r'(?:GeneratedExtensionRegistryLoader|ExtensionRegistryLite)\.class:.*Could not inline ServiceLoader\.load',   # pylint: disable=line-too-long
+    r'(?:GeneratedExtensionRegistryLoader|ExtensionRegistryLite)\.class:.*Could not inline ServiceLoader\.load',  # pylint: disable=line-too-long
     # This class is referenced by kotlinx-coroutines-core-jvm but it does not
     # depend on it. Not actually needed though.
     r'Missing class org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement',
@@ -597,7 +597,9 @@
 
   def format_config_contents(path, contents):
     formatted_contents = []
-    if not contents.strip():
+    # Ignore files that contain only comments (androidx has a lot of these).
+    if all(l.isspace() or l.rstrip().startswith('#')
+           for l in contents.splitlines()):
       return []
 
     # Fix up line endings (third_party configs can have windows endings).
diff --git a/build/android/gyp/util/server_utils.py b/build/android/gyp/util/server_utils.py
index bc8a96b8..3df3853 100644
--- a/build/android/gyp/util/server_utils.py
+++ b/build/android/gyp/util/server_utils.py
@@ -9,9 +9,7 @@
 import socket
 import platform
 import sys
-import subprocess
 import struct
-import time
 
 sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..'))
 from util import build_utils
@@ -79,10 +77,20 @@
   # Siso needs the stamp file to be created in order for the build step to
   # complete. If the task fails when the build server runs it, the build server
   # will delete the stamp file so that it will be run again next build.
-  pathlib.Path(stamp_file).touch()
+  build_utils.Touch(stamp_file)
   return True
 
 
+def MaybeTouch(stamp_file):
+  """Touch |stamp_file| if we are not running under the build_server."""
+  # If we are running under the build server, the stamp file has already been
+  # touched when the task was created. If we touch it again, siso will consider
+  # the target dirty.
+  if BUILD_SERVER_ENV_VARIABLE in os.environ:
+    return
+  build_utils.Touch(stamp_file)
+
+
 def SendMessage(sock: socket.socket, message: bytes):
   size_prefix = struct.pack('!i', len(message))
   sock.sendall(size_prefix + message)
diff --git a/cc/BUILD.gn b/cc/BUILD.gn
index c276f8e..1a276b2 100644
--- a/cc/BUILD.gn
+++ b/cc/BUILD.gn
@@ -271,8 +271,6 @@
     "resources/resource_pool.h",
     "resources/scoped_ui_resource.cc",
     "resources/scoped_ui_resource.h",
-    "resources/shared_bitmap_id_registrar.cc",
-    "resources/shared_bitmap_id_registrar.h",
     "resources/ui_resource_bitmap.cc",
     "resources/ui_resource_bitmap.h",
     "resources/ui_resource_client.h",
diff --git a/cc/layers/layer.cc b/cc/layers/layer.cc
index f51e549..cd6c1bb 100644
--- a/cc/layers/layer.cc
+++ b/cc/layers/layer.cc
@@ -50,16 +50,18 @@
   SameSizeAsLayer();
   ~SameSizeAsLayer() override;
 
-  void* pointers[4];
+  raw_ptr<void> raw_pointers[2];
+  std::unique_ptr<void> unique_pointers[2];
 
   struct {
     LayerList children;
     gfx::Size bounds;
-    unsigned bitfields;
+    HitTestOpaqueness hit_test_opaqueness;
+    bool bitfields;
     SkColor4f background_color;
     TouchActionRegion touch_action_region;
     ElementId element_id;
-    raw_ptr<void> rare_inputs;
+    std::unique_ptr<void> rare_inputs;
   } inputs;
   gfx::Rect update_rect;
   int int_fields[7];
diff --git a/cc/layers/texture_layer.cc b/cc/layers/texture_layer.cc
index 82127f76..5b237ac4 100644
--- a/cc/layers/texture_layer.cc
+++ b/cc/layers/texture_layer.cc
@@ -131,18 +131,10 @@
   // If we're removed from the tree, the TextureLayerImpl will be destroyed, and
   // we will need to set the mailbox again on a new TextureLayerImpl the next
   // time we push.
-  if (!host && resource_holder_.Read(*this))
+  if (!host && resource_holder_.Read(*this)) {
     needs_set_resource_.Write(*this) = true;
-  if (host) {
-    // When attached to a new LayerTreeHost, all previously registered
-    // SharedBitmapIds will need to be re-sent to the new TextureLayerImpl
-    // representing this layer on the compositor thread.
-    auto& registered_bitmaps = registered_bitmaps_.Write(*this);
-    to_register_bitmaps_.Write(*this).insert(
-        std::make_move_iterator(registered_bitmaps.begin()),
-        std::make_move_iterator(registered_bitmaps.end()));
-    registered_bitmaps.clear();
   }
+
   Layer::SetLayerTreeHost(host);
 }
 
@@ -234,57 +226,9 @@
                                              std::move(release_callback));
       needs_set_resource_.Write(*this) = false;
     }
-    auto& to_register_bitmaps = to_register_bitmaps_.Write(*this);
-    for (auto& pair : to_register_bitmaps) {
-      texture_layer->RegisterSharedBitmapId(pair.first, pair.second);
-    }
-    // Store the registered SharedBitmapIds in case we get a new
-    // TextureLayerImpl, in a new tree, to re-send them to.
-    registered_bitmaps_.Write(*this).insert(
-        std::make_move_iterator(to_register_bitmaps.begin()),
-        std::make_move_iterator(to_register_bitmaps.end()));
-    to_register_bitmaps.clear();
-    auto& to_unregister_bitmap_ids = to_unregister_bitmap_ids_.Write(*this);
-    for (const auto& id : to_unregister_bitmap_ids) {
-      texture_layer->UnregisterSharedBitmapId(id);
-    }
-    to_unregister_bitmap_ids.clear();
   }
 }
 
-SharedBitmapIdRegistration TextureLayer::RegisterSharedBitmapId(
-    const viz::SharedBitmapId& id,
-    scoped_refptr<CrossThreadSharedBitmap> bitmap) {
-  DCHECK(!base::Contains(to_register_bitmaps_.Read(*this), id));
-  DCHECK(!base::Contains(registered_bitmaps_.Read(*this), id));
-  to_register_bitmaps_.Write(*this)[id] = std::move(bitmap);
-  std::erase(to_unregister_bitmap_ids_.Write(*this), id);
-
-  // This does not SetNeedsCommit() to be as lazy as possible.
-  // Notifying a SharedBitmapId is not needed until it is used,
-  // and using it will require a commit, so we can wait for that commit
-  // before forwarding the notification instead of forcing it to happen
-  // as a side effect of this method.
-  SetNeedsPushProperties();
-  return SharedBitmapIdRegistration(weak_ptr_factory_.GetMutableWeakPtr(), id);
-}
-
-void TextureLayer::UnregisterSharedBitmapId(viz::SharedBitmapId id) {
-  // If we didn't get to sending the registration to the compositor thread yet,
-  // just remove it.
-  to_register_bitmaps_.Write(*this).erase(id);
-  // Since we also track all previously sent registrations, we must remove that
-  // to in order to prevent re-registering on another LayerTreeHost.
-  registered_bitmaps_.Write(*this).erase(id);
-
-  to_unregister_bitmap_ids_.Write(*this).push_back(id);
-  // Unregistering a SharedBitmapId needs to happen eventually to prevent
-  // leaking the SharedMemory in the display compositor. But this attempts to be
-  // lazy and not force a commit prematurely, so just requests a
-  // PushPropertiesTo() without requesting a commit.
-  SetNeedsPushProperties();
-}
-
 TextureLayer::TransferableResourceHolder::TransferableResourceHolder(
     const viz::TransferableResource& resource,
     viz::ReleaseCallback release_callback)
diff --git a/cc/layers/texture_layer.h b/cc/layers/texture_layer.h
index 685665c..fff9948 100644
--- a/cc/layers/texture_layer.h
+++ b/cc/layers/texture_layer.h
@@ -19,7 +19,6 @@
 #include "cc/cc_export.h"
 #include "cc/layers/layer.h"
 #include "cc/resources/cross_thread_shared_bitmap.h"
-#include "cc/resources/shared_bitmap_id_registrar.h"
 #include "components/viz/common/resources/release_callback.h"
 #include "components/viz/common/resources/transferable_resource.h"
 #include "ui/gfx/hdr_metadata.h"
@@ -36,7 +35,7 @@
 // to display gpu or software resources, depending if the compositor is working
 // in gpu or software compositing mode (the resources must match the compositing
 // mode).
-class CC_EXPORT TextureLayer : public Layer, SharedBitmapIdRegistrar {
+class CC_EXPORT TextureLayer : public Layer {
  public:
   class CC_EXPORT TransferableResourceHolder
       : public base::RefCountedThreadSafe<TransferableResourceHolder> {
@@ -123,17 +122,6 @@
   bool Update() override;
   bool IsSnappedToPixelGridInTarget() const override;
 
-  // Request a mapping from SharedBitmapId to SharedMemory be registered via the
-  // LayerTreeFrameSink with the display compositor. Once this mapping is
-  // registered, the SharedBitmapId can be used in TransferableResources given
-  // to the TextureLayer for display. The SharedBitmapId registration will end
-  // when the returned SharedBitmapIdRegistration object is destroyed.
-  // Implemented as a SharedBitmapIdRegistrar interface so that clients can
-  // have a limited API access.
-  SharedBitmapIdRegistration RegisterSharedBitmapId(
-      const viz::SharedBitmapId& id,
-      scoped_refptr<CrossThreadSharedBitmap> bitmap) override;
-
   const viz::TransferableResource current_transferable_resource() const {
     if (const auto& resource_holder = resource_holder_.Read(*this))
       return resource_holder->resource();
@@ -160,12 +148,6 @@
       viz::ReleaseCallback release_callback,
       bool requires_commit);
 
-  // Friends to give access to UnregisterSharedBitmapId().
-  friend SharedBitmapIdRegistration;
-  // Remove a mapping from SharedBitmapId to SharedMemory in the display
-  // compositor.
-  void UnregisterSharedBitmapId(viz::SharedBitmapId id);
-
   // Dangling on `mac-rel` in `blink_web_tests`:
   // `fast/events/touch/touch-handler-iframe-plugin-assert.html`
   ProtectedSequenceForbidden<raw_ptr<TextureLayerClient, DanglingUntriaged>>
@@ -186,21 +168,6 @@
                          scoped_refptr<CrossThreadSharedBitmap>>
       BitMapMap;
 
-  // The set of SharedBitmapIds to register with the LayerTreeFrameSink on the
-  // compositor thread. These requests are forwarded to the TextureLayerImpl to
-  // use, then stored in |registered_bitmaps_| to re-send if the
-  // TextureLayerImpl object attached to this layer changes, by moving out of
-  // the LayerTreeHost.
-  ProtectedSequenceWritable<BitMapMap> to_register_bitmaps_;
-  // The set of previously registered SharedBitmapIds for the current
-  // LayerTreeHost. If the LayerTreeHost changes, these must be re-sent to the
-  // (new) TextureLayerImpl to be re-registered.
-  ProtectedSequenceWritable<BitMapMap> registered_bitmaps_;
-  // The SharedBitmapIds to unregister on the compositor thread, passed to the
-  // TextureLayerImpl.
-  ProtectedSequenceWritable<std::vector<viz::SharedBitmapId>>
-      to_unregister_bitmap_ids_;
-
   const base::WeakPtrFactory<TextureLayer> weak_ptr_factory_{this};
 };
 
diff --git a/cc/layers/texture_layer_impl.cc b/cc/layers/texture_layer_impl.cc
index 3c7581f..fde65334 100644
--- a/cc/layers/texture_layer_impl.cc
+++ b/cc/layers/texture_layer_impl.cc
@@ -32,15 +32,6 @@
 
 TextureLayerImpl::~TextureLayerImpl() {
   FreeTransferableResource();
-
-  LayerTreeFrameSink* sink = layer_tree_impl()->layer_tree_frame_sink();
-  // The LayerTreeFrameSink may be gone, in which case there's no need to
-  // unregister anything.
-  if (sink) {
-    for (const auto& pair : registered_bitmaps_) {
-      sink->DidDeleteSharedBitmap(pair.first);
-    }
-  }
 }
 
 mojom::LayerType TextureLayerImpl::GetLayerType() const {
@@ -70,12 +61,6 @@
                                            std::move(release_callback_));
     own_resource_ = false;
   }
-  for (auto& pair : to_register_bitmaps_)
-    texture_layer->RegisterSharedBitmapId(pair.first, std::move(pair.second));
-  to_register_bitmaps_.clear();
-  for (const auto& id : to_unregister_bitmap_ids_)
-    texture_layer->UnregisterSharedBitmapId(id);
-  to_unregister_bitmap_ids_.clear();
 }
 
 bool TextureLayerImpl::WillDraw(
@@ -132,18 +117,6 @@
                                    AppendQuadsData* append_quads_data) {
   DCHECK(resource_id_);
 
-  LayerTreeFrameSink* sink = layer_tree_impl()->layer_tree_frame_sink();
-  for (const auto& pair : to_register_bitmaps_) {
-    sink->DidAllocateSharedBitmap(pair.second->shared_region().Duplicate(),
-                                  pair.first);
-  }
-  // All |to_register_bitmaps_| have been registered above, so we can move them
-  // all to the |registered_bitmaps_|.
-  registered_bitmaps_.insert(
-      std::make_move_iterator(to_register_bitmaps_.begin()),
-      std::make_move_iterator(to_register_bitmaps_.end()));
-  to_register_bitmaps_.clear();
-
   SkColor4f bg_color =
       blend_background_color_ ? background_color() : SkColors::kTransparent;
 
@@ -209,19 +182,6 @@
   // resources are still valid, and we can keep them here in that case.
   if (!transferable_resource_.is_software)
     FreeTransferableResource();
-
-  // The LayerTreeFrameSink is gone and being replaced, so we will have to
-  // re-register all SharedBitmapIds on the new LayerTreeFrameSink. We don't
-  // need to do that until the SharedBitmapIds will be used, in AppendQuads(),
-  // but we mark them all as to be registered here.
-  to_register_bitmaps_.insert(
-      std::make_move_iterator(registered_bitmaps_.begin()),
-      std::make_move_iterator(registered_bitmaps_.end()));
-  registered_bitmaps_.clear();
-  // The |to_unregister_bitmap_ids_| are kept since the active layer will re-
-  // register its SharedBitmapIds with a new LayerTreeFrameSink in the future,
-  // so we must remember that we want to unregister it (or avoid registering at
-  // all) instead.
 }
 
 gfx::ContentColorUsage TextureLayerImpl::GetContentColorUsage() const {
@@ -261,38 +221,6 @@
   own_resource_ = true;
 }
 
-void TextureLayerImpl::RegisterSharedBitmapId(
-    viz::SharedBitmapId id,
-    scoped_refptr<CrossThreadSharedBitmap> bitmap) {
-  // If a TextureLayer leaves and rejoins a tree without the TextureLayerImpl
-  // being destroyed, then it will re-request registration of ids that are still
-  // registered on the impl side, so we can just ignore these requests.
-  if (!base::Contains(registered_bitmaps_, id)) {
-    // If this is a pending layer, these will be moved to the active layer
-    // when we PushPropertiesTo(). Otherwise, we don't need to notify these to
-    // the LayerTreeFrameSink until we're going to use them, so defer it until
-    // AppendQuads().
-    to_register_bitmaps_[id] = std::move(bitmap);
-  }
-  std::erase(to_unregister_bitmap_ids_, id);
-}
-
-void TextureLayerImpl::UnregisterSharedBitmapId(viz::SharedBitmapId id) {
-  if (IsActive()) {
-    LayerTreeFrameSink* sink = layer_tree_impl()->layer_tree_frame_sink();
-    if (sink && base::Contains(registered_bitmaps_, id)) {
-      sink->DidDeleteSharedBitmap(id);
-    }
-    to_register_bitmaps_.erase(id);
-    registered_bitmaps_.erase(id);
-  } else {
-    // The active layer will unregister. We do this because it may be using the
-    // SharedBitmapId, so we should remove the SharedBitmapId only after we've
-    // had a chance to replace it with activation.
-    to_unregister_bitmap_ids_.push_back(id);
-  }
-}
-
 void TextureLayerImpl::FreeTransferableResource() {
   if (own_resource_) {
     DCHECK(!resource_id_);
diff --git a/cc/layers/texture_layer_impl.h b/cc/layers/texture_layer_impl.h
index 2d32b1a5..3a9633a 100644
--- a/cc/layers/texture_layer_impl.h
+++ b/cc/layers/texture_layer_impl.h
@@ -63,19 +63,6 @@
                                viz::ReleaseCallback release_callback);
   bool NeedSetTransferableResource() const;
 
-  // These methods notify the display compositor, through the
-  // CompositorFrameSink, of the existence of a SharedBitmapId and its
-  // mapping to a SharedMemory in |bitmap|. Then this SharedBitmapId can be used
-  // in TransferableResources inserted on the layer while it is registered. If
-  // the layer is destroyed, the SharedBitmapId will be unregistered
-  // automatically, and if the CompositorFrameSink is replaced, it will be
-  // re-registered on the new one. The SharedMemory must be kept alive while it
-  // is registered.
-  // If this is a pending layer, the registration is deferred to the active
-  // layer.
-  void RegisterSharedBitmapId(viz::SharedBitmapId id,
-                              scoped_refptr<CrossThreadSharedBitmap> bitmap);
-  void UnregisterSharedBitmapId(viz::SharedBitmapId id);
   void SetInInvisibleLayerTree() override;
   // Whether the resource may be evicted in background. If it returns true, main
   // is responsible for making sure that the resource is imported again after a
@@ -107,26 +94,6 @@
   // TransferableResource given to it.
   viz::ResourceId resource_id_ = viz::kInvalidResourceId;
   viz::ReleaseCallback release_callback_;
-
-  // As a pending layer, the set of SharedBitmapIds and the underlying
-  // base::SharedMemory that must be notified to the display compositor through
-  // the LayerTreeFrameSink. These will be passed to the active layer. As an
-  // active layer, the set of SharedBitmapIds that need to be registered but
-  // have not been yet, since it is done lazily.
-  base::flat_map<viz::SharedBitmapId, scoped_refptr<CrossThreadSharedBitmap>>
-      to_register_bitmaps_;
-
-  // For active layers only. The set of SharedBitmapIds and ownership of the
-  // underlying base::SharedMemory that have been notified to the display
-  // compositor through the LayerTreeFrameSink. These will need to be
-  // re-registered if the LayerTreeFrameSink changes (ie ReleaseResources()
-  // occurs).
-  base::flat_map<viz::SharedBitmapId, scoped_refptr<CrossThreadSharedBitmap>>
-      registered_bitmaps_;
-
-  // As a pending layer, the set of SharedBitmapIds that the active layer should
-  // unregister.
-  std::vector<viz::SharedBitmapId> to_unregister_bitmap_ids_;
 };
 
 }  // namespace cc
diff --git a/cc/metrics/compositor_frame_reporting_controller.cc b/cc/metrics/compositor_frame_reporting_controller.cc
index 2399afe..5dc01d1 100644
--- a/cc/metrics/compositor_frame_reporting_controller.cc
+++ b/cc/metrics/compositor_frame_reporting_controller.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "cc/metrics/compositor_frame_reporting_controller.h"
 
 #include <utility>
diff --git a/cc/metrics/compositor_frame_reporting_controller.h b/cc/metrics/compositor_frame_reporting_controller.h
index a41d62b..d3b3b89 100644
--- a/cc/metrics/compositor_frame_reporting_controller.h
+++ b/cc/metrics/compositor_frame_reporting_controller.h
@@ -104,7 +104,11 @@
     tick_clock_ = tick_clock;
   }
 
-  std::unique_ptr<CompositorFrameReporter>* reporters() { return reporters_; }
+  std::array<std::unique_ptr<CompositorFrameReporter>,
+             PipelineStage::kNumPipelineStages>&
+  ReportersForTesting() {
+    return reporters_;
+  }
 
   void SetDroppedFrameCounter(DroppedFrameCounter* counter);
 
@@ -225,8 +229,9 @@
       scroll_jank_dropped_frame_tracker_;
   std::unique_ptr<ScrollJankUkmReporter> scroll_jank_ukm_reporter_;
 
-  std::unique_ptr<CompositorFrameReporter>
-      reporters_[PipelineStage::kNumPipelineStages];
+  std::array<std::unique_ptr<CompositorFrameReporter>,
+             PipelineStage::kNumPipelineStages>
+      reporters_;
 
   // Mapping of frame token to pipeline reporter for submitted compositor
   // frames.
diff --git a/cc/metrics/compositor_frame_reporting_controller_unittest.cc b/cc/metrics/compositor_frame_reporting_controller_unittest.cc
index 8d9ef5c2..5b6fc372 100644
--- a/cc/metrics/compositor_frame_reporting_controller_unittest.cc
+++ b/cc/metrics/compositor_frame_reporting_controller_unittest.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "cc/metrics/compositor_frame_reporting_controller.h"
 
 #include <string>
@@ -55,15 +50,16 @@
   int ActiveReporters() {
     int count = 0;
     for (int i = 0; i < PipelineStage::kNumPipelineStages; ++i) {
-      if (reporters()[i])
+      if (ReportersForTesting()[i]) {
         ++count;
+      }
     }
     return count;
   }
 
   void ResetReporters() {
     for (int i = 0; i < PipelineStage::kNumPipelineStages; ++i) {
-      reporters()[i] = nullptr;
+      ReportersForTesting()[i] = nullptr;
     }
   }
 
@@ -76,7 +72,7 @@
         PipelineStage::kActivate,
     };
     for (auto stage : kStages) {
-      auto& reporter = reporters()[stage];
+      auto& reporter = ReportersForTesting()[stage];
       if (reporter &&
           reporter->partial_update_dependents_size_for_testing() > 0) {
         ++count;
@@ -94,7 +90,7 @@
         PipelineStage::kActivate,
     };
     for (auto stage : kStages) {
-      auto& reporter = reporters()[stage];
+      auto& reporter = ReportersForTesting()[stage];
       if (reporter)
         count += reporter->partial_update_dependents_size_for_testing();
     }
@@ -110,7 +106,7 @@
         PipelineStage::kActivate,
     };
     for (auto stage : kStages) {
-      auto& reporter = reporters()[stage];
+      auto& reporter = ReportersForTesting()[stage];
       if (reporter)
         count += reporter->owned_partial_update_dependents_size_for_testing();
     }
@@ -137,12 +133,14 @@
   }
 
   void SimulateBeginMainFrame() {
-    if (!reporting_controller_.reporters()[CompositorFrameReportingController::
-                                               PipelineStage::kBeginImplFrame])
+    if (!reporting_controller_
+             .ReportersForTesting()[CompositorFrameReportingController::
+                                        PipelineStage::kBeginImplFrame]) {
       SimulateBeginImplFrame();
-    CHECK(
-        reporting_controller_.reporters()[CompositorFrameReportingController::
-                                              PipelineStage::kBeginImplFrame]);
+    }
+    CHECK(reporting_controller_
+              .ReportersForTesting()[CompositorFrameReportingController::
+                                         PipelineStage::kBeginImplFrame]);
     begin_main_time_ = AdvanceNowByMs(10);
     reporting_controller_.WillBeginMainFrame(args_);
     begin_main_start_time_ = AdvanceNowByMs(10);
@@ -150,13 +148,13 @@
 
   void SimulateCommit(std::unique_ptr<BeginMainFrameMetrics> blink_breakdown) {
     if (!reporting_controller_
-             .reporters()[CompositorFrameReportingController::PipelineStage::
-                              kBeginMainFrame]) {
+             .ReportersForTesting()[CompositorFrameReportingController::
+                                        PipelineStage::kBeginMainFrame]) {
       SimulateBeginMainFrame();
     }
-    CHECK(
-        reporting_controller_.reporters()[CompositorFrameReportingController::
-                                              PipelineStage::kBeginMainFrame]);
+    CHECK(reporting_controller_
+              .ReportersForTesting()[CompositorFrameReportingController::
+                                         PipelineStage::kBeginMainFrame]);
     reporting_controller_.BeginMainFrameStarted(begin_main_start_time_);
     reporting_controller_.NotifyReadyToCommit(std::move(blink_breakdown));
     begin_commit_time_ = AdvanceNowByMs(10);
@@ -166,10 +164,11 @@
   }
 
   void SimulateActivate() {
-    if (!reporting_controller_.reporters()
-             [CompositorFrameReportingController::PipelineStage::kCommit])
+    if (!reporting_controller_.ReportersForTesting()
+             [CompositorFrameReportingController::PipelineStage::kCommit]) {
       SimulateCommit(nullptr);
-    CHECK(reporting_controller_.reporters()
+    }
+    CHECK(reporting_controller_.ReportersForTesting()
               [CompositorFrameReportingController::PipelineStage::kCommit]);
     begin_activation_time_ = AdvanceNowByMs(10);
     reporting_controller_.WillActivate();
@@ -179,10 +178,11 @@
   }
 
   void SimulateSubmitCompositorFrame(EventMetricsSet events_metrics) {
-    if (!reporting_controller_.reporters()
-             [CompositorFrameReportingController::PipelineStage::kActivate])
+    if (!reporting_controller_.ReportersForTesting()
+             [CompositorFrameReportingController::PipelineStage::kActivate]) {
       SimulateActivate();
-    CHECK(reporting_controller_.reporters()
+    }
+    CHECK(reporting_controller_.ReportersForTesting()
               [CompositorFrameReportingController::PipelineStage::kActivate]);
     submit_time_ = AdvanceNowByMs(10);
     ++current_token_;
@@ -2774,6 +2774,5 @@
                                   std::vector<std::string>{"32", "1"}));
 }
 
-
 }  // namespace
 }  // namespace cc
diff --git a/cc/metrics/frame_sequence_metrics.cc b/cc/metrics/frame_sequence_metrics.cc
index e960accf..443b386 100644
--- a/cc/metrics/frame_sequence_metrics.cc
+++ b/cc/metrics/frame_sequence_metrics.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "cc/metrics/frame_sequence_metrics.h"
 
 #include <array>
@@ -564,7 +559,7 @@
   // Use different names, because otherwise the trace-viewer shows the slices in
   // the same color, and that makes it difficult to tell the traces apart from
   // each other.
-  auto trace_names =
+  static constexpr auto trace_names =
       std::to_array<const char*>({"Frame", "Frame ", "Frame   "});
   TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
       "cc,benchmark", trace_names[++this->frame_count % 3],
diff --git a/cc/resources/shared_bitmap_id_registrar.cc b/cc/resources/shared_bitmap_id_registrar.cc
deleted file mode 100644
index e6f43d2..0000000
--- a/cc/resources/shared_bitmap_id_registrar.cc
+++ /dev/null
@@ -1,37 +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.
-
-#include "cc/resources/shared_bitmap_id_registrar.h"
-
-#include <utility>
-
-#include "cc/layers/texture_layer.h"
-
-namespace cc {
-
-SharedBitmapIdRegistration::SharedBitmapIdRegistration() = default;
-
-SharedBitmapIdRegistration::SharedBitmapIdRegistration(
-    base::WeakPtr<TextureLayer> layer_ptr,
-    const viz::SharedBitmapId& id)
-    : layer_ptr_(std::move(layer_ptr)), id_(id) {}
-
-SharedBitmapIdRegistration::~SharedBitmapIdRegistration() {
-  if (layer_ptr_)
-    layer_ptr_->UnregisterSharedBitmapId(id_);
-}
-
-SharedBitmapIdRegistration::SharedBitmapIdRegistration(
-    SharedBitmapIdRegistration&&) noexcept = default;
-
-SharedBitmapIdRegistration& SharedBitmapIdRegistration::operator=(
-    SharedBitmapIdRegistration&& other) noexcept {
-  if (layer_ptr_)
-    layer_ptr_->UnregisterSharedBitmapId(id_);
-  layer_ptr_ = std::move(other.layer_ptr_);
-  id_ = std::move(other.id_);
-  return *this;
-}
-
-}  // namespace cc
diff --git a/cc/resources/shared_bitmap_id_registrar.h b/cc/resources/shared_bitmap_id_registrar.h
deleted file mode 100644
index b925bae7..0000000
--- a/cc/resources/shared_bitmap_id_registrar.h
+++ /dev/null
@@ -1,73 +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.
-
-#ifndef CC_RESOURCES_SHARED_BITMAP_ID_REGISTRAR_H_
-#define CC_RESOURCES_SHARED_BITMAP_ID_REGISTRAR_H_
-
-#include "base/memory/ref_counted.h"
-#include "base/memory/weak_ptr.h"
-#include "cc/cc_export.h"
-#include "components/viz/common/resources/shared_bitmap.h"
-
-namespace cc {
-class CrossThreadSharedBitmap;
-class SharedBitmapIdRegistration;
-class TextureLayer;
-
-// An interface exposed to clients of TextureLayer for registering
-// SharedBitmapIds that they will be using in viz::TransferableResources given
-// to the TextureLayer. SharedBitmapId-SharedMemory pairs registered as such are
-// then given to the display compositor, and the mapping between the pair is
-// kept valid while the returned SharedBitmapIdRegistration is kept alive.
-//
-// These mappings are per-layer-tree. So if a client has multiple TextureLayers
-// in the same tree, and wants to use the SharedBitmapId in more than one of
-// them over time, it still only should register with a single TextureLayer. But
-// if the TextureLayer is removed from the tree, they would need to be
-// registered with another TextureLayer that is in each tree where they are
-// being used.
-class CC_EXPORT SharedBitmapIdRegistrar {
- public:
-  virtual ~SharedBitmapIdRegistrar() = default;
-  virtual SharedBitmapIdRegistration RegisterSharedBitmapId(
-      const viz::SharedBitmapId& id,
-      scoped_refptr<CrossThreadSharedBitmap> bitmap) = 0;
-};
-
-// A scoped object that maintains a mapping of SharedBitmapId to SharedMemory
-// that was registered through SharedBitmapIdRegistrar for the display
-// compositor. Keep this object alive while the SharedBitmapId may be used
-// in viz::TransferableResources given to the TextureLayer. Typically that means
-// as long as the client keeps the SharedMemory alive with a reference to the
-// CrossThreadSharedBitmap, which it should keep alive at least until the
-// TextureLayer calls back to the ReleaseCallback indicating the display
-// compositor is no longer using the resource. When this object is destroyed, or
-// assigned to, then the mapping registration will be dropped from the display
-// compositor, and the SharedBitmapId will no longer be able to be used in the
-// TextureLayer.
-class CC_EXPORT SharedBitmapIdRegistration {
- public:
-  SharedBitmapIdRegistration();
-  SharedBitmapIdRegistration(const SharedBitmapIdRegistration&) = delete;
-  SharedBitmapIdRegistration(SharedBitmapIdRegistration&&) noexcept;
-  ~SharedBitmapIdRegistration();
-
-  SharedBitmapIdRegistration& operator=(const SharedBitmapIdRegistration&) =
-      delete;
-  SharedBitmapIdRegistration& operator=(SharedBitmapIdRegistration&&) noexcept;
-
- private:
-  // Constructed by TextureLayer only, then held by the client as long
-  // as they wish to
-  friend TextureLayer;
-  SharedBitmapIdRegistration(base::WeakPtr<TextureLayer> layer_ptr,
-                             const viz::SharedBitmapId& id);
-
-  base::WeakPtr<TextureLayer> layer_ptr_;
-  viz::SharedBitmapId id_;
-};
-
-}  // namespace cc
-
-#endif  // CC_RESOURCES_SHARED_BITMAP_ID_REGISTRAR_H_
diff --git a/cc/trees/property_tree.cc b/cc/trees/property_tree.cc
index aee8ccd6..732648c 100644
--- a/cc/trees/property_tree.cc
+++ b/cc/trees/property_tree.cc
@@ -34,12 +34,6 @@
 
 namespace cc {
 
-void AnimationUpdateOnMissingPropertyNodeUMALog(bool missing_property_node) {
-  UMA_HISTOGRAM_BOOLEAN(
-      "Compositing.Renderer.AnimationUpdateOnMissingPropertyNode",
-      missing_property_node);
-}
-
 AnchorPositionScrollData::AnchorPositionScrollData() = default;
 AnchorPositionScrollData::~AnchorPositionScrollData() = default;
 AnchorPositionScrollData::AnchorPositionScrollData(
@@ -189,10 +183,8 @@
   // TODO(crbug.com/40828469): Remove this when we no longer animate
   // non-existent nodes.
   if (!node) {
-    AnimationUpdateOnMissingPropertyNodeUMALog(true);
     return false;
   }
-  AnimationUpdateOnMissingPropertyNodeUMALog(false);
   if (node->local == transform)
     return false;
   node->local = transform;
@@ -1090,10 +1082,8 @@
   // TODO(crbug.com/40828469): Remove this when we no longer animate
   // non-existent nodes.
   if (!node) {
-    AnimationUpdateOnMissingPropertyNodeUMALog(true);
     return false;
   }
-  AnimationUpdateOnMissingPropertyNodeUMALog(false);
   if (node->opacity == opacity)
     return false;
   node->opacity = opacity;
@@ -1109,10 +1099,8 @@
   // TODO(crbug.com/40828469): Remove this when we no longer animate
   // non-existent nodes.
   if (!node) {
-    AnimationUpdateOnMissingPropertyNodeUMALog(true);
     return false;
   }
-  AnimationUpdateOnMissingPropertyNodeUMALog(false);
   if (node->filters == filters)
     return false;
   node->filters = filters;
@@ -1129,10 +1117,8 @@
   // TODO(crbug.com/40828469): Remove this when we no longer animate
   // non-existent nodes.
   if (!node) {
-    AnimationUpdateOnMissingPropertyNodeUMALog(true);
     return false;
   }
-  AnimationUpdateOnMissingPropertyNodeUMALog(false);
   if (node->backdrop_filters == backdrop_filters)
     return false;
   node->backdrop_filters = backdrop_filters;
diff --git a/chrome/android/chrome_junit_test_java_sources.gni b/chrome/android/chrome_junit_test_java_sources.gni
index 5af7a98..4c131d8 100644
--- a/chrome/android/chrome_junit_test_java_sources.gni
+++ b/chrome/android/chrome_junit_test_java_sources.gni
@@ -165,6 +165,7 @@
   "junit/src/org/chromium/chrome/browser/contextualsearch/RelatedSearchesStampTest.java",
   "junit/src/org/chromium/chrome/browser/contextualsearch/SelectionClientManagerTest.java",
   "junit/src/org/chromium/chrome/browser/cookies/CanonicalCookieTest.java",
+  "junit/src/org/chromium/chrome/browser/creator/CreatorActionDelegateImplTest.java",
   "junit/src/org/chromium/chrome/browser/customtabs/AuthTabColorProviderUnitTest.java",
   "junit/src/org/chromium/chrome/browser/customtabs/AuthTabIntentDataProviderUnitTest.java",
   "junit/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinatorUnitTest.java",
diff --git a/chrome/android/expectations/monochrome_64_32_public_bundle.proguard_flags.expected b/chrome/android/expectations/monochrome_64_32_public_bundle.proguard_flags.expected
index e97b4f5..057ddb20 100644
--- a/chrome/android/expectations/monochrome_64_32_public_bundle.proguard_flags.expected
+++ b/chrome/android/expectations/monochrome_64_32_public_bundle.proguard_flags.expected
@@ -921,7 +921,7 @@
 -dontwarn javax.annotation.**
 -dontwarn org.checkerframework.**
 -dontwarn com.google.errorprone.annotations.**
--dontwarn org.jspecify.nullness.NullMarked
+-dontwarn org.jspecify.annotations.NullMarked
 
 # Annotations no longer exist. Suppression prevents ProGuard failures in
 # SDKs which depend on earlier versions of play-services-basement.
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiUtilsUnitTest.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiUtilsUnitTest.java
index ea805215..760f06d 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiUtilsUnitTest.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiUtilsUnitTest.java
@@ -12,7 +12,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import static org.chromium.ui.test.util.MockitoHelper.doCallback;
+import static org.chromium.ui.test.util.MockitoHelper.runWithValue;
 
 import androidx.test.core.app.ApplicationProvider;
 
@@ -179,11 +179,7 @@
 
     @Test
     public void testDeleteSharedTabGroup_Positive() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processDeleteSharedGroupAttempt(any(), any());
 
@@ -207,11 +203,7 @@
 
     @Test
     public void testDeleteSharedTabGroup_Negative() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_NEGATIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_NEGATIVE)
                 .when(mActionConfirmationManager)
                 .processDeleteSharedGroupAttempt(any(), any());
 
@@ -232,11 +224,7 @@
 
     @Test
     public void testDeleteSharedTabGroup_NullTab() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processDeleteSharedGroupAttempt(any(), any());
 
@@ -257,11 +245,7 @@
 
     @Test
     public void testDeleteSharedTabGroup_NullTabGroupId() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processDeleteSharedGroupAttempt(any(), any());
 
@@ -282,11 +266,7 @@
 
     @Test
     public void testDeleteSharedTabGroup_NullSavedTabGroup() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processDeleteSharedGroupAttempt(any(), any());
 
@@ -301,11 +281,7 @@
 
     @Test
     public void testDeleteSharedTabGroup_NullCollaborationId() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processDeleteSharedGroupAttempt(any(), any());
 
@@ -325,11 +301,7 @@
 
     @Test
     public void testLeaveTabGroup_Positive() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processLeaveGroupAttempt(any(), any());
 
@@ -356,11 +328,7 @@
 
     @Test
     public void testLeaveTabGroup_Negative() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_NEGATIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_NEGATIVE)
                 .when(mActionConfirmationManager)
                 .processLeaveGroupAttempt(any(), any());
 
@@ -383,11 +351,7 @@
 
     @Test
     public void testLeaveTabGroup_NullTab() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processLeaveGroupAttempt(any(), any());
 
@@ -410,11 +374,7 @@
 
     @Test
     public void testLeaveTabGroup_NullSavedTabGroup() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processLeaveGroupAttempt(any(), any());
 
@@ -433,11 +393,7 @@
 
     @Test
     public void testLeaveTabGroup_NullCoreAccountInfo() {
-        doCallback(
-                        1,
-                        (Callback<Integer> resultCallback) ->
-                                resultCallback.onResult(
-                                        ActionConfirmationResult.CONFIRMATION_POSITIVE))
+        runWithValue(1, ActionConfirmationResult.CONFIRMATION_POSITIVE)
                 .when(mActionConfirmationManager)
                 .processLeaveGroupAttempt(any(), any());
 
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettings.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettings.java
index 2d04e360..b173951 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettings.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettings.java
@@ -131,7 +131,7 @@
                 (TextMessagePreference)
                         findPreference(PREF_SHARE_TITLES_AND_URLS_WITH_OS_LEARN_MORE);
 
-        if (!AuxiliarySearchControllerFactory.getInstance().isEnabled()) {
+        if (!AuxiliarySearchControllerFactory.getInstance().isEnabledAndDeviceCompatible()) {
             shareTitlesAndUrlsWithOsSwitch.setVisible(false);
             learnMoreTextMessagePreference.setVisible(false);
             return;
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettingsUnitTest.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettingsUnitTest.java
index 3eaaa4e..2780d55 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettingsUnitTest.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabsSettingsUnitTest.java
@@ -45,6 +45,7 @@
 import org.chromium.chrome.browser.auxiliary_search.AuxiliarySearchHooks;
 import org.chromium.chrome.browser.auxiliary_search.AuxiliarySearchUtils;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
 import org.chromium.chrome.browser.preferences.Pref;
 import org.chromium.chrome.browser.profiles.Profile;
@@ -231,11 +232,34 @@
 
     @Test
     @SmallTest
+    public void testLaunchTabsSettingsShareTabs_NotShowWhenDeviceNotCompatible() {
+        AuxiliarySearchHooks hooksMock = Mockito.mock(AuxiliarySearchHooks.class);
+        when(hooksMock.isEnabled()).thenReturn(true);
+        when(hooksMock.isSettingDefaultEnabledByOs()).thenReturn(true);
+        AuxiliarySearchControllerFactory.getInstance().setHooksForTesting(hooksMock);
+        // Sets no consumer schema exists.
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, false);
+
+        TabsSettings tabsSettings = launchFragment();
+        ChromeSwitchPreference shareTitlesAndUrlsWithOsSwitch =
+                tabsSettings.findPreference(TabsSettings.PREF_SHARE_TITLES_AND_URLS_WITH_OS_SWITCH);
+        TextMessagePreference learnMoreTextMessagePreference =
+                tabsSettings.findPreference(
+                        TabsSettings.PREF_SHARE_TITLES_AND_URLS_WITH_OS_LEARN_MORE);
+        assertFalse(shareTitlesAndUrlsWithOsSwitch.isVisible());
+        assertFalse(learnMoreTextMessagePreference.isVisible());
+    }
+
+    @Test
+    @SmallTest
     public void testLaunchTabsSettingsShareTabs() {
         AuxiliarySearchHooks hooksMock = Mockito.mock(AuxiliarySearchHooks.class);
         when(hooksMock.isEnabled()).thenReturn(true);
         when(hooksMock.isSettingDefaultEnabledByOs()).thenReturn(true);
         AuxiliarySearchControllerFactory.getInstance().setHooksForTesting(hooksMock);
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, true);
         assertTrue(AuxiliarySearchControllerFactory.getInstance().isSettingDefaultEnabledByOs());
         assertTrue(AuxiliarySearchUtils.isShareTabsWithOsEnabled());
 
@@ -263,6 +287,8 @@
     public void testLaunchTabsSettingsShareTabs_DefaultDisabled() {
         AuxiliarySearchHooks hooksMock = Mockito.mock(AuxiliarySearchHooks.class);
         when(hooksMock.isEnabled()).thenReturn(true);
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, true);
         // Sets the setting as default disabled.
         when(hooksMock.isSettingDefaultEnabledByOs()).thenReturn(false);
         AuxiliarySearchControllerFactory.getInstance().setHooksForTesting(hooksMock);
@@ -295,6 +321,8 @@
         when(hooksMock.isEnabled()).thenReturn(true);
         when(hooksMock.isSettingDefaultEnabledByOs()).thenReturn(true);
         AuxiliarySearchControllerFactory.getInstance().setHooksForTesting(hooksMock);
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, true);
 
         TabsSettings tabsSettings = launchFragment();
         ChromeSwitchPreference shareTitlesAndUrlsWithOsSwitch =
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActionDelegateImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActionDelegateImpl.java
index ac6801d..86883cf1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActionDelegateImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActionDelegateImpl.java
@@ -15,14 +15,10 @@
 import org.chromium.chrome.browser.bookmarks.BookmarkModel;
 import org.chromium.chrome.browser.bookmarks.BookmarkUtils;
 import org.chromium.chrome.browser.creator.CreatorCoordinator;
-import org.chromium.chrome.browser.device_lock.DeviceLockActivityLauncherImpl;
 import org.chromium.chrome.browser.feed.FeedActionDelegate;
 import org.chromium.chrome.browser.feed.R;
-import org.chromium.chrome.browser.feed.signinbottomsheet.SigninBottomSheetCoordinator;
-import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.signin.SigninAndHistorySyncActivityLauncherImpl;
-import org.chromium.chrome.browser.signin.SyncConsentActivityLauncherImpl;
 import org.chromium.chrome.browser.tab.TabLaunchType;
 import org.chromium.chrome.browser.tabmodel.AsyncTabCreationParams;
 import org.chromium.chrome.browser.tabmodel.document.ChromeAsyncTabLauncher;
@@ -35,7 +31,6 @@
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.signin.metrics.SigninAccessPoint;
 import org.chromium.content_public.browser.LoadUrlParams;
-import org.chromium.ui.base.WindowAndroid;
 import org.chromium.ui.mojom.WindowOpenDisposition;
 import org.chromium.url.GURL;
 
@@ -116,12 +111,6 @@
     }
 
     @Override
-    public void showSyncConsentActivity(int signinAccessPoint) {
-        SyncConsentActivityLauncherImpl.getForProfile(mProfile)
-                .launchActivityForPromoDefaultFlow(mActivity, signinAccessPoint, null);
-    }
-
-    @Override
     public void startSigninFlow(@SigninAccessPoint int signinAccessPoint) {
         AccountPickerBottomSheetStrings strings =
                 new AccountPickerBottomSheetStrings.Builder(
@@ -148,56 +137,30 @@
     @Override
     public void showSignInInterstitial(
             @SigninAccessPoint int signinAccessPoint,
-            BottomSheetController mBottomSheetController,
-            WindowAndroid mWindowAndroid) {
-        if (ChromeFeatureList.isEnabled(
-                ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
-            AccountPickerBottomSheetStrings strings =
-                    new AccountPickerBottomSheetStrings.Builder(
-                                    R.string
-                                            .signin_account_picker_bottom_sheet_title_for_back_of_card_menu_signin)
-                            .setSubtitleStringId(
-                                    R.string
-                                            .signin_account_picker_bottom_sheet_subtitle_for_back_of_card_menu_signin)
-                            .setDismissButtonStringId(R.string.cancel)
-                            .build();
-            BottomSheetSigninAndHistorySyncConfig config =
-                    new BottomSheetSigninAndHistorySyncConfig.Builder(
-                                    strings,
-                                    NoAccountSigninMode.BOTTOM_SHEET,
-                                    WithAccountSigninMode.DEFAULT_ACCOUNT_BOTTOM_SHEET,
-                                    HistorySyncConfig.OptInMode.NONE)
-                            .build();
-            @Nullable
-            Intent intent =
-                    SigninAndHistorySyncActivityLauncherImpl.get()
-                            .createBottomSheetSigninIntentOrShowError(
-                                    mActivity, mProfile, config, signinAccessPoint);
-            if (intent != null) {
-                mActivity.startActivity(intent);
-            }
-            return;
-        }
+            BottomSheetController mBottomSheetController) {
         AccountPickerBottomSheetStrings strings =
                 new AccountPickerBottomSheetStrings.Builder(
                                 R.string
-                                        .signin_account_picker_bottom_sheet_title_for_cormorant_signin)
+                                        .signin_account_picker_bottom_sheet_title_for_back_of_card_menu_signin)
                         .setSubtitleStringId(
                                 R.string
-                                        .signin_account_picker_bottom_sheet_subtitle_for_cormorant_signin)
-                        .setDismissButtonStringId(R.string.close)
+                                        .signin_account_picker_bottom_sheet_subtitle_for_back_of_card_menu_signin)
+                        .setDismissButtonStringId(R.string.cancel)
                         .build();
-        SigninBottomSheetCoordinator signinCoordinator =
-                new SigninBottomSheetCoordinator(
-                        mWindowAndroid,
-                        DeviceLockActivityLauncherImpl.get(),
-                        mBottomSheetController,
-                        mProfile,
-                        strings,
-                        () -> {
-                            showSyncConsentActivity(signinAccessPoint);
-                        },
-                        signinAccessPoint);
-        signinCoordinator.show();
+        BottomSheetSigninAndHistorySyncConfig config =
+                new BottomSheetSigninAndHistorySyncConfig.Builder(
+                                strings,
+                                NoAccountSigninMode.BOTTOM_SHEET,
+                                WithAccountSigninMode.DEFAULT_ACCOUNT_BOTTOM_SHEET,
+                                HistorySyncConfig.OptInMode.NONE)
+                        .build();
+        @Nullable
+        Intent intent =
+                SigninAndHistorySyncActivityLauncherImpl.get()
+                        .createBottomSheetSigninIntentOrShowError(
+                                mActivity, mProfile, config, signinAccessPoint);
+        if (intent != null) {
+            mActivity.startActivity(intent);
+        }
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActivity.java
index 11fd1fa..c52b928b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/creator/CreatorActivity.java
@@ -199,6 +199,6 @@
     // This implements the SignInInterstitialInitiator interface.
     public void showSignInInterstitial() {
         mCreatorActionDelegate.showSignInInterstitial(
-                SigninAccessPoint.CREATOR_FEED_FOLLOW, mBottomSheetController, mWindowAndroid);
+                SigninAccessPoint.CREATOR_FEED_FOLLOW, mBottomSheetController);
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/download/home/DownloadActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/app/download/home/DownloadActivity.java
index 3a29b4a..2c459ad 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/download/home/DownloadActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/download/home/DownloadActivity.java
@@ -100,9 +100,11 @@
 
     @Override
     protected void onDestroy() {
-        mDownloadCoordinator.removeObserver(mUiObserver);
-        mDownloadCoordinator.destroy();
-        mModalDialogManager.destroy();
+        if (mDownloadCoordinator != null) {
+            mDownloadCoordinator.removeObserver(mUiObserver);
+            mDownloadCoordinator.destroy();
+            mModalDialogManager.destroy();
+        }
         super.onDestroy();
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/feed/FeedActionDelegateImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/app/feed/FeedActionDelegateImpl.java
index b38f194..1917a05 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/feed/FeedActionDelegateImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/feed/FeedActionDelegateImpl.java
@@ -14,11 +14,9 @@
 import org.chromium.chrome.browser.app.creator.CreatorActivity;
 import org.chromium.chrome.browser.bookmarks.BookmarkModel;
 import org.chromium.chrome.browser.bookmarks.BookmarkUtils;
-import org.chromium.chrome.browser.device_lock.DeviceLockActivityLauncherImpl;
 import org.chromium.chrome.browser.feed.FeedActionDelegate;
 import org.chromium.chrome.browser.feed.R;
 import org.chromium.chrome.browser.feed.SingleWebFeedEntryPoint;
-import org.chromium.chrome.browser.feed.signinbottomsheet.SigninBottomSheetCoordinator;
 import org.chromium.chrome.browser.feed.webfeed.CreatorIntentConstants;
 import org.chromium.chrome.browser.feed.webfeed.WebFeedBridge;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
@@ -28,8 +26,6 @@
 import org.chromium.chrome.browser.offlinepages.RequestCoordinatorBridge;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.signin.SigninAndHistorySyncActivityLauncherImpl;
-import org.chromium.chrome.browser.signin.SyncConsentActivityLauncherImpl;
-import org.chromium.chrome.browser.signin.services.SigninMetricsUtils;
 import org.chromium.chrome.browser.suggestions.SuggestionsConfig;
 import org.chromium.chrome.browser.tab.EmptyTabObserver;
 import org.chromium.chrome.browser.tab.Tab;
@@ -48,7 +44,6 @@
 import org.chromium.content_public.common.Referrer;
 import org.chromium.net.NetError;
 import org.chromium.ui.base.PageTransition;
-import org.chromium.ui.base.WindowAndroid;
 import org.chromium.ui.mojom.WindowOpenDisposition;
 import org.chromium.url.GURL;
 
@@ -174,14 +169,6 @@
     }
 
     @Override
-    public void showSyncConsentActivity(@SigninAccessPoint int signinAccessPoint) {
-        if (ChromeFeatureList.isEnabled(ChromeFeatureList.FEED_SHOW_SIGN_IN_COMMAND)) {
-            SyncConsentActivityLauncherImpl.getForProfile(mProfile)
-                    .launchActivityIfAllowed(mActivity, signinAccessPoint);
-        }
-    }
-
-    @Override
     public void startSigninFlow(@SigninAccessPoint int signinAccessPoint) {
         if (!ChromeFeatureList.isEnabled(ChromeFeatureList.FEED_SHOW_SIGN_IN_COMMAND)) {
             return;
@@ -209,58 +196,31 @@
 
     @Override
     public void showSignInInterstitial(
-            @SigninAccessPoint int signinAccessPoint,
-            BottomSheetController bottomSheetController,
-            WindowAndroid windowAndroid) {
-        if (ChromeFeatureList.isEnabled(
-                ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
-            AccountPickerBottomSheetStrings bottomSheetStrings =
-                    new AccountPickerBottomSheetStrings.Builder(
-                                    R.string
-                                            .signin_account_picker_bottom_sheet_title_for_back_of_card_menu_signin)
-                            .setSubtitleStringId(
-                                    R.string
-                                            .signin_account_picker_bottom_sheet_subtitle_for_back_of_card_menu_signin)
-                            .setDismissButtonStringId(R.string.cancel)
-                            .build();
-            BottomSheetSigninAndHistorySyncConfig config =
-                    new BottomSheetSigninAndHistorySyncConfig.Builder(
-                                    bottomSheetStrings,
-                                    NoAccountSigninMode.BOTTOM_SHEET,
-                                    WithAccountSigninMode.DEFAULT_ACCOUNT_BOTTOM_SHEET,
-                                    HistorySyncConfig.OptInMode.NONE)
-                            .build();
-            @Nullable
-            Intent intent =
-                    SigninAndHistorySyncActivityLauncherImpl.get()
-                            .createBottomSheetSigninIntentOrShowError(
-                                    mActivity, mProfile, config, signinAccessPoint);
-            if (intent != null) {
-                mActivity.startActivity(intent);
-            }
-            return;
-        }
+            @SigninAccessPoint int signinAccessPoint, BottomSheetController bottomSheetController) {
         AccountPickerBottomSheetStrings bottomSheetStrings =
                 new AccountPickerBottomSheetStrings.Builder(
                                 R.string
                                         .signin_account_picker_bottom_sheet_title_for_back_of_card_menu_signin)
                         .setSubtitleStringId(
                                 R.string
-                                        .signin_account_picker_bottom_sheet_subtitle_for_back_of_card_menu_signin_old)
-                        .setDismissButtonStringId(R.string.close)
+                                        .signin_account_picker_bottom_sheet_subtitle_for_back_of_card_menu_signin)
+                        .setDismissButtonStringId(R.string.cancel)
                         .build();
-        SigninMetricsUtils.logSigninStarted(signinAccessPoint);
-        SigninMetricsUtils.logSigninUserActionForAccessPoint(signinAccessPoint);
-        SigninBottomSheetCoordinator signinCoordinator =
-                new SigninBottomSheetCoordinator(
-                        windowAndroid,
-                        DeviceLockActivityLauncherImpl.get(),
-                        bottomSheetController,
-                        mProfile,
-                        bottomSheetStrings,
-                        null,
-                        signinAccessPoint);
-        signinCoordinator.show();
+        BottomSheetSigninAndHistorySyncConfig config =
+                new BottomSheetSigninAndHistorySyncConfig.Builder(
+                                bottomSheetStrings,
+                                NoAccountSigninMode.BOTTOM_SHEET,
+                                WithAccountSigninMode.DEFAULT_ACCOUNT_BOTTOM_SHEET,
+                                HistorySyncConfig.OptInMode.NONE)
+                        .build();
+        @Nullable
+        Intent intent =
+                SigninAndHistorySyncActivityLauncherImpl.get()
+                        .createBottomSheetSigninIntentOrShowError(
+                                mActivity, mProfile, config, signinAccessPoint);
+        if (intent != null) {
+            mActivity.startActivity(intent);
+        }
     }
 
     /**
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSurveyController.java b/chrome/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSurveyController.java
index ed9dc35b..ba433144 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSurveyController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSurveyController.java
@@ -49,15 +49,28 @@
 
 /** Class that controls and manages when and if surveys should be shown. */
 public class PrivacySandboxSurveyController {
-    // TODO(crbug.com/379930582): Replace this enum with an `@IntDef`.
-    private enum PrivacySandboxSurveyType {
-        UNKNOWN,
-        CCT_EEA_ACCEPTED,
-        CCT_EEA_DECLINED,
-        CCT_EEA_CONTROL,
-        CCT_ROW_ACKNOWLEDGED,
-        CCT_ROW_CONTROL,
-        SENTIMENT_SURVEY,
+    /** List of all the survey types that this controller manages. */
+    @IntDef({
+        PrivacySandboxSurveyType.UNKNOWN,
+        PrivacySandboxSurveyType.CCT_EEA_ACCEPTED,
+        PrivacySandboxSurveyType.CCT_EEA_DECLINED,
+        PrivacySandboxSurveyType.CCT_EEA_CONTROL,
+        PrivacySandboxSurveyType.CCT_ROW_ACKNOWLEDGED,
+        PrivacySandboxSurveyType.CCT_ROW_CONTROL,
+        PrivacySandboxSurveyType.SENTIMENT_SURVEY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PrivacySandboxSurveyType {
+        // Default survey type if we don't surey type not explicitly defined.
+        int UNKNOWN = 0;
+        // Represents the surveys for the Ads CCT notice.
+        int CCT_EEA_ACCEPTED = 1;
+        int CCT_EEA_DECLINED = 2;
+        int CCT_EEA_CONTROL = 3;
+        int CCT_ROW_ACKNOWLEDGED = 4;
+        int CCT_ROW_CONTROL = 5;
+        // Represents the always on sentiment survey.
+        int SENTIMENT_SURVEY = 6;
     }
 
     // LINT.IfChange(PrivacySandboxCctAdsNoticeSurveyFailures)
@@ -96,8 +109,9 @@
 
     // LINT.ThenChange(/tools/metrics/histograms/enums.xml:PrivacySandboxCctAdsNoticeSurveyFailures)
 
-    private static final Map<PrivacySandboxSurveyType, String> sSurveyTriggers =
-            ImmutableMap.<PrivacySandboxSurveyType, String>builder()
+    // Maps {@link PrivacySandboxSurveyType} to their survey triggerid.
+    private static final Map<Integer, String> sSurveyTriggers =
+            ImmutableMap.<Integer, String>builder()
                     .put(
                             PrivacySandboxSurveyType.CCT_EEA_ACCEPTED,
                             "privacy-sandbox-cct-ads-notice-eea-accepted")
@@ -217,7 +231,7 @@
     // Determines the appropriate survey to launch based on the user interaction with either the EEA
     // consent or the ROW notice and launches the survey.
     private void maybeLaunchAdsCctTreatmentSurvey() {
-        PrivacySandboxSurveyType surveyType = PrivacySandboxSurveyType.UNKNOWN;
+        @PrivacySandboxSurveyType int surveyType = PrivacySandboxSurveyType.UNKNOWN;
         PrefService prefs = UserPrefs.get(mProfile);
         // Check if the EEA consent was shown.
         if (prefs.getBoolean(Pref.PRIVACY_SANDBOX_M1_CONSENT_DECISION_MADE)) {
@@ -269,7 +283,7 @@
         }
     }
 
-    private SurveyClient constructSurveyClient(PrivacySandboxSurveyType survey) {
+    private SurveyClient constructSurveyClient(@PrivacySandboxSurveyType int survey) {
         SurveyConfig surveyConfig = SurveyConfig.get(mProfile, sSurveyTriggers.get(survey));
         if (surveyConfig == null) {
             emitInvalidSurveyConfigHistogram(survey);
@@ -298,7 +312,7 @@
                 mActivity.getResources(), mMessage);
     }
 
-    private void showSurvey(PrivacySandboxSurveyType surveyType) {
+    private void showSurvey(@PrivacySandboxSurveyType int surveyType) {
         SurveyClient surveyClient = constructSurveyClient(surveyType);
         if (surveyClient == null) {
             return;
@@ -328,14 +342,14 @@
                 };
     }
 
-    private Map<String, Boolean> populateSurveyPsb(PrivacySandboxSurveyType surveyType) {
+    private Map<String, Boolean> populateSurveyPsb(@PrivacySandboxSurveyType int surveyType) {
         if (surveyType == PrivacySandboxSurveyType.SENTIMENT_SURVEY) {
             return getSentimentSurveyPsb();
         }
         return Collections.emptyMap();
     }
 
-    private Map<String, String> populateSurveyPsd(PrivacySandboxSurveyType surveyType) {
+    private Map<String, String> populateSurveyPsd(@PrivacySandboxSurveyType int surveyType) {
         if (surveyType == PrivacySandboxSurveyType.SENTIMENT_SURVEY) {
             return getSentimentSurveyPsd();
         }
@@ -417,29 +431,29 @@
                 CctAdsNoticeSurveyFailures.INVALID_ROW_CONTROL_SURVEY_CONFIG + 1);
     }
 
-    private static void emitInvalidSurveyConfigHistogram(PrivacySandboxSurveyType surveyType) {
+    private static void emitInvalidSurveyConfigHistogram(@PrivacySandboxSurveyType int surveyType) {
         switch (surveyType) {
-            case SENTIMENT_SURVEY:
+            case PrivacySandboxSurveyType.SENTIMENT_SURVEY:
                 recordSentimentSurveyStatus(
                         PrivacySandboxSentimentSurveyStatus.INVALID_SURVEY_CONFIG);
                 return;
-            case CCT_EEA_ACCEPTED:
+            case PrivacySandboxSurveyType.CCT_EEA_ACCEPTED:
                 recordCctAdsNoticeSurveyFailures(
                         CctAdsNoticeSurveyFailures.INVALID_EEA_ACCEPTED_SURVEY_CONFIG);
                 return;
-            case CCT_EEA_DECLINED:
+            case PrivacySandboxSurveyType.CCT_EEA_DECLINED:
                 recordCctAdsNoticeSurveyFailures(
                         CctAdsNoticeSurveyFailures.INVALID_EEA_DECLINED_SURVEY_CONFIG);
                 return;
-            case CCT_EEA_CONTROL:
+            case PrivacySandboxSurveyType.CCT_EEA_CONTROL:
                 recordCctAdsNoticeSurveyFailures(
                         CctAdsNoticeSurveyFailures.INVALID_EEA_CONTROL_SURVEY_CONFIG);
                 return;
-            case CCT_ROW_ACKNOWLEDGED:
+            case PrivacySandboxSurveyType.CCT_ROW_ACKNOWLEDGED:
                 recordCctAdsNoticeSurveyFailures(
                         CctAdsNoticeSurveyFailures.INVALID_ROW_ACKNOWLEDGED_SURVEY_CONFIG);
                 return;
-            case CCT_ROW_CONTROL:
+            case PrivacySandboxSurveyType.CCT_ROW_CONTROL:
                 recordCctAdsNoticeSurveyFailures(
                         CctAdsNoticeSurveyFailures.INVALID_ROW_CONTROL_SURVEY_CONFIG);
                 return;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/creator/CreatorActionDelegateImplTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/creator/CreatorActionDelegateImplTest.java
new file mode 100644
index 0000000..31132d27
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/creator/CreatorActionDelegateImplTest.java
@@ -0,0 +1,96 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.creator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.app.creator.CreatorActionDelegateImpl;
+import org.chromium.chrome.browser.bookmarks.BookmarkModel;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.signin.SigninAndHistorySyncActivityLauncherImpl;
+import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
+import org.chromium.chrome.browser.ui.signin.BottomSheetSigninAndHistorySyncConfig;
+import org.chromium.chrome.browser.ui.signin.BottomSheetSigninAndHistorySyncConfig.NoAccountSigninMode;
+import org.chromium.chrome.browser.ui.signin.BottomSheetSigninAndHistorySyncConfig.WithAccountSigninMode;
+import org.chromium.chrome.browser.ui.signin.SigninAndHistorySyncActivityLauncher;
+import org.chromium.chrome.browser.ui.signin.history_sync.HistorySyncConfig;
+import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
+import org.chromium.components.signin.metrics.SigninAccessPoint;
+
+/** Test suite for {@link CreatorActionDelegateImpl}, especially the sign-in behavior. */
+@RunWith(BaseRobolectricTestRunner.class)
+public class CreatorActionDelegateImplTest {
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+    @Mock private SigninAndHistorySyncActivityLauncher mSigninLauncher;
+    @Mock private SnackbarManager mSnackbarManager;
+    @Mock private BookmarkModel mBookmarkModel;
+    @Mock private Activity mActivity;
+    @Mock private Profile mProfile;
+    @Mock private BottomSheetController mBottomSheetController;
+    @Mock private Intent mSigninIntent;
+    @Mock private CreatorCoordinator mCreatorCoordinator;
+
+    @Captor ArgumentCaptor<Intent> mIntentCaptor;
+
+    private CreatorActionDelegateImpl mCreatorActionDelegateImpl;
+
+    @Before
+    public void setUp() {
+        SigninAndHistorySyncActivityLauncherImpl.setLauncherForTest(mSigninLauncher);
+        mCreatorActionDelegateImpl =
+                new CreatorActionDelegateImpl(
+                        mActivity,
+                        mProfile,
+                        mSnackbarManager,
+                        mCreatorCoordinator,
+                        0,
+                        mBottomSheetController);
+    }
+
+    @Test
+    public void testShowSignInInterstitial() {
+        @SigninAccessPoint int signinAccessPoint = SigninAccessPoint.NTP_FEED_BOTTOM_PROMO;
+        when(mSigninLauncher.createBottomSheetSigninIntentOrShowError(
+                        any(), any(), any(), eq(signinAccessPoint)))
+                .thenReturn(mSigninIntent);
+
+        mCreatorActionDelegateImpl.showSignInInterstitial(
+                signinAccessPoint, mBottomSheetController);
+
+        ArgumentCaptor<BottomSheetSigninAndHistorySyncConfig> configCaptor =
+                ArgumentCaptor.forClass(BottomSheetSigninAndHistorySyncConfig.class);
+        verify(mSigninLauncher)
+                .createBottomSheetSigninIntentOrShowError(
+                        any(), any(), configCaptor.capture(), eq(signinAccessPoint));
+        BottomSheetSigninAndHistorySyncConfig config = configCaptor.getValue();
+        assertEquals(config.noAccountSigninMode, NoAccountSigninMode.BOTTOM_SHEET);
+        assertEquals(
+                config.withAccountSigninMode, WithAccountSigninMode.DEFAULT_ACCOUNT_BOTTOM_SHEET);
+        assertEquals(config.historyOptInMode, HistorySyncConfig.OptInMode.NONE);
+        assertNull(config.selectedCoreAccountId);
+        verify(mActivity).startActivity(mSigninIntent);
+    }
+}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/creator/OWNERS b/chrome/android/junit/src/org/chromium/chrome/browser/creator/OWNERS
new file mode 100644
index 0000000..b5a554a
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/creator/OWNERS
@@ -0,0 +1 @@
+file://chrome/browser/creator/OWNERS
\ No newline at end of file
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/feed/FeedActionDelegateImplTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/feed/FeedActionDelegateImplTest.java
index f292870..96e2d68 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/feed/FeedActionDelegateImplTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/feed/FeedActionDelegateImplTest.java
@@ -35,14 +35,12 @@
 import org.chromium.chrome.browser.native_page.NativePageNavigationDelegate;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.signin.SigninAndHistorySyncActivityLauncherImpl;
-import org.chromium.chrome.browser.signin.SyncConsentActivityLauncherImpl;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
 import org.chromium.chrome.browser.ui.signin.BottomSheetSigninAndHistorySyncConfig;
 import org.chromium.chrome.browser.ui.signin.BottomSheetSigninAndHistorySyncConfig.NoAccountSigninMode;
 import org.chromium.chrome.browser.ui.signin.BottomSheetSigninAndHistorySyncConfig.WithAccountSigninMode;
 import org.chromium.chrome.browser.ui.signin.SigninAndHistorySyncActivityLauncher;
-import org.chromium.chrome.browser.ui.signin.SyncConsentActivityLauncher;
 import org.chromium.chrome.browser.ui.signin.history_sync.HistorySyncConfig;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.signin.metrics.SigninAccessPoint;
@@ -53,7 +51,6 @@
 
     @Mock private WebFeedBridge.Natives mWebFeedBridgeJniMock;
 
-    @Mock private SyncConsentActivityLauncher mMockSyncConsentActivityLauncher;
     @Mock private SigninAndHistorySyncActivityLauncher mMockSigninLauncher;
 
     @Mock private SigninAndHistorySyncActivityLauncher mMockSigninAndHistorySyncActivityLauncher;
@@ -82,7 +79,6 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        SyncConsentActivityLauncherImpl.setLauncherForTest(mMockSyncConsentActivityLauncher);
         SigninAndHistorySyncActivityLauncherImpl.setLauncherForTest(
                 mMockSigninAndHistorySyncActivityLauncher);
         mFeedActionDelegateImpl =
@@ -101,22 +97,6 @@
 
     @Test
     @EnableFeatures(ChromeFeatureList.FEED_SHOW_SIGN_IN_COMMAND)
-    public void testShowSyncConsentActivity_shownWhenFlagEnabled() {
-        mFeedActionDelegateImpl.showSyncConsentActivity(SigninAccessPoint.NTP_FEED_TOP_PROMO);
-        verify(mMockSyncConsentActivityLauncher)
-                .launchActivityIfAllowed(any(), eq(SigninAccessPoint.NTP_FEED_TOP_PROMO));
-    }
-
-    @Test
-    @DisableFeatures(ChromeFeatureList.FEED_SHOW_SIGN_IN_COMMAND)
-    public void testShowSyncConsentActivity_dontShowWhenFlagDisabled() {
-        mFeedActionDelegateImpl.showSyncConsentActivity(SigninAccessPoint.NTP_FEED_TOP_PROMO);
-        verify(mMockSyncConsentActivityLauncher, never())
-                .launchActivityIfAllowed(any(), eq(SigninAccessPoint.NTP_FEED_TOP_PROMO));
-    }
-
-    @Test
-    @EnableFeatures(ChromeFeatureList.FEED_SHOW_SIGN_IN_COMMAND)
     public void testStartSigninFlow_shownWhenFlagEnabled() {
         when(mMockSigninAndHistorySyncActivityLauncher.createBottomSheetSigninIntentOrShowError(
                         any(), any(), any(), eq(SigninAccessPoint.NTP_FEED_TOP_PROMO)))
@@ -152,12 +132,12 @@
 
     @Test
     @EnableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
-    public void testShowSigninInterstitial_replaceSyncPromosWithSignInPromosEnabled() {
+    public void testShowSigninInterstitial() {
         when(mMockSigninAndHistorySyncActivityLauncher.createBottomSheetSigninIntentOrShowError(
                         any(), any(), any(), eq(SigninAccessPoint.NTP_FEED_CARD_MENU_PROMO)))
                 .thenReturn(mSigninIntent);
         mFeedActionDelegateImpl.showSignInInterstitial(
-                SigninAccessPoint.NTP_FEED_CARD_MENU_PROMO, null, null);
+                SigninAccessPoint.NTP_FEED_CARD_MENU_PROMO, null);
 
         ArgumentCaptor<BottomSheetSigninAndHistorySyncConfig> configCaptor =
                 ArgumentCaptor.forClass(BottomSheetSigninAndHistorySyncConfig.class);
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 398c736..9fe324f 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -174,6 +174,8 @@
     "autocomplete/chrome_autocomplete_scheme_classifier.h",
     "autocomplete/document_suggestions_service_factory.cc",
     "autocomplete/document_suggestions_service_factory.h",
+    "autocomplete/enterprise_search_aggregator_suggestions_service_factory.cc",
+    "autocomplete/enterprise_search_aggregator_suggestions_service_factory.h",
     "autocomplete/in_memory_url_index_factory.cc",
     "autocomplete/in_memory_url_index_factory.h",
     "autocomplete/provider_state_service_factory.cc",
@@ -3846,6 +3848,8 @@
       "payments/payment_request_factory.h",
       "payments/webapps/twa_package_helper.cc",
       "payments/webapps/twa_package_helper.h",
+      "performance_manager/execution_context_priority/side_panel_loading_voter.cc",
+      "performance_manager/execution_context_priority/side_panel_loading_voter.h",
       "performance_manager/mechanisms/page_discarder.cc",
       "performance_manager/mechanisms/page_discarder.h",
       "performance_manager/mechanisms/page_loader.cc",
@@ -3869,9 +3873,11 @@
       "performance_manager/policies/urgent_page_discarding_policy.cc",
       "performance_manager/policies/urgent_page_discarding_policy.h",
       "performance_manager/public/background_tab_loading_policy.h",
+      "performance_manager/public/side_panel_loading_policy.h",
       "performance_manager/public/user_tuning/battery_saver_mode_manager.h",
       "performance_manager/public/user_tuning/performance_detection_manager.h",
       "performance_manager/public/user_tuning/user_performance_tuning_manager.h",
+      "performance_manager/side_panel_loading_policy.cc",
       "performance_manager/user_tuning/battery_saver_mode_manager.cc",
       "performance_manager/user_tuning/cpu_health_tracker.cc",
       "performance_manager/user_tuning/cpu_health_tracker.h",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index f755f73..bdfa88d 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -2562,6 +2562,15 @@
         {"skip_device_check", "true"},
         {"use_large_favicon", "true"}};
 const FeatureEntry::FeatureParam
+    kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipSchemaCheck[] = {
+        {"skip_schema_check", "true"},
+        {"use_large_favicon", "true"}};
+const FeatureEntry::FeatureParam
+    kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipDeviceAndSchemaChecks
+        [] = {{"skip_device_check", "true"},
+              {"skip_schema_check", "true"},
+              {"use_large_favicon", "true"}};
+const FeatureEntry::FeatureParam
     kAndroidAppIntegrationWithFavicon_DelayTime200Ms[] = {
         {"schedule_delay_time_ms", "200"}};
 const FeatureEntry::FeatureParam
@@ -2569,23 +2578,32 @@
         {"schedule_delay_time_ms", "200"},
         {"use_large_favicon", "true"}};
 
-const FeatureEntry::FeatureVariation
-    kAndroidAppIntegrationWithFaviconVariations[] = {
-        {"Use large favicon (no delay)",
-         kAndroidAppIntegrationWithFavicon_UseLargeFavicon,
-         std::size(kAndroidAppIntegrationWithFavicon_UseLargeFavicon), nullptr},
-        {"Skip device check + use large favicon (no delay)",
-         kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipDeviceCheck,
-         std::size(
-             kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipDeviceCheck),
-         nullptr},
-        {"200ms delay", kAndroidAppIntegrationWithFavicon_DelayTime200Ms,
-         std::size(kAndroidAppIntegrationWithFavicon_DelayTime200Ms), nullptr},
-        {"200ms delay with large favicon",
-         kAndroidAppIntegrationWithFavicon_DelayTime200Ms_UseLargeFavicon,
-         std::size(
-             kAndroidAppIntegrationWithFavicon_DelayTime200Ms_UseLargeFavicon),
-         nullptr}};
+const FeatureEntry::FeatureVariation kAndroidAppIntegrationWithFaviconVariations[] =
+    {{"Use large favicon (no delay)",
+      kAndroidAppIntegrationWithFavicon_UseLargeFavicon,
+      std::size(kAndroidAppIntegrationWithFavicon_UseLargeFavicon), nullptr},
+     {"Skip device check + use large favicon (no delay)",
+      kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipDeviceCheck,
+      std::size(
+          kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipDeviceCheck),
+      nullptr},
+     {"Skip schema check + use large favicon (no delay)",
+      kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipSchemaCheck,
+      std::size(
+          kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipSchemaCheck),
+      nullptr},
+     {"Skip both device and schema checks + use large favicon (no delay)",
+      kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipDeviceAndSchemaChecks,
+      std::size(
+          kAndroidAppIntegrationWithFavicon_UseLargeFavicon_SkipDeviceAndSchemaChecks),
+      nullptr},
+     {"200ms delay", kAndroidAppIntegrationWithFavicon_DelayTime200Ms,
+      std::size(kAndroidAppIntegrationWithFavicon_DelayTime200Ms), nullptr},
+     {"200ms delay with large favicon",
+      kAndroidAppIntegrationWithFavicon_DelayTime200Ms_UseLargeFavicon,
+      std::size(
+          kAndroidAppIntegrationWithFavicon_DelayTime200Ms_UseLargeFavicon),
+      nullptr}};
 
 const FeatureEntry::FeatureParam
     kAndroidAppIntegrationModule_ForceCardShown_Pixel[] = {
@@ -5544,6 +5562,12 @@
      flag_descriptions::kNavBarColorAnimationDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(chrome::android::kNavBarColorAnimation)},
 
+    // Tab closure methods refactor.
+    {"tab-closure-method-refactor",
+     flag_descriptions::kTabClosureMethodRefactorName,
+     flag_descriptions::kTabClosureMethodRefactorDescription, kOsAndroid,
+     FEATURE_VALUE_TYPE(chrome::android::kTabClosureMethodRefactor)},
+
 #endif  // BUILDFLAG(IS_ANDROID)
     {"disallow-doc-written-script-loads",
      flag_descriptions::kDisallowDocWrittenScriptsUiName,
diff --git a/chrome/browser/ai/ai_data_keyed_service.cc b/chrome/browser/ai/ai_data_keyed_service.cc
index f00f538..57430f9 100644
--- a/chrome/browser/ai/ai_data_keyed_service.cc
+++ b/chrome/browser/ai/ai_data_keyed_service.cc
@@ -87,7 +87,9 @@
 
   optimization_guide::OnAIPageContentDone callback = base::BindOnce(
       &OnGotAIPageContentForModelPrototyping, std::move(continue_callback));
-  optimization_guide::GetAIPageContent(web_contents, std::move(callback));
+  optimization_guide::GetAIPageContent(
+      web_contents, optimization_guide::DefaultAIPageContentOptions(),
+      std::move(callback));
 }
 
 // Fills an AiData proto with information from GetInnerText. If no result,
diff --git a/chrome/browser/apps/guest_view/web_view_browsertest.cc b/chrome/browser/apps/guest_view/web_view_browsertest.cc
index f08e72b..b7fd754 100644
--- a/chrome/browser/apps/guest_view/web_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_browsertest.cc
@@ -3340,16 +3340,12 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasAccessAllowCamera) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testAllowCamera",
              "web_view/permissions_test/embedder_has_permission",
              NEEDS_TEST_SERVER);
 }
 
 IN_PROC_BROWSER_TEST_P(WebViewTest, PermissionsAPIEmbedderHasAccessDenyCamera) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testDenyCamera",
              "web_view/permissions_test/embedder_has_permission",
              NEEDS_TEST_SERVER);
@@ -3357,8 +3353,6 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasAccessAllowMicrophone) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testAllowMicrophone",
              "web_view/permissions_test/embedder_has_permission",
              NEEDS_TEST_SERVER);
@@ -3366,24 +3360,18 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasAccessDenyMicrophone) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testDenyMicrophone",
              "web_view/permissions_test/embedder_has_permission",
              NEEDS_TEST_SERVER);
 }
 
 IN_PROC_BROWSER_TEST_P(WebViewTest, PermissionsAPIEmbedderHasAccessAllowMedia) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testAllowMedia",
              "web_view/permissions_test/embedder_has_permission",
              NEEDS_TEST_SERVER);
 }
 
 IN_PROC_BROWSER_TEST_P(WebViewTest, PermissionsAPIEmbedderHasAccessDenyMedia) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testDenyMedia",
              "web_view/permissions_test/embedder_has_permission",
              NEEDS_TEST_SERVER);
@@ -3556,8 +3544,6 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasNoAccessAllowCamera) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testAllowCamera",
              "web_view/permissions_test/embedder_has_no_permission",
              NEEDS_TEST_SERVER);
@@ -3565,8 +3551,6 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasNoAccessDenyCamera) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testDenyCamera",
              "web_view/permissions_test/embedder_has_no_permission",
              NEEDS_TEST_SERVER);
@@ -3574,8 +3558,6 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasNoAccessAllowMicrophone) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testAllowMicrophone",
              "web_view/permissions_test/embedder_has_no_permission",
              NEEDS_TEST_SERVER);
@@ -3583,8 +3565,6 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasNoAccessDenyMicrophone) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testDenyMicrophone",
              "web_view/permissions_test/embedder_has_no_permission",
              NEEDS_TEST_SERVER);
@@ -3592,8 +3572,6 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasNoAccessAllowMedia) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testAllowMedia",
              "web_view/permissions_test/embedder_has_no_permission",
              NEEDS_TEST_SERVER);
@@ -3601,8 +3579,6 @@
 
 IN_PROC_BROWSER_TEST_P(WebViewTest,
                        PermissionsAPIEmbedderHasNoAccessDenyMedia) {
-  SKIP_FOR_MPARCH();  // TODO(crbug.com/40202416): Enable test for MPArch.
-
   TestHelper("testDenyMedia",
              "web_view/permissions_test/embedder_has_no_permission",
              NEEDS_TEST_SERVER);
diff --git a/chrome/browser/autocomplete/enterprise_search_aggregator_suggestions_service_factory.cc b/chrome/browser/autocomplete/enterprise_search_aggregator_suggestions_service_factory.cc
new file mode 100644
index 0000000..9965240
--- /dev/null
+++ b/chrome/browser/autocomplete/enterprise_search_aggregator_suggestions_service_factory.cc
@@ -0,0 +1,57 @@
+// Copyright 2025 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/autocomplete/enterprise_search_aggregator_suggestions_service_factory.h"
+
+#include <memory>
+
+#include "base/no_destructor.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_selections.h"
+#include "components/omnibox/browser/search_aggregator_suggestions_service.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition.h"
+
+// static
+SearchAggregatorSuggestionsService*
+EnterpriseSearchAggregatorSuggestionsServiceFactory::GetForProfile(
+    Profile* profile,
+    bool create_if_necessary) {
+  return static_cast<SearchAggregatorSuggestionsService*>(
+      GetInstance()->GetServiceForBrowserContext(profile, create_if_necessary));
+}
+
+// static
+EnterpriseSearchAggregatorSuggestionsServiceFactory*
+EnterpriseSearchAggregatorSuggestionsServiceFactory::GetInstance() {
+  static base::NoDestructor<EnterpriseSearchAggregatorSuggestionsServiceFactory>
+      instance;
+  return instance.get();
+}
+
+std::unique_ptr<KeyedService>
+EnterpriseSearchAggregatorSuggestionsServiceFactory::
+    BuildServiceInstanceForBrowserContext(
+        content::BrowserContext* context) const {
+  Profile* profile = Profile::FromBrowserContext(context);
+
+  return std::make_unique<SearchAggregatorSuggestionsService>(
+      profile->GetDefaultStoragePartition()
+          ->GetURLLoaderFactoryForBrowserProcess());
+}
+
+EnterpriseSearchAggregatorSuggestionsServiceFactory::
+    EnterpriseSearchAggregatorSuggestionsServiceFactory()
+    : ProfileKeyedServiceFactory(
+          "SearchAggregatorSuggestionsService",
+          ProfileSelections::Builder()
+              .WithRegular(ProfileSelection::kOriginalOnly)
+              .WithGuest(ProfileSelection::kNone)
+              // TODO(crbug.com/41488885): Check if this service is needed for
+              //   Ash Internals.
+              .WithAshInternals(ProfileSelection::kOriginalOnly)
+              .Build()) {}
+
+EnterpriseSearchAggregatorSuggestionsServiceFactory::
+    ~EnterpriseSearchAggregatorSuggestionsServiceFactory() = default;
diff --git a/chrome/browser/autocomplete/enterprise_search_aggregator_suggestions_service_factory.h b/chrome/browser/autocomplete/enterprise_search_aggregator_suggestions_service_factory.h
new file mode 100644
index 0000000..c6479ba
--- /dev/null
+++ b/chrome/browser/autocomplete/enterprise_search_aggregator_suggestions_service_factory.h
@@ -0,0 +1,46 @@
+// Copyright 2025 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_AUTOCOMPLETE_ENTERPRISE_SEARCH_AGGREGATOR_SUGGESTIONS_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_AUTOCOMPLETE_ENTERPRISE_SEARCH_AGGREGATOR_SUGGESTIONS_SERVICE_FACTORY_H_
+
+#include <memory>
+
+#include "base/no_destructor.h"
+#include "chrome/browser/profiles/profile_keyed_service_factory.h"
+
+namespace content {
+class BrowserContext;
+}  // namespace content
+
+class SearchAggregatorSuggestionsService;
+class Profile;
+class KeyedService;
+
+class EnterpriseSearchAggregatorSuggestionsServiceFactory
+    : public ProfileKeyedServiceFactory {
+ public:
+  static SearchAggregatorSuggestionsService* GetForProfile(
+      Profile* profile,
+      bool create_if_necessary);
+  static EnterpriseSearchAggregatorSuggestionsServiceFactory* GetInstance();
+
+  EnterpriseSearchAggregatorSuggestionsServiceFactory(
+      const EnterpriseSearchAggregatorSuggestionsServiceFactory&) = delete;
+  EnterpriseSearchAggregatorSuggestionsServiceFactory& operator=(
+      const EnterpriseSearchAggregatorSuggestionsServiceFactory&) = delete;
+
+ private:
+  friend base::NoDestructor<
+      EnterpriseSearchAggregatorSuggestionsServiceFactory>;
+
+  EnterpriseSearchAggregatorSuggestionsServiceFactory();
+  ~EnterpriseSearchAggregatorSuggestionsServiceFactory() override;
+
+  // Overrides from BrowserContextKeyedServiceFactory:
+  std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
+      content::BrowserContext* context) const override;
+};
+
+#endif  // CHROME_BROWSER_AUTOCOMPLETE_ENTERPRISE_SEARCH_AGGREGATOR_SUGGESTIONS_SERVICE_FACTORY_H_
diff --git a/chrome/browser/autocomplete/remote_suggestions_service_factory.cc b/chrome/browser/autocomplete/remote_suggestions_service_factory.cc
index df132f2..0836c117 100644
--- a/chrome/browser/autocomplete/remote_suggestions_service_factory.cc
+++ b/chrome/browser/autocomplete/remote_suggestions_service_factory.cc
@@ -6,6 +6,7 @@
 
 #include "base/no_destructor.h"
 #include "chrome/browser/autocomplete/document_suggestions_service_factory.h"
+#include "chrome/browser/autocomplete/enterprise_search_aggregator_suggestions_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "components/omnibox/browser/remote_suggestions_service.h"
 #include "content/public/browser/storage_partition.h"
@@ -32,6 +33,8 @@
   return std::make_unique<RemoteSuggestionsService>(
       DocumentSuggestionsServiceFactory::GetForProfile(
           profile, /*create_if_necessary=*/true),
+      EnterpriseSearchAggregatorSuggestionsServiceFactory::GetForProfile(
+          profile, /*create_if_necessary=*/true),
       profile->GetDefaultStoragePartition()
           ->GetURLLoaderFactoryForBrowserProcess());
 }
@@ -48,6 +51,7 @@
               .WithAshInternals(ProfileSelection::kOriginalOnly)
               .Build()) {
   DependsOn(DocumentSuggestionsServiceFactory::GetInstance());
+  DependsOn(EnterpriseSearchAggregatorSuggestionsServiceFactory::GetInstance());
 }
 
 RemoteSuggestionsServiceFactory::~RemoteSuggestionsServiceFactory() = default;
diff --git a/chrome/browser/autocomplete/search_provider_unittest.cc b/chrome/browser/autocomplete/search_provider_unittest.cc
index 8960a0de..2af13f7 100644
--- a/chrome/browser/autocomplete/search_provider_unittest.cc
+++ b/chrome/browser/autocomplete/search_provider_unittest.cc
@@ -141,8 +141,8 @@
     network::TestURLLoaderFactory* test_url_loader_factory,
     content::BrowserContext* context) {
   return std::make_unique<RemoteSuggestionsService>(
-      DocumentSuggestionsServiceFactory::GetForProfile(
-          Profile::FromBrowserContext(context), /*create_if_necessary=*/true),
+      /*document_suggestions_service=*/nullptr,
+      /*search_aggregator_suggestions_service=*/nullptr,
       test_url_loader_factory->GetSafeWeakWrapper());
 }
 
diff --git a/chrome/browser/auxiliary_search/BUILD.gn b/chrome/browser/auxiliary_search/BUILD.gn
index ece4857f..cdab0d3 100644
--- a/chrome/browser/auxiliary_search/BUILD.gn
+++ b/chrome/browser/auxiliary_search/BUILD.gn
@@ -126,6 +126,7 @@
     "//components/background_task_scheduler:public_java",
     "//components/browser_ui/theme/android:java_resources",
     "//components/segmentation_platform/public:public_java",
+    "//third_party/android_deps:guava_android_java",
     "//third_party/android_deps:protobuf_lite_runtime_java",
     "//third_party/androidx:androidx_appsearch_appsearch_builtin_types_java",
     "//third_party/androidx:androidx_appsearch_appsearch_java",
diff --git a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactory.java b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactory.java
index d719ef8..33097d6 100644
--- a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactory.java
+++ b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactory.java
@@ -13,6 +13,8 @@
 import org.chromium.base.ServiceLoaderUtil;
 import org.chromium.chrome.browser.auxiliary_search.AuxiliarySearchDonor.SetDocumentClassVisibilityForPackageCallback;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
+import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 
@@ -48,6 +50,21 @@
         return mHooks != null && mHooks.isEnabled();
     }
 
+    /**
+     * Returns whether Tab Sharing is enabled and the device is ready to use it. This check is used
+     * to control the visibility of UI like the toggle in Tabs Settings and opt in card on the magic
+     * stack. This function should NOT be used to determine whether to create a Tab Sharing
+     * controller.
+     */
+    public boolean isEnabledAndDeviceCompatible() {
+        boolean consumerSchemaFound =
+                ChromeSharedPreferences.getInstance()
+                        .readBoolean(
+                                ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, false);
+
+        return consumerSchemaFound && isEnabled();
+    }
+
     /** Returns whether the sharing Tabs with the system is enabled by default on the device. */
     public boolean isSettingDefaultEnabledByOs() {
         if (mHooksForTesting != null) {
@@ -100,6 +117,19 @@
         return mIsTablet;
     }
 
+    @Nullable
+    public String getSupportedPackageName() {
+        if (mHooksForTesting != null) {
+            return mHooksForTesting.getSupportedPackageName();
+        }
+
+        if (mHooks != null) {
+            return mHooks.getSupportedPackageName();
+        }
+
+        return null;
+    }
+
     private @Nullable AuxiliarySearchController createAuxiliarySearchControllerImp(
             @NonNull Context context,
             @NonNull Profile profile,
diff --git a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerImpl.java b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerImpl.java
index a52af97..8a2808101 100644
--- a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerImpl.java
+++ b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerImpl.java
@@ -130,6 +130,7 @@
             long startTimeMillis) {
         if (!mDonor.canDonate()) return;
 
+        // mDonor will cache the donation list if the initialization of the donor is in progress.
         mDonor.donateFavicons(
                 tabs,
                 tabIdToFaviconMap,
diff --git a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonor.java b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonor.java
index afcc3fe..e09d80b 100644
--- a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonor.java
+++ b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonor.java
@@ -16,6 +16,8 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.SearchResult;
@@ -23,6 +25,7 @@
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.builtintypes.GlobalSearchApplicationInfo;
 import androidx.appsearch.builtintypes.ImageObject;
 import androidx.appsearch.builtintypes.WebPage;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -59,21 +62,26 @@
         void setDocumentClassVisibility(String packageName, String sha256Certificate);
     }
 
+    @VisibleForTesting static final String SCHEMA = "builtin:GlobalSearchApplicationInfo";
+
     private static final String TAG = "AuxiliarySearchDonor";
     private static final String TAB_PREFIX = "Tab-";
-
     private static final Executor UI_THREAD_EXECUTOR =
             (Runnable r) -> PostTask.postTask(TaskTraits.UI_DEFAULT, r);
     private static boolean sSkipInitializationForTesting;
 
     private final Context mContext;
     private final String mNamespace;
+    private final boolean mSkipSchemaCheck;
+
     private ListenableFuture<AppSearchSession> mAppSearchSession;
+    private ListenableFuture<GlobalSearchSession> mGlobalSearchSession;
     private Long mTtlMillis;
     private boolean mIsSchemaSet;
     private List<WebPage> mPendingDocuments;
     private Callback<Boolean> mPendingCallback;
     private boolean mSharedTabsWithOsState;
+    private Boolean mIsDeviceCompatible;
 
     /** Static class that implements the initialization-on-demand holder idiom. */
     private static class LazyHolder {
@@ -88,6 +96,7 @@
     private AuxiliarySearchDonor() {
         mContext = ContextUtils.getApplicationContext();
         mNamespace = mContext.getPackageName();
+        mSkipSchemaCheck = AuxiliarySearchUtils.SKIP_SCHEMA_CHECK.getValue();
 
         mSharedTabsWithOsState = AuxiliarySearchUtils.isShareTabsWithOsEnabled();
         if (mSharedTabsWithOsState) {
@@ -96,15 +105,28 @@
     }
 
     /** Creates a session and initializes the schema type. */
-    void createSessionAndInit() {
-        if (sSkipInitializationForTesting) return;
-
+    boolean createSessionAndInit() {
         if (mAppSearchSession != null) {
-            return;
+            return false;
         }
 
+        if (sSkipInitializationForTesting) return true;
+
+        // There are 3 steps for initialization:
+        // 1) Set up a new app search session for tab donation and a global search session for
+        //    checking device compatibility.
+        // 2) Checks if the system has stored the consumer schema. This schema indicates the device
+        //    is capable to use the Tabs from donation.
+        // 3) Checks if the WebPage schema has been set for Tab donations.
+        // If 2) failed, closes the app search session.
         mAppSearchSession = createAppSearchSession();
-        maySetSchema();
+        mGlobalSearchSession = createGlobalSearchSession();
+        if (mSkipSchemaCheck) {
+            onConsumerSchemaSearched(/* success= */ true);
+        } else {
+            searchConsumerSchema(this::onConsumerSchemaSearched);
+        }
+        return true;
     }
 
     /** Creates a session asynchronously. */
@@ -115,18 +137,61 @@
                         .build());
     }
 
+    /** Creates a session asynchronously. */
+    @SuppressLint("NewApi")
+    private ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
+        return PlatformStorage.createGlobalSearchSessionAsync(
+                new PlatformStorage.GlobalSearchContext.Builder(mContext).build());
+    }
+
     /**
      * Sets the document schema for the current session.
      *
      * @return false if the schema has been set before.
      */
+    @SuppressWarnings("CheckResult")
+    boolean onConsumerSchemaSearched(boolean success) {
+        boolean ret = onConsumerSchemaSearchedImpl(success);
+
+        // Closes the mGlobalSearchSession after querying the schema.
+        Futures.transform(
+                mGlobalSearchSession,
+                session -> {
+                    session.close();
+                    mGlobalSearchSession = null;
+                    return null;
+                },
+                AsyncTask.THREAD_POOL_EXECUTOR);
+
+        return ret;
+    }
+
     @SuppressLint({"CheckResult", "NewApi"})
     @VisibleForTesting
-    boolean maySetSchema() {
+    boolean onConsumerSchemaSearchedImpl(boolean success) {
+        mIsDeviceCompatible = success;
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, success);
+
         mIsSchemaSet =
                 ChromeSharedPreferences.getInstance()
                         .readBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_IS_SCHEMA_SET, false);
-        if (mIsSchemaSet) return false;
+
+        if (!mIsDeviceCompatible) {
+            if (mIsSchemaSet) {
+                // If WebPage schema has been set before while the device isn't capable for Tab
+                // donations, clean up now.
+                deleteAllTabs(null);
+                closeSession();
+            }
+            return false;
+        }
+
+        if (mIsSchemaSet) {
+            // The WebPage schema only needs to be set once. Early exits if it has been set before.
+            handlePendingDonations();
+            return false;
+        }
 
         Futures.transformAsync(
                 mAppSearchSession,
@@ -190,13 +255,17 @@
         ChromeSharedPreferences.getInstance()
                 .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_IS_SCHEMA_SET, true);
 
+        handlePendingDonations();
+    }
+
+    private void handlePendingDonations() {
+        if (mPendingDocuments == null) return;
+
         // If there is any pending donation, donates the documents now.
-        if (mPendingDocuments != null) {
-            donateTabsImpl(mPendingDocuments, mPendingCallback);
-            mPendingDocuments.clear();
-            mPendingDocuments = null;
-            mPendingCallback = null;
-        }
+        donateTabsImpl(mPendingDocuments, mPendingCallback);
+        mPendingDocuments.clear();
+        mPendingDocuments = null;
+        mPendingCallback = null;
     }
 
     /**
@@ -304,14 +373,23 @@
         return builder.build();
     }
 
+    /**
+     * Implement Tab donations. If donation should be disabled, the donation list will be abandoned.
+     * If the session hasn't been initialized, the list will be cached and executed after the
+     * initialization is completed. The initialization process will clean up if donation should be
+     * disabled.
+     *
+     * @param docs The documents to donate.
+     * @param callback The callback to be called after donation is completed.
+     */
     @SuppressLint("CheckResult")
     private void donateTabsImpl(@NonNull List<WebPage> docs, @Nullable Callback<Boolean> callback) {
         if (mAppSearchSession == null) {
             return;
         }
 
-        if (!mIsSchemaSet) {
-            // If the schema hasn't been set yet, cache the donation list.
+        if (!initialized()) {
+            // If the initialization hasn't been completed yet, cache the donation list.
             mPendingDocuments = docs;
             mPendingCallback = callback;
             return;
@@ -366,7 +444,7 @@
      */
     @SuppressLint("CheckResult")
     @VisibleForTesting
-    public boolean deleteAllTabs(@NonNull Callback<Boolean> onDeleteCompleteCallback) {
+    public boolean deleteAllTabs(@Nullable Callback<Boolean> onDeleteCompleteCallback) {
         if (mAppSearchSession == null) return false;
 
         SearchSpec spec = new SearchSpec.Builder().addFilterNamespaces(mNamespace).build();
@@ -380,12 +458,12 @@
                             new FutureCallback<Void>() {
                                 @Override
                                 public void onSuccess(Void result) {
-                                    onDeleteCompleteCallback.onResult(true);
+                                    Callback.runNullSafe(onDeleteCompleteCallback, true);
                                 }
 
                                 @Override
                                 public void onFailure(Throwable t) {
-                                    onDeleteCompleteCallback.onResult(false);
+                                    Callback.runNullSafe(onDeleteCompleteCallback, false);
                                 }
                             },
                             AsyncTask.THREAD_POOL_EXECUTOR);
@@ -395,6 +473,8 @@
         return true;
     }
 
+    /** Called when config in Tabs settings is changed. */
+    @VisibleForTesting
     void onConfigChanged(boolean enabled, @Nullable Callback<Boolean> onDeleteCompleteCallback) {
         if (mSharedTabsWithOsState == enabled) return;
 
@@ -471,15 +551,100 @@
     }
 
     /**
-     * Returns whether the donor is able to donate Tabs. Returns true if the settings is enabled and
-     * the donor isn't destroyed.
+     * Returns whether the donor is able to donate Tabs. Returns true if 1) the settings is enabled,
+     * 2) the session isn't closed and 3) the device is compatible or the compatibility check is in
+     * progress.
      */
     boolean canDonate() {
-        return mSharedTabsWithOsState && mAppSearchSession != null;
+        if (!mSharedTabsWithOsState || mAppSearchSession == null) return false;
+
+        // If mIsDeviceCompatible is null, it means the checking of device compatibility is still
+        // working in progress. In this case, it is safe to let the caller to send a donation list,
+        // and this donor will either close the session or continue to donate the pending list when
+        // the check of mIsDeviceCompatible is done. The follow tasks are handled in {@link
+        // AuxiliarySearchDonor#onConsumerSchemaSearchedImpl(boolean)}.
+        if (Boolean.FALSE.equals(mIsDeviceCompatible)) return false;
+
+        return true;
+    }
+
+    /** Returns whether the donor is fully initialized. */
+    boolean initialized() {
+        return mIsSchemaSet && mIsDeviceCompatible != null;
+    }
+
+    /**
+     * Searches whether the device supports Tab donation feature.
+     *
+     * @param callback The callback to be called after the query is completed.
+     */
+    private void searchConsumerSchema(@NonNull Callback<Boolean> callback) {
+        String supportedPackageName =
+                AuxiliarySearchControllerFactory.getInstance().getSupportedPackageName();
+        if (supportedPackageName == null) {
+            callback.onResult(false);
+            return;
+        }
+
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .addFilterSchemas(SCHEMA)
+                        .addFilterPackageNames(supportedPackageName)
+                        .build();
+        ListenableFuture<SearchResults> searchFutureCallback =
+                Futures.transform(
+                        mGlobalSearchSession,
+                        session -> session.search("", searchSpec),
+                        UI_THREAD_EXECUTOR);
+
+        addRequestCallback(
+                searchFutureCallback,
+                (searchResults) -> {
+                    // Returns whether document is found for the given schema.
+                    iterateSearchResults(searchResults, callback);
+                },
+                AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    @SuppressWarnings({"CheckResult"})
+    private void iterateSearchResults(
+            @NonNull SearchResults searchResults, @NonNull Callback<Boolean> callback) {
+        Futures.transform(
+                searchResults.getNextPageAsync(),
+                page -> {
+                    onGetNextPage(page, callback);
+                    return null;
+                },
+                AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    @VisibleForTesting
+    @SuppressWarnings({"UnsafeOptInUsageError", "RequiresFeature"})
+    void onGetNextPage(@Nullable List<SearchResult> page, @NonNull Callback<Boolean> callback) {
+        if (page == null || page.isEmpty()) {
+            callback.onResult(false);
+            return;
+        }
+
+        for (int i = 0; i < page.size(); i++) {
+            GenericDocument genericDocument = page.get(i).getGenericDocument();
+            try {
+                GlobalSearchApplicationInfo info =
+                        genericDocument.toDocumentClass(GlobalSearchApplicationInfo.class);
+                if (info.getApplicationType()
+                        == GlobalSearchApplicationInfo.APPLICATION_TYPE_CONSUMER) {
+                    callback.onResult(true);
+                    return;
+                }
+            } catch (AppSearchException e) {
+                Log.i(TAG, "Failed to convert GenericDocument to" + " GlobalSearchApplicationInfo");
+            }
+        }
+        callback.onResult(false);
     }
 
     @SuppressLint("CheckResult")
-    public void searchDonationResultsForTesting(Callback<List<SearchResult>> callback) {
+    public void searchDonationResultsForTesting(@NonNull Callback<List<SearchResult>> callback) {
         SearchSpec searchSpec = new SearchSpec.Builder().addFilterNamespaces(mNamespace).build();
 
         ListenableFuture<SearchResults> searchFutureCallback =
@@ -518,6 +683,12 @@
         mPendingDocuments = docs;
     }
 
+    public void setPendingCallbackForTesting(Callback<Boolean> pendingCallbackForTesting) {
+        Callback<Boolean> oldPendingCallback = mPendingCallback;
+        mPendingCallback = pendingCallbackForTesting;
+        ResettersForTesting.register(() -> mPendingCallback = oldPendingCallback);
+    }
+
     public static void setSkipInitializationForTesting(boolean skipInitializationForTesting) {
         sSkipInitializationForTesting = skipInitializationForTesting;
         ResettersForTesting.register(() -> sSkipInitializationForTesting = false);
@@ -530,4 +701,17 @@
     public boolean getSharedTabsWithOsStateForTesting() {
         return mSharedTabsWithOsState;
     }
+
+    public void setSharedTabsWithOsStateForTesting(boolean sharedTabsWithOsState) {
+        boolean oldValue = mSharedTabsWithOsState;
+        mSharedTabsWithOsState = sharedTabsWithOsState;
+        ResettersForTesting.register(() -> mSharedTabsWithOsState = oldValue);
+    }
+
+    public void setAppSearchSessionForTesting(
+            ListenableFuture<AppSearchSession> sessionForTesting) {
+        ListenableFuture<AppSearchSession> oldSession = mAppSearchSession;
+        mAppSearchSession = sessionForTesting;
+        ResettersForTesting.register(() -> mAppSearchSession = oldSession);
+    }
 }
diff --git a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchHooks.java b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchHooks.java
index 1949d9a..43f3cb0d 100644
--- a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchHooks.java
+++ b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchHooks.java
@@ -40,4 +40,10 @@
 
     /** Sets whether the current device is a tablet. */
     default void setIsTablet(boolean isTablet) {}
+
+    /** Returns the package name of the supported app which reads the donated Tabs. */
+    @Nullable
+    default String getSupportedPackageName() {
+        return null;
+    }
 }
diff --git a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchUtils.java b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchUtils.java
index 7dc82f1..2f77db21 100644
--- a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchUtils.java
+++ b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchUtils.java
@@ -41,6 +41,10 @@
     static final BooleanCachedFeatureParam SHOW_THIRD_PARTY_CARD =
             ChromeFeatureList.sAndroidAppIntegrationModuleShowThirdPartyCard;
 
+    @VisibleForTesting
+    static final BooleanCachedFeatureParam SKIP_SCHEMA_CHECK =
+            ChromeFeatureList.sAndroidAppIntegrationWithFaviconSkipSchemaCheck;
+
     /** Convert a Bitmap instance to a byte array. */
     @Nullable
     public static byte[] bitmapToBytes(Bitmap bitmap) {
diff --git a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilder.java b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilder.java
index f30f153..5a5ae0e 100644
--- a/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilder.java
+++ b/chrome/browser/auxiliary_search/java/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilder.java
@@ -79,7 +79,7 @@
     @Override
     public boolean isEligible() {
         return ChromeFeatureList.sAndroidAppIntegrationModule.isEnabled()
-                && AuxiliarySearchControllerFactory.getInstance().isEnabled();
+                && AuxiliarySearchControllerFactory.getInstance().isEnabledAndDeviceCompatible();
     }
 
     @Override
diff --git a/chrome/browser/auxiliary_search/javatests/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorTest.java b/chrome/browser/auxiliary_search/javatests/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorTest.java
index 4be48ae..b47a5ad 100644
--- a/chrome/browser/auxiliary_search/javatests/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorTest.java
+++ b/chrome/browser/auxiliary_search/javatests/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorTest.java
@@ -81,7 +81,10 @@
 
     @Test
     @MediumTest
-    @EnableFeatures("AndroidAppIntegrationV2:content_ttl_hours/5")
+    @EnableFeatures({
+        "AndroidAppIntegrationV2:content_ttl_hours/5",
+        "AndroidAppIntegrationWithFavicon:skip_schema_check/true"
+    })
     @DisableIf.Build(sdk_is_less_than = VERSION_CODES.S, message = "The donation API is for S+.")
     public void testDonateTabs() {
         ThreadUtils.runOnUiThreadBlocking(() -> mAuxiliarySearchDonor.createSessionAndInit());
diff --git a/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactoryUnitTest.java b/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactoryUnitTest.java
index bf3f07e..312c005 100644
--- a/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactoryUnitTest.java
+++ b/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchControllerFactoryUnitTest.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.auxiliary_search;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -23,7 +24,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.robolectric.annotation.Config;
@@ -49,6 +49,7 @@
     @Mock private TabModelSelector mTabModelSelector;
     @Mock private AuxiliarySearchBridge.Natives mMockAuxiliarySearchBridgeJni;
     @Mock private FaviconHelper.Natives mMockFaviconHelperJni;
+    @Mock private AuxiliarySearchHooks mHooks;
 
     private AuxiliarySearchControllerFactory mFactory;
 
@@ -63,6 +64,7 @@
         AuxiliarySearchDonor.setSkipInitializationForTesting(true);
 
         mFactory = AuxiliarySearchControllerFactory.getInstance();
+        mFactory.setHooksForTesting(mHooks);
     }
 
     @Test
@@ -71,13 +73,11 @@
         mFactory.setHooksForTesting(null);
         assertFalse(mFactory.isEnabled());
 
-        AuxiliarySearchHooks hooksMock = Mockito.mock(AuxiliarySearchHooks.class);
-        when(hooksMock.isEnabled()).thenReturn(false);
-        mFactory.setHooksForTesting(hooksMock);
+        when(mHooks.isEnabled()).thenReturn(false);
+        mFactory.setHooksForTesting(mHooks);
         assertFalse(mFactory.isEnabled());
 
-        when(hooksMock.isEnabled()).thenReturn(true);
-        mFactory.setHooksForTesting(hooksMock);
+        when(mHooks.isEnabled()).thenReturn(true);
         assertTrue(mFactory.isEnabled());
     }
 
@@ -86,17 +86,14 @@
     @DisableFeatures(ChromeFeatureList.ANDROID_APP_INTEGRATION_V2)
     @Config(sdk = VERSION_CODES.Q)
     public void testCreateAuxiliarySearchController_LessThanS() {
-        AuxiliarySearchHooks hooksMock = Mockito.mock(AuxiliarySearchHooks.class);
-        when(hooksMock.isEnabled()).thenReturn(false);
-        mFactory.setHooksForTesting(hooksMock);
+        when(mHooks.isEnabled()).thenReturn(false);
         assertFalse(mFactory.isEnabled());
         assertNull(mFactory.createAuxiliarySearchController(mContext, mProfile, mTabModelSelector));
 
-        when(hooksMock.isEnabled()).thenReturn(true);
-        mFactory.setHooksForTesting(hooksMock);
+        when(mHooks.isEnabled()).thenReturn(true);
         assertTrue(mFactory.isEnabled());
         mFactory.createAuxiliarySearchController(mContext, mProfile, mTabModelSelector);
-        verify(hooksMock)
+        verify(mHooks)
                 .createAuxiliarySearchController(eq(mContext), eq(mProfile), eq(mTabModelSelector));
     }
 
@@ -105,35 +102,30 @@
     @EnableFeatures(ChromeFeatureList.ANDROID_APP_INTEGRATION_V2)
     @Config(sdk = VERSION_CODES.S)
     public void testCreateAuxiliarySearchController() {
-        AuxiliarySearchHooks hooksMock = Mockito.mock(AuxiliarySearchHooks.class);
-        when(hooksMock.isEnabled()).thenReturn(false);
-        mFactory.setHooksForTesting(hooksMock);
+        when(mHooks.isEnabled()).thenReturn(false);
         assertFalse(mFactory.isEnabled());
         assertNull(mFactory.createAuxiliarySearchController(mContext, mProfile, mTabModelSelector));
 
-        when(hooksMock.isEnabled()).thenReturn(true);
-        mFactory.setHooksForTesting(hooksMock);
+        when(mHooks.isEnabled()).thenReturn(true);
         assertTrue(mFactory.isEnabled());
         when(mProfile.isOffTheRecord()).thenReturn(false);
 
         AuxiliarySearchController controller =
                 mFactory.createAuxiliarySearchController(mContext, mProfile, mTabModelSelector);
         assertTrue(controller instanceof AuxiliarySearchControllerImpl);
-        verify(hooksMock, never())
+        verify(mHooks, never())
                 .createAuxiliarySearchController(eq(mContext), eq(mProfile), eq(mTabModelSelector));
     }
 
     @Test
     @SmallTest
     public void testIsSettingDefaultEnabledByOs() {
-        AuxiliarySearchHooks hooksMock = Mockito.mock(AuxiliarySearchHooks.class);
-        when(hooksMock.isEnabled()).thenReturn(false);
-        when(hooksMock.isSettingDefaultEnabledByOs()).thenReturn(true);
-        mFactory.setHooksForTesting(hooksMock);
+        when(mHooks.isEnabled()).thenReturn(false);
+        when(mHooks.isSettingDefaultEnabledByOs()).thenReturn(true);
 
         assertTrue(mFactory.isSettingDefaultEnabledByOs());
 
-        when(hooksMock.isSettingDefaultEnabledByOs()).thenReturn(false);
+        when(mHooks.isSettingDefaultEnabledByOs()).thenReturn(false);
         assertFalse(mFactory.isSettingDefaultEnabledByOs());
     }
 
@@ -151,4 +143,16 @@
         mFactory.setIsTablet(false);
         assertTrue(mFactory.isTablet());
     }
+
+    @Test
+    @SmallTest
+    public void testGetSupportedPackageName() {
+        String packageName = "name";
+        when(mHooks.getSupportedPackageName()).thenReturn(packageName);
+
+        assertEquals(packageName, mFactory.getSupportedPackageName());
+
+        mFactory.setHooksForTesting(null);
+        assertNull(mFactory.getSupportedPackageName());
+    }
 }
diff --git a/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorUnitTest.java b/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorUnitTest.java
index 6300dd3..938f143 100644
--- a/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorUnitTest.java
+++ b/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchDonorUnitTest.java
@@ -10,16 +10,25 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.SetSchemaResponse.MigrationFailure;
+import androidx.appsearch.builtintypes.GlobalSearchApplicationInfo;
 import androidx.appsearch.builtintypes.WebPage;
+import androidx.appsearch.exceptions.AppSearchException;
 import androidx.test.filters.SmallTest;
 
+import com.google.common.util.concurrent.ListenableFuture;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -32,7 +41,6 @@
 import org.chromium.base.Callback;
 import org.chromium.base.shared_preferences.SharedPreferencesManager;
 import org.chromium.base.test.BaseRobolectricTestRunner;
-import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.Features.EnableFeatures;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
@@ -42,13 +50,16 @@
 import java.util.List;
 
 /** Unit tests for AuxiliarySearchDonor. */
-@Batch(Batch.UNIT_TESTS)
 @RunWith(BaseRobolectricTestRunner.class)
+@SuppressWarnings("DoNotMock") // Mock ListenableFuture.
 public class AuxiliarySearchDonorUnitTest {
     @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
 
     @Mock private MigrationFailure mMigrationFailure;
     @Mock private AuxiliarySearchHooks mHooks;
+    @Mock private ListenableFuture<AppSearchSession> mAppSearchSession;
+    @Mock private AppSearchSession mSession;
+    @Mock private Callback<Boolean> mCallback;
 
     private AuxiliarySearchDonor mAuxiliarySearchDonor;
 
@@ -62,6 +73,20 @@
 
         AuxiliarySearchDonor.setSkipInitializationForTesting(true);
         mAuxiliarySearchDonor = AuxiliarySearchDonor.getInstance();
+        try {
+            when(mAppSearchSession.get()).thenReturn(mSession);
+            mAuxiliarySearchDonor.setAppSearchSessionForTesting(mAppSearchSession);
+        } catch (Exception e) {
+            // Just continue.
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testCreateSessionAndInit() {
+        // #createSessionAndInit() has been called in AuxiliarySearchDonor's constructor.
+        // Verifies that calling createSessionAndInit() again will early exit.
+        assertFalse(mAuxiliarySearchDonor.createSessionAndInit());
     }
 
     @Test
@@ -169,8 +194,9 @@
                 ChromePreferenceKeys.AUXILIARY_SEARCH_IS_SCHEMA_SET, true);
         assertFalse(mAuxiliarySearchDonor.getIsSchemaSetForTesting());
 
-        // Verifies not to set the schema again if it has been set.
-        assertFalse(mAuxiliarySearchDonor.maySetSchema());
+        // Verifies that #onConsumerSchemaSearchedImpl() returns false, i.e., not to set the schema
+        // again if it has been set.
+        assertFalse(mAuxiliarySearchDonor.onConsumerSchemaSearchedImpl(/* success= */ true));
         assertTrue(mAuxiliarySearchDonor.getIsSchemaSetForTesting());
 
         chromeSharedPreferences.removeKey(ChromePreferenceKeys.AUXILIARY_SEARCH_IS_SCHEMA_SET);
@@ -191,4 +217,93 @@
         assertTrue(mAuxiliarySearchDonor.getSharedTabsWithOsStateForTesting());
         assertTrue(AuxiliarySearchUtils.isShareTabsWithOsEnabled());
     }
+
+    @Test
+    @SmallTest
+    public void testCanDonate() {
+        mAuxiliarySearchDonor.setSharedTabsWithOsStateForTesting(
+                /* sharedTabsWithOsState= */ false);
+        assertFalse(mAuxiliarySearchDonor.canDonate());
+
+        mAuxiliarySearchDonor.setSharedTabsWithOsStateForTesting(/* sharedTabsWithOsState= */ true);
+        mAuxiliarySearchDonor.onConsumerSchemaSearchedImpl(/* success= */ false);
+        assertFalse(mAuxiliarySearchDonor.canDonate());
+
+        mAuxiliarySearchDonor.onConsumerSchemaSearchedImpl(/* success= */ true);
+        assertTrue(mAuxiliarySearchDonor.canDonate());
+    }
+
+    @Test
+    @SmallTest
+    public void testOnConsumerSchemaSearchedImpl() {
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_IS_SCHEMA_SET, true);
+        Callback<Boolean> callback = Mockito.mock(Callback.class);
+        mAuxiliarySearchDonor.setPendingCallbackForTesting(callback);
+
+        // Verifies that closeSession() which calls the pending callback is executed when device
+        // isn't capable for Tabs donation while previously donation was allowed (schema is set).
+        assertFalse(mAuxiliarySearchDonor.onConsumerSchemaSearchedImpl(/* success= */ false));
+        assertFalse(
+                ChromeSharedPreferences.getInstance()
+                        .readBoolean(
+                                ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND,
+                                false));
+        verify(callback).onResult(eq(false));
+
+        // Verifies that onConsumerSchemaSearchedImpl() doesn't reset the schema thus returns
+        // false, while AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND is set to be true.
+        assertFalse(mAuxiliarySearchDonor.onConsumerSchemaSearchedImpl(/* success= */ true));
+        assertTrue(
+                ChromeSharedPreferences.getInstance()
+                        .readBoolean(
+                                ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND,
+                                false));
+
+        // Verifies that onConsumerSchemaSearchedImpl() returns true to set the schema, and
+        // AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND is set to be true.
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_IS_SCHEMA_SET, false);
+        assertTrue(mAuxiliarySearchDonor.onConsumerSchemaSearchedImpl(/* success= */ true));
+        assertTrue(
+                ChromeSharedPreferences.getInstance()
+                        .readBoolean(
+                                ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND,
+                                false));
+    }
+
+    @Test
+    @SmallTest
+    public void testOnGetNextPage() {
+        List<SearchResult> page = new ArrayList<>();
+        assertTrue(page.isEmpty());
+
+        mAuxiliarySearchDonor.onGetNextPage(page, mCallback);
+        verify(mCallback).onResult(eq(false));
+
+        SearchResult searchResult1 =
+                createSearchResult(GlobalSearchApplicationInfo.APPLICATION_TYPE_PRODUCER);
+        SearchResult searchResult2 =
+                createSearchResult(GlobalSearchApplicationInfo.APPLICATION_TYPE_CONSUMER);
+
+        page.add(searchResult1);
+        mAuxiliarySearchDonor.onGetNextPage(page, mCallback);
+        verify(mCallback, times(2)).onResult(eq(false));
+
+        page.add(searchResult2);
+        mAuxiliarySearchDonor.onGetNextPage(page, mCallback);
+        verify(mCallback).onResult(eq(true));
+    }
+
+    private SearchResult createSearchResult(int applicationType) {
+        GlobalSearchApplicationInfo appInfo =
+                new GlobalSearchApplicationInfo.Builder("namespace", "id", applicationType)
+                        .setSchemaTypes(Arrays.asList(AuxiliarySearchDonor.SCHEMA))
+                        .build();
+        try {
+            return new SearchResult.Builder("package", "database").setDocument(appInfo).build();
+        } catch (AppSearchException e) {
+            return null;
+        }
+    }
 }
diff --git a/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilderUnitTest.java b/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilderUnitTest.java
index 9bcf3c9..52c00a7 100644
--- a/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilderUnitTest.java
+++ b/chrome/browser/auxiliary_search/junit/src/org/chromium/chrome/browser/auxiliary_search/module/AuxiliarySearchModuleBuilderUnitTest.java
@@ -81,6 +81,8 @@
         when(mHooks.isSettingDefaultEnabledByOs()).thenReturn(true);
         mFactory.setHooksForTesting(mHooks);
         assertTrue(mFactory.isEnabled());
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, true);
 
         mBuilder = new AuxiliarySearchModuleBuilder(mContext, mOpenSettingsRunnable);
     }
@@ -94,6 +96,14 @@
         when(mHooks.isEnabled()).thenReturn(false);
         assertFalse(mFactory.isEnabled());
         assertFalse(mBuilder.isEligible());
+
+        // Verifies that if the device doesn't provide a consumer schema, we no longer build the
+        // opt in card module.
+        when(mHooks.isEnabled()).thenReturn(true);
+        ChromeSharedPreferences.getInstance()
+                .writeBoolean(ChromePreferenceKeys.AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND, false);
+        assertTrue(mFactory.isEnabled());
+        assertFalse(mBuilder.isEligible());
     }
 
     @Test
diff --git a/chrome/browser/chrome_browser_interface_binders.cc b/chrome/browser/chrome_browser_interface_binders.cc
index 2cebc32..d048035 100644
--- a/chrome/browser/chrome_browser_interface_binders.cc
+++ b/chrome/browser/chrome_browser_interface_binders.cc
@@ -492,7 +492,7 @@
 
 #if BUILDFLAG(ENABLE_GLIC)
 #include "chrome/browser/glic/glic_enabling.h"
-#include "chrome/browser/ui/webui/glic/glic_ui.h"
+#include "chrome/browser/glic/glic_ui.h"
 #endif
 
 namespace chrome::internal {
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
index 83ff1fe..13eecea 100644
--- a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
@@ -54,18 +54,14 @@
     default;
 
 bool ControlledFrameMediaAccessHandler::SupportsStreamType(
-    content::WebContents* web_contents,
+    content::RenderFrameHost* render_frame_host,
     const blink::mojom::MediaStreamType type,
     const extensions::Extension* extension) {
-  if (!web_contents || extension) {
+  if (!render_frame_host || extension) {
     return false;
   }
-
-  // TODO(b/40238394): |GuestView| should be looked up from |RenderFrameHost|
-  // instead of |WebContents|. To fix this, |SupportsStreamType| could pass
-  // |RenderFrameHost| instead of |WebContents|.
   extensions::WebViewGuest* web_view =
-      extensions::WebViewGuest::FromWebContents(web_contents);
+      extensions::WebViewGuest::FromRenderFrameHost(render_frame_host);
 
   bool is_controlled_frame = web_view && web_view->attached() &&
                              web_view->IsOwnedByControlledFrameEmbedder();
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h
index d10a21e..08e7135e 100644
--- a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h
+++ b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h
@@ -42,7 +42,7 @@
   ~ControlledFrameMediaAccessHandler() override;
 
   // MediaAccessHandler implementation:
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType type,
                           const extensions::Extension* extension) override;
   bool CheckMediaAccessPermission(
diff --git a/chrome/browser/devtools/protocol/target_handler.cc b/chrome/browser/devtools/protocol/target_handler.cc
index dfd7a6f7..7ba4064 100644
--- a/chrome/browser/devtools/protocol/target_handler.cc
+++ b/chrome/browser/devtools/protocol/target_handler.cc
@@ -4,6 +4,10 @@
 
 #include "chrome/browser/devtools/protocol/target_handler.h"
 
+#include <ranges>
+#include <string_view>
+
+#include "base/notreached.h"
 #include "chrome/browser/devtools/chrome_devtools_manager_delegate.h"
 #include "chrome/browser/devtools/devtools_browser_context_manager.h"
 #include "chrome/browser/profiles/profile_manager.h"
@@ -11,6 +15,8 @@
 #include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_navigator.h"
 #include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
+#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
 #include "chrome/common/webui_url_constants.h"
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/web_contents.h"
@@ -79,6 +85,7 @@
     std::optional<int> top,
     std::optional<int> width,
     std::optional<int> height,
+    std::optional<std::string> window_state,
     std::optional<std::string> browser_context_id,
     std::optional<bool> enable_begin_frame_control,
     std::optional<bool> new_window,
@@ -141,13 +148,35 @@
         "Target position can only be set for new windows");
   }
 
+  static std::string_view kActionableWindowStates[] = {
+      protocol::Target::WindowStateEnum::Minimized,
+      protocol::Target::WindowStateEnum::Maximized,
+      protocol::Target::WindowStateEnum::Fullscreen,
+  };
+
+  bool set_window_state = !!window_state;
+  if (set_window_state) {
+    if (!create_new_window) {
+      return protocol::Response::ServerError(
+          "Target window state can only be set for new windows");
+    }
+    if (*window_state == protocol::Target::WindowStateEnum::Normal) {
+      set_window_state = false;
+    } else if (std::ranges::find(kActionableWindowStates, *window_state) ==
+               std::end(kActionableWindowStates)) {
+      return protocol::Response::ServerError("Invalid target window state: " +
+                                             *window_state);
+    }
+  }
+
   NavigateParams params = CreateNavigateParams(
       profile, gurl, ui::PAGE_TRANSITION_AUTO_TOPLEVEL, create_new_window,
       create_in_background, target_browser);
 
   Navigate(&params);
-  if (!params.navigated_or_inserted_contents)
+  if (!params.navigated_or_inserted_contents) {
     return protocol::Response::ServerError("Failed to open a new tab");
+  }
 
   if (set_window_position) {
     BrowserWindow* browser_window = params.browser->window();
@@ -168,6 +197,20 @@
     browser_window->SetBounds(bounds);
   }
 
+  if (set_window_state) {
+    if (*window_state == protocol::Target::WindowStateEnum::Minimized) {
+      params.browser->window()->Minimize();
+    } else if (*window_state == protocol::Target::WindowStateEnum::Maximized) {
+      params.browser->window()->Maximize();
+    } else if (*window_state == protocol::Target::WindowStateEnum::Fullscreen) {
+      params.browser->exclusive_access_manager()
+          ->fullscreen_controller()
+          ->ToggleBrowserFullscreenMode(/*user_initiated=*/false);
+    } else {
+      NOTREACHED();
+    }
+  }
+
   if (!create_in_background) {
     params.navigated_or_inserted_contents->Focus();
   }
diff --git a/chrome/browser/devtools/protocol/target_handler.h b/chrome/browser/devtools/protocol/target_handler.h
index 05332ea2..67cddb8 100644
--- a/chrome/browser/devtools/protocol/target_handler.h
+++ b/chrome/browser/devtools/protocol/target_handler.h
@@ -35,6 +35,7 @@
       std::optional<int> top,
       std::optional<int> width,
       std::optional<int> height,
+      std::optional<std::string> window_state,
       std::optional<std::string> browser_context_id,
       std::optional<bool> enable_begin_frame_control,
       std::optional<bool> new_window,
diff --git a/chrome/browser/extensions/BUILD.gn b/chrome/browser/extensions/BUILD.gn
index 3f7b740..dfa0ad3 100644
--- a/chrome/browser/extensions/BUILD.gn
+++ b/chrome/browser/extensions/BUILD.gn
@@ -1467,7 +1467,9 @@
       deps += [
         "//chrome/browser/glic:enabling",
         "//chrome/browser/glic:glic",
+        "//chrome/browser/glic:impl",
       ]
+      allow_circular_includes_from += [ "//chrome/browser/glic:impl" ]
     }
   }  # if (enable_extensions)
 }
diff --git a/chrome/browser/extensions/api/declarative_content/declarative_content_apitest.cc b/chrome/browser/extensions/api/declarative_content/declarative_content_apitest.cc
index 1123d5b..16e0763c 100644
--- a/chrome/browser/extensions/api/declarative_content/declarative_content_apitest.cc
+++ b/chrome/browser/extensions/api/declarative_content/declarative_content_apitest.cc
@@ -5,6 +5,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
@@ -258,7 +259,7 @@
             ExecuteScriptInBackgroundPage(
                 extension->id(),
                 base::StringPrintf(kSetIsBookmarkedRule,
-                                   match_is_bookmarked ? "true" : "false")));
+                                   base::ToString(match_is_bookmarked))));
   EXPECT_EQ(!match_is_bookmarked, action->GetIsVisible(tab_id));
 
   // Check rule evaluation on add/remove bookmark.
diff --git a/chrome/browser/extensions/api/declarative_content/declarative_content_is_bookmarked_condition_tracker_unittest.cc b/chrome/browser/extensions/api/declarative_content/declarative_content_is_bookmarked_condition_tracker_unittest.cc
index d053b3ef..315c7a3 100644
--- a/chrome/browser/extensions/api/declarative_content/declarative_content_is_bookmarked_condition_tracker_unittest.cc
+++ b/chrome/browser/extensions/api/declarative_content/declarative_content_is_bookmarked_condition_tracker_unittest.cc
@@ -12,6 +12,7 @@
 #include "base/containers/contains.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted.h"
+#include "base/strings/to_string.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
 #include "chrome/browser/extensions/api/declarative_content/content_predicate_evaluator.h"
@@ -139,16 +140,16 @@
     testing::AssertionResult result = testing::AssertionFailure();
     if (!is_bookmarked_predicate_success) {
       result << "IsBookmarkedPredicate(true): expected "
-             << (page_is_bookmarked ? "true" : "false") << " got "
-             << (page_is_bookmarked ? "false" : "true");
+             << base::ToString(page_is_bookmarked) << " got "
+             << base::ToString(!page_is_bookmarked);
     }
 
     if (!is_not_bookmarked_predicate_success) {
       if (!is_bookmarked_predicate_success)
         result << "; ";
       result << "IsBookmarkedPredicate(false): expected "
-             << (page_is_bookmarked ? "false" : "true") << " got "
-             << (page_is_bookmarked ? "true" : "false");
+             << base::ToString(!page_is_bookmarked) << " got "
+             << base::ToString(page_is_bookmarked);
     }
 
     return result;
diff --git a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
index d24ad8d..cf18d96f 100644
--- a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
+++ b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
@@ -29,6 +29,7 @@
 #include "base/sequence_checker.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/synchronization/lock.h"
 #include "base/task/single_thread_task_runner.h"
@@ -4406,7 +4407,7 @@
   for (const auto& test_case : test_cases) {
     SCOPED_TRACE(base::StringPrintf("Testing URL: %s, using referrer: %s",
                                     test_case.url.spec().c_str(),
-                                    test_case.use_referrer ? "true" : "false"));
+                                    base::ToString(test_case.use_referrer)));
 
     NavigateFrame(kFrameName1, test_case.url, test_case.use_referrer);
     EXPECT_EQ(test_case.expected_ext_1_badge_text,
@@ -4559,7 +4560,7 @@
           last_loaded_extension_id(), kGetOnRuleMatchedDebugScript);
 
   std::string expected_event_availability =
-      GetLoadType() == ExtensionLoadType::UNPACKED ? "true" : "false";
+      base::ToString(GetLoadType() == ExtensionLoadType::UNPACKED);
 
   ASSERT_EQ(expected_event_availability, actual_event_availability);
 }
diff --git a/chrome/browser/extensions/api/management/management_api_browsertest.cc b/chrome/browser/extensions/api/management/management_api_browsertest.cc
index 2f5d12c..2a3af85 100644
--- a/chrome/browser/extensions/api/management/management_api_browsertest.cc
+++ b/chrome/browser/extensions/api/management/management_api_browsertest.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 "extensions/browser/api/management/management_api.h"
+
 #include "base/auto_reset.h"
 #include "base/files/file_path.h"
 #include "base/files/scoped_temp_dir.h"
@@ -22,7 +24,6 @@
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_utils.h"
-#include "extensions/browser/api/management/management_api.h"
 #include "extensions/browser/api/management/management_api_constants.h"
 #include "extensions/browser/api_test_utils.h"
 #include "extensions/browser/extension_dialog_auto_confirm.h"
@@ -338,7 +339,6 @@
     scoped_refptr<ManagementSetEnabledFunction> function(
         new ManagementSetEnabledFunction);
     function->set_extension(extension);
-    const char* const enabled_string = enabled ? "true" : "false";
     if (user_gesture)
       function->set_user_gesture(true);
     function->SetRenderFrameHost(browser()
@@ -346,7 +346,8 @@
                                      ->GetActiveWebContents()
                                      ->GetPrimaryMainFrame());
     bool response = test_utils::RunFunction(
-        function.get(), base::StringPrintf("[\"%s\", %s]", kId, enabled_string),
+        function.get(),
+        base::StringPrintf("[\"%s\", %s]", kId, base::ToString(enabled)),
         browser()->profile(), api_test_utils::FunctionMode::kNone);
     if (expected_error.empty()) {
       EXPECT_EQ(true, response);
diff --git a/chrome/browser/extensions/api/messaging/messaging_apitest.cc b/chrome/browser/extensions/api/messaging/messaging_apitest.cc
index 1d53e789..0d5af11 100644
--- a/chrome/browser/extensions/api/messaging/messaging_apitest.cc
+++ b/chrome/browser/extensions/api/messaging/messaging_apitest.cc
@@ -19,6 +19,7 @@
 #include "base/run_loop.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/values.h"
@@ -333,8 +334,7 @@
                                           const char* message) {
     std::string command = base::StringPrintf(
         "assertions.canConnectAndSendMessages('%s', %s, %s)",
-        extension->id().c_str(),
-        extension->is_platform_app() ? "true" : "false",
+        extension->id().c_str(), base::ToString(extension->is_platform_app()),
         message ? base::StringPrintf("'%s'", message).c_str() : "undefined");
     int result = content::EvalJs(frame, command).ExtractInt();
     return static_cast<Result>(result);
@@ -596,7 +596,7 @@
                                            bool include_tls_channel_id,
                                            const char* message) {
     std::string args = "'" + extension->id() + "', ";
-    args += include_tls_channel_id ? "true" : "false";
+    args += base::ToString(include_tls_channel_id);
     if (message)
       args += std::string(", '") + message + "'";
     return content::EvalJs(
diff --git a/chrome/browser/extensions/api/tab_capture/tab_capture_performancetest.cc b/chrome/browser/extensions/api/tab_capture/tab_capture_performancetest.cc
index 7b232cdd..0934099 100644
--- a/chrome/browser/extensions/api/tab_capture/tab_capture_performancetest.cc
+++ b/chrome/browser/extensions/api/tab_capture/tab_capture_performancetest.cc
@@ -9,6 +9,7 @@
 #include "base/containers/flat_map.h"
 #include "base/files/file_util.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/test/trace_event_analyzer.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
@@ -307,7 +308,7 @@
   NavigateToTestPage(test_page_html_);
   const base::Value response = SendMessageToExtension(
       base::StringPrintf("{start:true, passThroughWebRTC:%s}",
-                         HasFlag(kTestThroughWebRTC) ? "true" : "false"));
+                         base::ToString(HasFlag(kTestThroughWebRTC))));
   ASSERT_TRUE(response.is_dict());
   const std::string* reason = response.GetDict().FindString("reason");
   ASSERT_TRUE(response.GetDict().FindBool("success").value_or(false))
diff --git a/chrome/browser/extensions/api/tabs/tabs_apitest.cc b/chrome/browser/extensions/api/tabs/tabs_apitest.cc
index 893f3bd..3a62be4b 100644
--- a/chrome/browser/extensions/api/tabs/tabs_apitest.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_apitest.cc
@@ -2,12 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/extensions/extension_apitest.h"
-
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/extensions/api/tabs/tabs_api.h"
+#include "chrome/browser/extensions/extension_apitest.h"
 #include "chrome/browser/extensions/extension_tab_util.h"
 #include "chrome/browser/prefs/incognito_mode_prefs.h"
 #include "chrome/browser/profiles/profile.h"
@@ -503,7 +503,7 @@
       OpenURLOffTheRecord(browser()->profile(), GURL("about:blank"));
   std::string args = base::StringPrintf(
       R"({"isIncognito": %s, "windowId": %d})",
-      is_incognito_enabled ? "true" : "false",
+      base::ToString(is_incognito_enabled),
       extensions::ExtensionTabUtil::GetWindowId(incognito_browser));
 
   EXPECT_TRUE(RunExtensionTest("tabs/basics/incognito",
diff --git a/chrome/browser/extensions/api/tabs/tabs_test.cc b/chrome/browser/extensions/api/tabs/tabs_test.cc
index a588df8f..7ebc72bf 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -18,6 +18,7 @@
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/test/gmock_expected_support.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_tick_clock.h"
@@ -2648,7 +2649,7 @@
     if (test_case.set_self_as_opener) {
       maybe_specify_set_self_as_opener =
           base::StringPrintf(", setSelfAsOpener: %s",
-                             *test_case.set_self_as_opener ? "true" : "false");
+                             base::ToString(*test_case.set_self_as_opener));
     }
     std::string script = base::StringPrintf(
         R"( chrome.windows.create({url: '%s'%s}); )", test_case.url.c_str(),
diff --git a/chrome/browser/extensions/api/web_request/web_request_apitest.cc b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
index de845766..22dfcfa 100644
--- a/chrome/browser/extensions/api/web_request/web_request_apitest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
@@ -20,6 +20,7 @@
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_split.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/synchronization/lock.h"
 #include "base/task/sequenced_task_runner.h"
@@ -1119,7 +1120,7 @@
         R"({"testName": "%s", "runInIncognito": %s})";
 
     return base::StringPrintf(custom_arg_format, test_name,
-                              GetEnableIncognito() ? "true" : "false");
+                              base::ToString(GetEnableIncognito()));
   }
 };
 
diff --git a/chrome/browser/extensions/desktop_android/desktop_android_extension_system.cc b/chrome/browser/extensions/desktop_android/desktop_android_extension_system.cc
index d686a2ae..b2fbfff 100644
--- a/chrome/browser/extensions/desktop_android/desktop_android_extension_system.cc
+++ b/chrome/browser/extensions/desktop_android/desktop_android_extension_system.cc
@@ -101,6 +101,8 @@
       scoped_refptr<const Extension> extension) override {}
   void PostDeactivateExtension(
       scoped_refptr<const Extension> extension) override {}
+  void ShowExtensionDisabledError(const Extension* extension,
+                                  bool is_remote_install) override {}
 
   void LoadExtensionForReload(
       const ExtensionId& extension_id,
@@ -117,6 +119,7 @@
     DCHECK_EQ(extension->id(), extension_id);
   }
 
+  bool CanAddExtension(const Extension* extension) override { return true; }
   bool CanEnableExtension(const Extension* extension) override { return true; }
   bool CanDisableExtension(const Extension* extension) override { return true; }
   bool ShouldBlockExtension(const Extension* extension) override {
diff --git a/chrome/browser/extensions/extension_service.cc b/chrome/browser/extensions/extension_service.cc
index 1c41559a..392218b2 100644
--- a/chrome/browser/extensions/extension_service.cc
+++ b/chrome/browser/extensions/extension_service.cc
@@ -107,6 +107,7 @@
 #include "extensions/browser/updater/manifest_fetch_data.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/crash_keys.h"
+#include "extensions/common/extension.h"
 #include "extensions/common/extension_features.h"
 #include "extensions/common/extension_urls.h"
 #include "extensions/common/features/feature_developer_mode_only.h"
@@ -789,6 +790,11 @@
   }
 }
 
+void ExtensionService::ShowExtensionDisabledError(const Extension* extension,
+                                                  bool is_remote_install) {
+  AddExtensionDisabledError(this, extension, is_remote_install);
+}
+
 void ExtensionService::OnUnpackedReloadFailure(const Extension* extension,
                                                const base::FilePath& file_path,
                                                const std::string& error) {
@@ -1103,13 +1109,9 @@
 // as appropriate.
 void ExtensionService::UnblockAllExtensions() {
   block_extensions_ = false;
-  const ExtensionSet to_unblock =
-      registry_->GenerateInstalledExtensionsSet(ExtensionRegistry::BLOCKED);
 
-  for (const auto& extension : to_unblock) {
-    registry_->RemoveBlocked(extension->id());
-    AddExtension(extension.get());
-  }
+  extension_registrar_.UnblockAllExtensions();
+
   // While extensions are blocked, we won't display any external install
   // warnings. Now that they are unblocked, we should update the error.
   external_install_manager_->UpdateExternalExtensionAlert();
@@ -1527,46 +1529,7 @@
 }
 
 void ExtensionService::AddExtension(const Extension* extension) {
-  if (!Manifest::IsValidLocation(extension->location())) {
-    // TODO(devlin): We should *never* add an extension with an invalid
-    // location, but some bugs (e.g. crbug.com/692069) seem to indicate we do.
-    // Track down the cases when this can happen, and remove this
-    // DumpWithoutCrashing() (possibly replacing it with a CHECK).
-    DEBUG_ALIAS_FOR_CSTR(extension_id_copy, extension->id().c_str(), 33);
-    ManifestLocation location = extension->location();
-    int creation_flags = extension->creation_flags();
-    Manifest::Type type = extension->manifest()->type();
-    base::debug::Alias(&location);
-    base::debug::Alias(&creation_flags);
-    base::debug::Alias(&type);
-    NOTREACHED();
-  }
-
-  // TODO(jstritar): We may be able to get rid of this branch by overriding the
-  // default extension state to DISABLED when the --disable-extensions flag
-  // is set (http://crbug.com/29067).
-  if (!extensions_enabled_ &&
-      !Manifest::ShouldAlwaysLoadExtension(extension->location(),
-                                           extension->is_theme()) &&
-      disable_flag_exempted_extensions_.count(extension->id()) == 0) {
-    return;
-  }
-
   extension_registrar_.AddExtension(extension);
-
-  if (registry_->disabled_extensions().Contains(extension->id())) {
-    // Show the extension disabled error if a permissions increase or a remote
-    // installation is the reason it was disabled, and no other reasons exist.
-    int reasons = extension_prefs_->GetDisableReasons(extension->id());
-    const int kReasonMask = disable_reason::DISABLE_PERMISSIONS_INCREASE |
-                            disable_reason::DISABLE_REMOTE_INSTALL;
-    if (reasons & kReasonMask && !(reasons & ~kReasonMask)) {
-      AddExtensionDisabledError(
-          this, extension,
-          extension_prefs_->HasDisableReason(
-              extension->id(), disable_reason::DISABLE_REMOTE_INSTALL));
-    }
-  }
 }
 
 void ExtensionService::AddComponentExtension(const Extension* extension) {
@@ -2299,6 +2262,19 @@
   CheckPermissionsIncrease(extension, !!old_extension);
 }
 
+bool ExtensionService::CanAddExtension(const Extension* extension) {
+  // TODO(jstritar): We may be able to get rid of this branch by overriding the
+  // default extension state to DISABLED when the --disable-extensions flag
+  // is set (http://crbug.com/29067).
+  if (!extensions_enabled_ &&
+      !Manifest::ShouldAlwaysLoadExtension(extension->location(),
+                                           extension->is_theme()) &&
+      disable_flag_exempted_extensions_.count(extension->id()) == 0) {
+    return false;
+  }
+  return true;
+}
+
 bool ExtensionService::CanEnableExtension(const Extension* extension) {
   return !system_->management_policy()->MustRemainDisabled(extension, nullptr);
 }
diff --git a/chrome/browser/extensions/extension_service.h b/chrome/browser/extensions/extension_service.h
index fe1a252c..1751cdad1 100644
--- a/chrome/browser/extensions/extension_service.h
+++ b/chrome/browser/extensions/extension_service.h
@@ -545,6 +545,10 @@
       const ExtensionId& extension_id,
       const base::FilePath& path,
       ExtensionRegistrar::LoadErrorBehavior load_error_behavior) override;
+  void ShowExtensionDisabledError(const Extension* extension,
+                                  bool is_remote_install) override;
+
+  bool CanAddExtension(const Extension* extension) override;
   bool CanEnableExtension(const Extension* extension) override;
   bool CanDisableExtension(const Extension* extension) override;
   bool ShouldBlockExtension(const Extension* extension) override;
diff --git a/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsControllerRobolectricTest.java b/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsControllerRobolectricTest.java
index 095e2b40..31035e8 100644
--- a/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsControllerRobolectricTest.java
+++ b/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsControllerRobolectricTest.java
@@ -64,6 +64,7 @@
 import org.robolectric.Robolectric;
 
 import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.HistogramWatcher;
 import org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.FooterProperties;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.components.autofill.payments.AccountType;
@@ -74,6 +75,7 @@
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
+import org.chromium.components.facilitated_payments.core.ui_utils.FopSelectorAction;
 import org.chromium.components.facilitated_payments.core.ui_utils.UiEvent;
 import org.chromium.components.payments.ui.InputProtector;
 import org.chromium.components.payments.ui.test_support.FakeClock;
@@ -254,14 +256,14 @@
         mCoordinator.showSheetForEwallet(List.of(EWALLET_1, EWALLET_2));
 
         // Verify the screen contents set in the model when 2 eWallets exist.
-        // TODO(crbug.com/40280186): Modify the assertions when other items of eWallet FOP selector
-        // are implemented.
         ModelList itemList =
                 mFacilitatedPaymentsPaymentMethodsModel.get(SCREEN_VIEW_MODEL).get(SCREEN_ITEMS);
-        assertThat(itemList.size(), is(3));
+        assertThat(itemList.size(), is(5));
         assertEquals(itemList.get(0).type, HEADER);
         assertEquals(itemList.get(1).type, EWALLET);
         assertEquals(itemList.get(2).type, EWALLET);
+        assertEquals(itemList.get(3).type, ADDITIONAL_INFO);
+        assertEquals(itemList.get(4).type, FOOTER);
     }
 
     @Test
@@ -284,14 +286,14 @@
         mCoordinator.showSheetForEwallet(List.of(EWALLET_1));
 
         // Verify the screen contents set in the model when only 1 eWallet account exists.
-        // TODO(crbug.com/40280186): Modify the assertions when other items of eWallet FOP selector
-        // are implemented.
         ModelList itemList =
                 mFacilitatedPaymentsPaymentMethodsModel.get(SCREEN_VIEW_MODEL).get(SCREEN_ITEMS);
-        assertThat(itemList.size(), is(3));
+        assertThat(itemList.size(), is(5));
         assertEquals(itemList.get(0).type, HEADER);
         assertEquals(itemList.get(1).type, EWALLET);
-        assertEquals(itemList.get(2).type, CONTINUE_BUTTON);
+        assertEquals(itemList.get(2).type, ADDITIONAL_INFO);
+        assertEquals(itemList.get(3).type, CONTINUE_BUTTON);
+        assertEquals(itemList.get(4).type, FOOTER);
     }
 
     @Test
@@ -423,10 +425,18 @@
     }
 
     @Test
-    public void testShowFinancialAccountsManagementSettings() {
+    public void testPixShowFinancialAccountsManagementSettings() {
         mCoordinator.showSheetForPix(List.of(BANK_ACCOUNT_1, BANK_ACCOUNT_2));
 
-        // The additional info is the last item of the screen items list right now.
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                        .PIX_FOP_SELECTOR_USER_ACTION_HISTOGRAM,
+                                FopSelectorAction.TURN_OFF_PAYMENT_PROMPT_LINK_CLICKED)
+                        .build();
+
+        // The additional info is the second to last item of the screen items list right now.
         int lastItemPos =
                 mFacilitatedPaymentsPaymentMethodsModel
                                 .get(SCREEN_VIEW_MODEL)
@@ -441,6 +451,103 @@
                 .get(SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
                 .run();
 
+        histogramWatcher.assertExpected();
+        verify(mDelegateMock).showFinancialAccountsManagementSettings(mContext);
+    }
+
+    @Test
+    public void testSingleFidoUnenrolledEwalletShowFinancialAccountsManagementSettings() {
+        mCoordinator.showSheetForEwallet(List.of(EWALLET_3));
+
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                                .EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM
+                                        + "SingleUnboundEwallet",
+                                FopSelectorAction.TURN_OFF_PAYMENT_PROMPT_LINK_CLICKED)
+                        .build();
+
+        // The additional info is the third to last item of the screen items list right now.
+        int lastItemPos =
+                mFacilitatedPaymentsPaymentMethodsModel
+                                .get(SCREEN_VIEW_MODEL)
+                                .get(SCREEN_ITEMS)
+                                .size()
+                        - 3;
+        mFacilitatedPaymentsPaymentMethodsModel
+                .get(SCREEN_VIEW_MODEL)
+                .get(SCREEN_ITEMS)
+                .get(lastItemPos)
+                .model
+                .get(SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
+                .run();
+
+        histogramWatcher.assertExpected();
+        verify(mDelegateMock).showFinancialAccountsManagementSettings(mContext);
+    }
+
+    @Test
+    public void testSingleFidoEnrolledEwalletShowFinancialAccountsManagementSettings() {
+        mCoordinator.showSheetForEwallet(List.of(EWALLET_2));
+
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                                .EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM
+                                        + "SingleBoundEwallet",
+                                FopSelectorAction.TURN_OFF_PAYMENT_PROMPT_LINK_CLICKED)
+                        .build();
+
+        // The additional info is the third to last item of the screen items list right now.
+        int lastItemPos =
+                mFacilitatedPaymentsPaymentMethodsModel
+                                .get(SCREEN_VIEW_MODEL)
+                                .get(SCREEN_ITEMS)
+                                .size()
+                        - 3;
+        mFacilitatedPaymentsPaymentMethodsModel
+                .get(SCREEN_VIEW_MODEL)
+                .get(SCREEN_ITEMS)
+                .get(lastItemPos)
+                .model
+                .get(SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
+                .run();
+
+        histogramWatcher.assertExpected();
+        verify(mDelegateMock).showFinancialAccountsManagementSettings(mContext);
+    }
+
+    @Test
+    public void testMultipleEwalletsShowFinancialAccountsManagementSettings() {
+        mCoordinator.showSheetForEwallet(List.of(EWALLET_3, EWALLET_4));
+
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                                .EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM
+                                        + "MultipleEwallets",
+                                FopSelectorAction.TURN_OFF_PAYMENT_PROMPT_LINK_CLICKED)
+                        .build();
+
+        // The additional info is the second to last item of the screen items list right now.
+        int lastItemPos =
+                mFacilitatedPaymentsPaymentMethodsModel
+                                .get(SCREEN_VIEW_MODEL)
+                                .get(SCREEN_ITEMS)
+                                .size()
+                        - 2;
+        mFacilitatedPaymentsPaymentMethodsModel
+                .get(SCREEN_VIEW_MODEL)
+                .get(SCREEN_ITEMS)
+                .get(lastItemPos)
+                .model
+                .get(SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
+                .run();
+
+        histogramWatcher.assertExpected();
         verify(mDelegateMock).showFinancialAccountsManagementSettings(mContext);
     }
 
@@ -505,16 +612,23 @@
     }
 
     @Test
-    public void testShowManagePaymentMethodsSettingsOnFooter() {
+    public void testPixShowManagePaymentMethodsSettingsOnFooter() {
         mCoordinator.showSheetForPix(List.of(BANK_ACCOUNT_1));
 
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                        .PIX_FOP_SELECTOR_USER_ACTION_HISTOGRAM,
+                                FopSelectorAction.MANAGE_PAYMENT_METHODS_OPTION_SELECTED)
+                        .build();
+
         int lastItemPos =
                 mFacilitatedPaymentsPaymentMethodsModel
                                 .get(SCREEN_VIEW_MODEL)
                                 .get(SCREEN_ITEMS)
                                 .size()
                         - 1;
-
         mFacilitatedPaymentsPaymentMethodsModel
                 .get(SCREEN_VIEW_MODEL)
                 .get(SCREEN_ITEMS)
@@ -523,6 +637,100 @@
                 .get(FooterProperties.SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
                 .run();
 
+        histogramWatcher.assertExpected();
+        verify(mDelegateMock).showManagePaymentMethodsSettings(mContext);
+    }
+
+    @Test
+    public void testSingleFidoUnenrolledEwalletShowManagePaymentMethodsSettingsOnFooter() {
+        mCoordinator.showSheetForEwallet(List.of(EWALLET_4));
+
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                                .EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM
+                                        + "SingleUnboundEwallet",
+                                FopSelectorAction.MANAGE_PAYMENT_METHODS_OPTION_SELECTED)
+                        .build();
+
+        int lastItemPos =
+                mFacilitatedPaymentsPaymentMethodsModel
+                                .get(SCREEN_VIEW_MODEL)
+                                .get(SCREEN_ITEMS)
+                                .size()
+                        - 1;
+        mFacilitatedPaymentsPaymentMethodsModel
+                .get(SCREEN_VIEW_MODEL)
+                .get(SCREEN_ITEMS)
+                .get(lastItemPos)
+                .model
+                .get(FooterProperties.SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
+                .run();
+
+        histogramWatcher.assertExpected();
+        verify(mDelegateMock).showManagePaymentMethodsSettings(mContext);
+    }
+
+    @Test
+    public void testSingleFidoEnrolledEwalletShowManagePaymentMethodsSettingsOnFooter() {
+        mCoordinator.showSheetForEwallet(List.of(EWALLET_1));
+
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                                .EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM
+                                        + "SingleBoundEwallet",
+                                FopSelectorAction.MANAGE_PAYMENT_METHODS_OPTION_SELECTED)
+                        .build();
+
+        int lastItemPos =
+                mFacilitatedPaymentsPaymentMethodsModel
+                                .get(SCREEN_VIEW_MODEL)
+                                .get(SCREEN_ITEMS)
+                                .size()
+                        - 1;
+        mFacilitatedPaymentsPaymentMethodsModel
+                .get(SCREEN_VIEW_MODEL)
+                .get(SCREEN_ITEMS)
+                .get(lastItemPos)
+                .model
+                .get(FooterProperties.SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
+                .run();
+
+        histogramWatcher.assertExpected();
+        verify(mDelegateMock).showManagePaymentMethodsSettings(mContext);
+    }
+
+    @Test
+    public void testMultipleEwalletsShowManagePaymentMethodsSettingsOnFooter() {
+        mCoordinator.showSheetForEwallet(List.of(EWALLET_3, EWALLET_2));
+
+        HistogramWatcher histogramWatcher =
+                HistogramWatcher.newBuilder()
+                        .expectIntRecord(
+                                FacilitatedPaymentsPaymentMethodsMediator
+                                                .EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM
+                                        + "MultipleEwallets",
+                                FopSelectorAction.MANAGE_PAYMENT_METHODS_OPTION_SELECTED)
+                        .build();
+
+        int lastItemPos =
+                mFacilitatedPaymentsPaymentMethodsModel
+                                .get(SCREEN_VIEW_MODEL)
+                                .get(SCREEN_ITEMS)
+                                .size()
+                        - 1;
+        mFacilitatedPaymentsPaymentMethodsModel
+                .get(SCREEN_VIEW_MODEL)
+                .get(SCREEN_ITEMS)
+                .get(lastItemPos)
+                .model
+                .get(FooterProperties.SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK)
+                .run();
+
+        histogramWatcher.assertExpected();
         verify(mDelegateMock).showManagePaymentMethodsSettings(mContext);
     }
 
diff --git a/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsMediator.java b/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsMediator.java
index 1808b7d3..1d001a8 100644
--- a/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsMediator.java
+++ b/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsMediator.java
@@ -76,11 +76,17 @@
     static final String PIX_BANK_ACCOUNT_TRANSACTION_LIMIT = "500";
 
     // This histogram name should be in sync with the one in
-    // components/facilitated_payments/core/metrics/facilitated_payments_metrics.cc:LogFopSelected.
+    // components/facilitated_payments/core/metrics/facilitated_payments_metrics.cc:LogPixFopSelected.
     @VisibleForTesting
-    static final String FOP_SELECTOR_USER_ACTION_HISTOGRAM =
+    static final String PIX_FOP_SELECTOR_USER_ACTION_HISTOGRAM =
             "FacilitatedPayments.Pix.FopSelector.UserAction";
 
+    // This histogram name should be in sync with the one in
+    // components/facilitated_payments/core/metrics/facilitated_payments_metrics.cc:LogEwalletFopSelected.
+    @VisibleForTesting
+    static final String EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM =
+            "FacilitatedPayments.Ewallet.FopSelector.UserAction.";
+
     private Context mContext;
     private PropertyModel mModel;
     private Delegate mDelegate;
@@ -115,18 +121,17 @@
             screenItems.add(new ListItem(BANK_ACCOUNT, model));
         }
 
-        screenItems.add(buildAdditionalInfo());
+        screenItems.add(buildPixAdditionalInfo());
 
         maybeShowContinueButton(screenItems, BANK_ACCOUNT);
 
         screenItems.add(0, buildPixHeader(mContext));
-        screenItems.add(buildFooter());
+        screenItems.add(buildPixFooter());
 
         mModel.set(VISIBLE_STATE, SHOWN);
         mInputProtector.markShowTime();
     }
 
-    // TODO(crbug.com/40280186): Implement the content of eWallet FOP selector.
     void showSheetForEwallet(List<Ewallet> ewallets) {
         mInputProtector.markShowTime();
         if (ewallets == null || ewallets.isEmpty()) {
@@ -143,9 +148,12 @@
             screenItems.add(new ListItem(EWALLET, model));
         }
 
+        screenItems.add(buildEwalletAdditionalInfo(ewallets));
+
         maybeShowContinueButton(screenItems, EWALLET);
 
         screenItems.add(0, buildEwalletHeader(mContext, ewallets));
+        screenItems.add(buildEwalletFooter(ewallets));
 
         mModel.set(VISIBLE_STATE, SHOWN);
         mInputProtector.markShowTime();
@@ -260,18 +268,32 @@
                 FacilitatedPaymentsPaymentMethodsProperties.ItemType.HEADER, headerBuilder.build());
     }
 
-    private ListItem buildFooter() {
+    private ListItem buildPixFooter() {
         return new ListItem(
                 FacilitatedPaymentsPaymentMethodsProperties.ItemType.FOOTER,
                 new PropertyModel.Builder(FooterProperties.ALL_KEYS)
                         .with(
                                 FooterProperties.SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK,
-                                () -> this.onManagePaymentMethodsOptionSelected())
+                                () ->
+                                        this.onManagePaymentMethodsOptionSelected(
+                                                PIX_FOP_SELECTOR_USER_ACTION_HISTOGRAM))
+                        .build());
+    }
+
+    private ListItem buildEwalletFooter(List<Ewallet> ewallets) {
+        return new ListItem(
+                FacilitatedPaymentsPaymentMethodsProperties.ItemType.FOOTER,
+                new PropertyModel.Builder(FooterProperties.ALL_KEYS)
+                        .with(
+                                FooterProperties.SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK,
+                                () ->
+                                        this.onManagePaymentMethodsOptionSelected(
+                                                getEwalletFopSelectorUserActionHistogram(ewallets)))
                         .build());
     }
 
     @VisibleForTesting
-    ListItem buildAdditionalInfo() {
+    ListItem buildPixAdditionalInfo() {
         return new ListItem(
                 FacilitatedPaymentsPaymentMethodsProperties.ItemType.ADDITIONAL_INFO,
                 new PropertyModel.Builder(AdditionalInfoProperties.ALL_KEYS)
@@ -280,7 +302,26 @@
                                 R.string.pix_payment_additional_info)
                         .with(
                                 SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK,
-                                () -> this.onTurnOffPaymentPromptLinkClicked())
+                                () ->
+                                        this.onTurnOffPaymentPromptLinkClicked(
+                                                PIX_FOP_SELECTOR_USER_ACTION_HISTOGRAM))
+                        .build());
+    }
+
+    @VisibleForTesting
+    ListItem buildEwalletAdditionalInfo(List<Ewallet> ewallets) {
+
+        return new ListItem(
+                FacilitatedPaymentsPaymentMethodsProperties.ItemType.ADDITIONAL_INFO,
+                new PropertyModel.Builder(AdditionalInfoProperties.ALL_KEYS)
+                        .with(
+                                AdditionalInfoProperties.DESCRIPTION_ID,
+                                R.string.ewallet_payment_additional_info)
+                        .with(
+                                SHOW_PAYMENT_METHOD_SETTINGS_CALLBACK,
+                                () ->
+                                        this.onTurnOffPaymentPromptLinkClicked(
+                                                getEwalletFopSelectorUserActionHistogram(ewallets)))
                         .build());
     }
 
@@ -348,24 +389,34 @@
         mDelegate.onEwalletSelected(ewallet.getInstrumentId());
     }
 
-    private void onManagePaymentMethodsOptionSelected() {
+    private void onManagePaymentMethodsOptionSelected(String histogramName) {
         mDelegate.showManagePaymentMethodsSettings(mContext);
 
         RecordHistogram.recordEnumeratedHistogram(
-                FOP_SELECTOR_USER_ACTION_HISTOGRAM,
+                histogramName,
                 FopSelectorAction.MANAGE_PAYMENT_METHODS_OPTION_SELECTED,
                 FopSelectorAction.MAX_VALUE + 1);
     }
 
-    private void onTurnOffPaymentPromptLinkClicked() {
+    private void onTurnOffPaymentPromptLinkClicked(String histogramName) {
         mDelegate.showFinancialAccountsManagementSettings(mContext);
 
         RecordHistogram.recordEnumeratedHistogram(
-                FOP_SELECTOR_USER_ACTION_HISTOGRAM,
+                histogramName,
                 FopSelectorAction.TURN_OFF_PAYMENT_PROMPT_LINK_CLICKED,
                 FopSelectorAction.MAX_VALUE + 1);
     }
 
+    private String getEwalletFopSelectorUserActionHistogram(List<Ewallet> ewallets) {
+        if (ewallets.size() == 1) {
+            if (ewallets.get(0).getIsFidoEnrolled()) {
+                return EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM + "SingleBoundEwallet";
+            }
+            return EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM + "SingleUnboundEwallet";
+        }
+        return EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM + "MultipleEwallets";
+    }
+
     @VisibleForTesting
     static String getBankAccountSummaryString(Context context, BankAccount bankAccount) {
         return context.getString(
diff --git a/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsViewTest.java b/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsViewTest.java
index b916270..aee09f2 100644
--- a/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsViewTest.java
+++ b/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsViewTest.java
@@ -368,13 +368,13 @@
 
     @Test
     @MediumTest
-    public void testDescriptionLine() {
+    public void testPixDescriptionLine() {
         runOnUiThreadBlocking(
                 () -> {
                     mModel.set(SCREEN, FOP_SELECTOR);
                     mModel.get(SCREEN_VIEW_MODEL)
                             .get(SCREEN_ITEMS)
-                            .add(mMediator.buildAdditionalInfo());
+                            .add(mMediator.buildPixAdditionalInfo());
                     mModel.set(VISIBLE_STATE, SHOWN);
                 });
         BottomSheetTestSupport.waitForOpen(mBottomSheetController);
@@ -388,6 +388,28 @@
 
     @Test
     @MediumTest
+    public void testEwalletDescriptionLine() {
+        runOnUiThreadBlocking(
+                () -> {
+                    mModel.set(SCREEN, FOP_SELECTOR);
+                    mModel.get(SCREEN_VIEW_MODEL)
+                            .get(SCREEN_ITEMS)
+                            .add(mMediator.buildEwalletAdditionalInfo(List.of(EWALLET_1)));
+                    mModel.set(VISIBLE_STATE, SHOWN);
+                });
+        BottomSheetTestSupport.waitForOpen(mBottomSheetController);
+
+        TextView descriptionLine1 = mView.getContentView().findViewById(R.id.description_line);
+        assertThat(
+                descriptionLine1.getText(),
+                hasToString(
+                        containsString(
+                                "Your saved auto-pay method may be used for this payment. To turn"
+                                        + " off eWallets in Chrome, go to your payment settings")));
+    }
+
+    @Test
+    @MediumTest
     public void testContinueButtonText() {
         runOnUiThreadBlocking(
                 () -> {
diff --git a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedActionDelegate.java b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedActionDelegate.java
index a8b6dc8..035ac84 100644
--- a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedActionDelegate.java
+++ b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedActionDelegate.java
@@ -8,9 +8,8 @@
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.signin.metrics.SigninAccessPoint;
 import org.chromium.content_public.browser.LoadUrlParams;
-import org.chromium.ui.base.WindowAndroid;
 
-/** Interface for Feed actions implemented by the Browser.*/
+/** Interface for Feed actions implemented by the Browser. */
 public interface FeedActionDelegate {
     /** Information about a page visit. */
     public class VisitResult {
@@ -105,13 +104,6 @@
     default void onStreamCreated() {}
 
     /**
-     * Shows a sign in activity as a result of a feed user action.
-     *
-     * @param signinAccessPoint the entry point for the signin.
-     */
-    default void showSyncConsentActivity(@SigninAccessPoint int signinAccessPoint) {}
-
-    /**
      * Starts sign in flow as a result of a feed user action.
      *
      * @param signinAccessPoint the entry point for the signin.
@@ -123,10 +115,8 @@
      *
      * @param signinAccessPoint the entry point for the signin.
      * @param mBottomSheetController bottomsheet controller attached to the activity.
-     * @param mWindowAndroid window used by the feed.
      */
     default void showSignInInterstitial(
             @SigninAccessPoint int signinAccessPoint,
-            BottomSheetController mBottomSheetController,
-            WindowAndroid mWindowAndroid) {}
+            BottomSheetController mBottomSheetController) {}
 }
diff --git a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedStream.java b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedStream.java
index ecc32b7..eb7dac51 100644
--- a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedStream.java
+++ b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/FeedStream.java
@@ -341,12 +341,7 @@
 
         @Override
         public void showSyncConsentPrompt() {
-            if (ChromeFeatureList.isEnabled(
-                    ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
-                startSigninFlow();
-            } else {
-                mActionDelegate.showSyncConsentActivity(SigninAccessPoint.NTP_FEED_BOTTOM_PROMO);
-            }
+            startSigninFlow();
         }
 
         @Override
@@ -357,9 +352,7 @@
         @Override
         public void showSignInInterstitial() {
             mActionDelegate.showSignInInterstitial(
-                    SigninAccessPoint.NTP_FEED_CARD_MENU_PROMO,
-                    mBottomSheetController,
-                    mWindowAndroid);
+                    SigninAccessPoint.NTP_FEED_CARD_MENU_PROMO, mBottomSheetController);
         }
 
         @Override
diff --git a/chrome/browser/fingerprinting_protection/fingerprinting_protection_filter_user_bypass_browsertest.cc b/chrome/browser/fingerprinting_protection/fingerprinting_protection_filter_user_bypass_browsertest.cc
index 393c478..9ae6ab2 100644
--- a/chrome/browser/fingerprinting_protection/fingerprinting_protection_filter_user_bypass_browsertest.cc
+++ b/chrome/browser/fingerprinting_protection/fingerprinting_protection_filter_user_bypass_browsertest.cc
@@ -98,6 +98,13 @@
       SetRulesetToDisallowURLsWithPathSuffix("included_script.html"));
   ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
 
+  histogram_tester.ExpectBucketCount(
+      ActivationDecisionHistogramName,
+      subresource_filter::ActivationDecision::ACTIVATED, 1);
+  histogram_tester.ExpectBucketCount(
+      ActivationLevelHistogramName,
+      subresource_filter::mojom::ActivationLevel::kEnabled, 1);
+
   const std::vector<const char*> kSubframeNames{"one", "two"};
   const std::vector<bool> kExpectOnlySecondSubframe{false, true};
   ASSERT_NO_FATAL_FAILURE(ExpectParsedScriptElementLoadedStatusInFrames(
@@ -118,7 +125,7 @@
       subresource_filter::ActivationDecision::URL_ALLOWLISTED, 1);
   histogram_tester.ExpectBucketCount(
       ActivationLevelHistogramName,
-      subresource_filter::mojom::ActivationLevel::kEnabled, 1);
+      subresource_filter::mojom::ActivationLevel::kDisabled, 1);
 
   const std::vector<bool> kExpectAllSubframes{true, true};
   ASSERT_NO_FATAL_FAILURE(ExpectParsedScriptElementLoadedStatusInFrames(
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index aeb682c..c3c46dd 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -8318,6 +8318,11 @@
     "expiry_milestone": 130
   },
   {
+    "name": "tab-closure-method-refactor",
+    "owners": [ "madhavpruthi@google.org", "ckitagawa@chromium.org" ],
+    "expiry_milestone": 140
+  },
+  {
     "name": "tab-drag-drop",
     "owners": [ "ranjithkagathi@google.com", "gurmeetk@google.com" ],
     "expiry_milestone": 130
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 88a8e999..dc6c3d8 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -4431,6 +4431,11 @@
     "opted into Edge-to-Edge. Requires DrawCutoutEdgeToEdge to also be "
     "enabled.";
 
+const char kTabClosureMethodRefactorName[] = "Tab closure method refactor";
+const char kTabClosureMethodRefactorDescription[] =
+    "Enables the refactored changes for tab closure methods where existing "
+    "methods usages are switched off and newly introduced are made active.";
+
 const char kDynamicSafeAreaInsetsName[] = "DynamicSafeAreaInsets";
 const char kDynamicSafeAreaInsetsDescription[] =
     "Dynamically change the safe area insets based on the bottom browser "
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 97da391..401fd4d9 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -2610,6 +2610,9 @@
 extern const char kEnableCommandLineOnNonRootedName[];
 extern const char kEnableCommandLineOnNoRootedDescription[];
 
+extern const char kTabClosureMethodRefactorName[];
+extern const char kTabClosureMethodRefactorDescription[];
+
 extern const char kEnableClipboardDataControlsAndroidName[];
 extern const char kEnableClipboardDataControlsAndroidDescription[];
 
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index 56787de1..5d4724c2 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -209,6 +209,7 @@
     &kBlockIntentsWhileLocked,
     &kBookmarkPaneAndroid,
     &kBottomBrowserControlsRefactor,
+    &kTabClosureMethodRefactor,
     &kBrowserControlsEarlyResize,
     &kCacheActivityTaskID,
     &kCastDeviceFilter,
@@ -654,6 +655,10 @@
              "CCTOpenInBrowserButtonIfEnabledByEmbedder",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kTabClosureMethodRefactor,
+             "TabClosureMethodRefactor",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 BASE_FEATURE(kCCTPrewarmTab, "CCTPrewarmTab", base::FEATURE_ENABLED_BY_DEFAULT);
 
 BASE_FEATURE(kCCTReportParallelRequestStatus,
diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h
index c9b6072..34aceabc 100644
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -180,6 +180,7 @@
 BASE_DECLARE_FEATURE(kTabStripGroupReorderAndroid);
 BASE_DECLARE_FEATURE(kTabStripIncognitoMigration);
 BASE_DECLARE_FEATURE(kTabStripLayoutOptimization);
+BASE_DECLARE_FEATURE(kTabClosureMethodRefactor);
 BASE_DECLARE_FEATURE(kTabStripTransitionInDesktopWindow);
 BASE_DECLARE_FEATURE(kTabSwitcherFullNewTabButton);
 BASE_DECLARE_FEATURE(kTabWindowManagerIndexReassignmentActivityFinishing);
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
index b4a0a4ef..4e732d3 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -521,6 +521,7 @@
     public static final String SUPPRESS_TOOLBAR_CAPTURES_AT_GESTURE_END =
             "SuppressToolbarCapturesAtGestureEnd";
     public static final String ENABLE_BATCH_UPLOAD_FROM_SETTINGS = "EnableBatchUploadFromSettings";
+    public static final String TAB_CLOSURE_METHOD_REFACTOR = "TabClosureMethodRefactor";
     public static final String TAB_DRAG_DROP_ANDROID = "TabDragDropAndroid";
     public static final String TAB_GROUP_CREATION_DIALOG_ANDROID = "TabGroupCreationDialogAndroid";
     public static final String TAB_GROUP_PANE_ANDROID = "TabGroupPaneAndroid";
@@ -762,6 +763,8 @@
             newCachedFlag(SMALLER_TAB_STRIP_TITLE_LIMIT, true);
     public static final CachedFlag sStartSurfaceReturnTime =
             newCachedFlag(START_SURFACE_RETURN_TIME, true);
+    public static final CachedFlag sTabClosureMethodRefactor =
+            newCachedFlag(TAB_CLOSURE_METHOD_REFACTOR, false);
     public static final CachedFlag sTabDragDropAsWindowAndroid =
             newCachedFlag(TAB_DRAG_DROP_ANDROID, false);
     public static final CachedFlag sTabGroupCreationDialogAndroid =
@@ -891,6 +894,7 @@
                     sSkipIsolatedSplitPreload,
                     sSmallerTabStripTitleLimit,
                     sStartSurfaceReturnTime,
+                    sTabClosureMethodRefactor,
                     sTabDragDropAsWindowAndroid,
                     sTabGroupCreationDialogAndroid,
                     sTabGroupPaneAndroid,
@@ -1021,6 +1025,10 @@
             sAndroidAppIntegrationWithFaviconZeroStateFaviconNumber =
                     newIntCachedFeatureParam(
                             ANDROID_APP_INTEGRATION_WITH_FAVICON, "zero_state_favicon_number", 5);
+
+    public static final BooleanCachedFeatureParam sAndroidAppIntegrationWithFaviconSkipSchemaCheck =
+            newBooleanCachedFeatureParam(
+                    ANDROID_APP_INTEGRATION_WITH_FAVICON, "skip_schema_check", false);
     public static final IntCachedFeatureParam sCctAuthTabEnableHttpsRedirectsVerificationTimeoutMs =
             newIntCachedFeatureParam(
                     CCT_AUTH_TAB_ENABLE_HTTPS_REDIRECTS, "verification_timeout_ms", 10_000);
@@ -1282,6 +1290,7 @@
                     sAndroidAppIntegrationModuleForceCardShow,
                     sAndroidAppIntegrationModuleShowThirdPartyCard,
                     sAndroidAppIntegrationWithFaviconScheduleDelayTimeMs,
+                    sAndroidAppIntegrationWithFaviconSkipSchemaCheck,
                     sAndroidAppIntegrationWithFaviconUseLargeFavicon,
                     sAndroidAppIntegrationWithFaviconZeroStateFaviconNumber,
                     sCctAuthTabEnableHttpsRedirectsVerificationTimeoutMs,
diff --git a/chrome/browser/glic/BUILD.gn b/chrome/browser/glic/BUILD.gn
index ce9e98a..8381ba1 100644
--- a/chrome/browser/glic/BUILD.gn
+++ b/chrome/browser/glic/BUILD.gn
@@ -2,6 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//chrome/common/features.gni")
+import("//mojo/public/tools/bindings/mojom.gni")
+
+assert(enable_glic)
+
 source_set("glic") {
   sources = [
     "border_view.h",
@@ -11,28 +16,31 @@
     "glic_keyed_service.h",
     "glic_keyed_service_factory.h",
     "glic_page_context_fetcher.h",
+    "glic_page_handler.h",
     "glic_pref_names.h",
     "glic_profile_configuration.h",
     "glic_profile_manager.h",
     "glic_tab_indicator_helper.h",
+    "glic_ui.h",
     "glic_view.h",
     "glic_web_client_access.h",
     "glic_window_controller.h",
     "guest_util.h",
   ]
   public_deps = [
+    ":mojo_bindings",
     "//base",
     "//chrome/browser/content_extraction:content_extraction",
     "//chrome/browser/profiles:profile",
     "//chrome/browser/ui:browser_list",
     "//chrome/browser/ui/browser_window:browser_window",
     "//chrome/browser/ui/tabs:tab_strip_model_observer",
-    "//chrome/browser/ui/webui/glic:mojo_bindings",
     "//components/prefs",
     "//components/signin/public/identity_manager:identity_manager",
     "//content/public/browser",
     "//ui/views:views",
     "//ui/views/controls/webview",
+    "//ui/webui",
   ]
 }
 
@@ -45,9 +53,11 @@
     "glic_keyed_service.cc",
     "glic_keyed_service_factory.cc",
     "glic_page_context_fetcher.cc",
+    "glic_page_handler.cc",
     "glic_profile_configuration.cc",
     "glic_profile_manager.cc",
     "glic_tab_indicator_helper.cc",
+    "glic_ui.cc",
     "glic_view.cc",
     "glic_window_controller.cc",
     "guest_util.cc",
@@ -60,6 +70,8 @@
     "//chrome/browser:global_features",
     "//chrome/browser/glic/launcher",
     "//chrome/browser/media/webrtc",
+    "//chrome/browser/profiles:profile_util",
+    "//chrome/browser/resources/glic:resources",
     "//chrome/browser/ui:browser_element_identifiers",
     "//chrome/browser/ui:browser_list",
     "//chrome/browser/ui:browser_navigator_params_headers",
@@ -72,10 +84,24 @@
     "//components/guest_view/browser",
     "//components/sessions",
     "//extensions/browser:browser",
+    "//ui/webui",
     "//url",
   ]
 }
 
+mojom("mojo_bindings") {
+  sources = [ "glic.mojom" ]
+  webui_module_path = "/"
+
+  public_deps = [
+    "//mojo/public/mojom/base",
+    "//skia/public/mojom",
+    "//ui/gfx/geometry/mojom",
+    "//url/mojom:url_mojom_gurl",
+    "//url/mojom:url_mojom_origin",
+  ]
+}
+
 source_set("enabling") {
   sources = [
     "glic_enabling.cc",
diff --git a/chrome/browser/glic/OWNERS b/chrome/browser/glic/OWNERS
index 9940fa4..f85f139 100644
--- a/chrome/browser/glic/OWNERS
+++ b/chrome/browser/glic/OWNERS
@@ -3,3 +3,6 @@
 andreaxg@google.com
 cuianthony@chromium.org
 iwells@chromium.org
+
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/chrome/browser/ui/webui/glic/glic.mojom b/chrome/browser/glic/glic.mojom
similarity index 100%
rename from chrome/browser/ui/webui/glic/glic.mojom
rename to chrome/browser/glic/glic.mojom
diff --git a/chrome/browser/glic/glic_keyed_service.cc b/chrome/browser/glic/glic_keyed_service.cc
index 30f6027..6293aca 100644
--- a/chrome/browser/glic/glic_keyed_service.cc
+++ b/chrome/browser/glic/glic_keyed_service.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/glic/glic_keyed_service.h"
 
 #include "chrome/browser/glic/border_view.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "chrome/browser/glic/glic_enabling.h"
 #include "chrome/browser/glic/glic_focused_tab_manager.h"
 #include "chrome/browser/glic/glic_page_context_fetcher.h"
@@ -13,7 +14,6 @@
 #include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_navigator.h"
 #include "chrome/browser/ui/browser_navigator_params.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/common/url_constants.h"
 #include "ui/base/page_transition_types.h"
@@ -47,8 +47,6 @@
 
   profile_manager_->OnUILaunching(this);
   window_controller_.Show(glic_button_view);
-  // TODO(crbug.com/384740189): Find out if/how to start the border animation
-  // when web_contents is not available yet.
 }
 
 base::CallbackListSubscription GlicKeyedService::AddFocusedTabChangedCallback(
diff --git a/chrome/browser/glic/glic_keyed_service.h b/chrome/browser/glic/glic_keyed_service.h
index a9ccea6e..be92643 100644
--- a/chrome/browser/glic/glic_keyed_service.h
+++ b/chrome/browser/glic/glic_keyed_service.h
@@ -9,11 +9,11 @@
 
 #include "base/callback_list.h"
 #include "base/memory/raw_ptr.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "chrome/browser/glic/glic_cookie_synchronizer.h"
 #include "chrome/browser/glic/glic_focused_tab_manager.h"
 #include "chrome/browser/glic/glic_profile_configuration.h"
 #include "chrome/browser/glic/glic_window_controller.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "ui/views/view.h"
 
diff --git a/chrome/browser/glic/glic_page_context_fetcher.cc b/chrome/browser/glic/glic_page_context_fetcher.cc
index bbc3015..7f2aaa1 100644
--- a/chrome/browser/glic/glic_page_context_fetcher.cc
+++ b/chrome/browser/glic/glic_page_context_fetcher.cc
@@ -8,7 +8,7 @@
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/thread_pool.h"
 #include "chrome/browser/content_extraction/inner_text.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "components/favicon/content/content_favicon_driver.h"
 #include "components/sessions/content/session_tab_helper.h"
 #include "content/public/browser/render_widget_host_view.h"
diff --git a/chrome/browser/glic/glic_page_context_fetcher.h b/chrome/browser/glic/glic_page_context_fetcher.h
index f69e6913..c2e1d48 100644
--- a/chrome/browser/glic/glic_page_context_fetcher.h
+++ b/chrome/browser/glic/glic_page_context_fetcher.h
@@ -11,8 +11,8 @@
 #include "base/functional/callback_forward.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/content_extraction/inner_text.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom-forward.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
+#include "chrome/browser/glic/glic.mojom-forward.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "content/public/browser/web_contents_observer.h"
 #include "third_party/skia/include/core/SkSize.h"
 
diff --git a/chrome/browser/ui/webui/glic/glic_page_handler.cc b/chrome/browser/glic/glic_page_handler.cc
similarity index 98%
rename from chrome/browser/ui/webui/glic/glic_page_handler.cc
rename to chrome/browser/glic/glic_page_handler.cc
index 61dbdca1..f808e47 100644
--- a/chrome/browser/ui/webui/glic/glic_page_handler.cc
+++ b/chrome/browser/glic/glic_page_handler.cc
@@ -2,11 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/ui/webui/glic/glic_page_handler.h"
+#include "chrome/browser/glic/glic_page_handler.h"
 
 #include "base/strings/utf_string_conversions.h"
 #include "base/version_info/version_info.h"
 #include "chrome/browser/browser_process.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "chrome/browser/glic/glic_keyed_service.h"
 #include "chrome/browser/glic/glic_keyed_service_factory.h"
 #include "chrome/browser/glic/glic_pref_names.h"
@@ -15,7 +16,6 @@
 #include "chrome/browser/profiles/profile_attributes_storage.h"
 #include "chrome/browser/profiles/profile_manager.h"
 #include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
 #include "components/prefs/pref_service.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
 #include "mojo/public/cpp/bindings/callback_helpers.h"
diff --git a/chrome/browser/ui/webui/glic/glic_page_handler.h b/chrome/browser/glic/glic_page_handler.h
similarity index 86%
rename from chrome/browser/ui/webui/glic/glic_page_handler.h
rename to chrome/browser/glic/glic_page_handler.h
index 998ba3f..cfb6b80 100644
--- a/chrome/browser/ui/webui/glic/glic_page_handler.h
+++ b/chrome/browser/glic/glic_page_handler.h
@@ -2,10 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef CHROME_BROWSER_UI_WEBUI_GLIC_GLIC_PAGE_HANDLER_H_
-#define CHROME_BROWSER_UI_WEBUI_GLIC_GLIC_PAGE_HANDLER_H_
+#ifndef CHROME_BROWSER_GLIC_GLIC_PAGE_HANDLER_H_
+#define CHROME_BROWSER_GLIC_GLIC_PAGE_HANDLER_H_
 
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 
@@ -49,4 +49,4 @@
 };
 
 }  // namespace glic
-#endif  // CHROME_BROWSER_UI_WEBUI_GLIC_GLIC_PAGE_HANDLER_H_
+#endif  // CHROME_BROWSER_GLIC_GLIC_PAGE_HANDLER_H_
diff --git a/chrome/browser/ui/webui/glic/glic_ui.cc b/chrome/browser/glic/glic_ui.cc
similarity index 96%
rename from chrome/browser/ui/webui/glic/glic_ui.cc
rename to chrome/browser/glic/glic_ui.cc
index 638600f3..07ae716 100644
--- a/chrome/browser/ui/webui/glic/glic_ui.cc
+++ b/chrome/browser/glic/glic_ui.cc
@@ -2,14 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/ui/webui/glic/glic_ui.h"
+#include "chrome/browser/glic/glic_ui.h"
 
 #include <string>
 
 #include "base/command_line.h"
 #include "chrome/browser/extensions/tab_helper.h"
+#include "chrome/browser/glic/glic_page_handler.h"
 #include "chrome/browser/glic/guest_util.h"
-#include "chrome/browser/ui/webui/glic/glic_page_handler.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/webui_url_constants.h"
diff --git a/chrome/browser/ui/webui/glic/glic_ui.h b/chrome/browser/glic/glic_ui.h
similarity index 82%
rename from chrome/browser/ui/webui/glic/glic_ui.h
rename to chrome/browser/glic/glic_ui.h
index 8162c9b..76d8ba9 100644
--- a/chrome/browser/ui/webui/glic/glic_ui.h
+++ b/chrome/browser/glic/glic_ui.h
@@ -2,11 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef CHROME_BROWSER_UI_WEBUI_GLIC_GLIC_UI_H_
-#define CHROME_BROWSER_UI_WEBUI_GLIC_GLIC_UI_H_
+#ifndef CHROME_BROWSER_GLIC_GLIC_UI_H_
+#define CHROME_BROWSER_GLIC_GLIC_UI_H_
 
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
-#include "chrome/common/webui_url_constants.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "content/public/browser/web_ui_controller.h"
 #include "content/public/browser/webui_config.h"
 #include "content/public/common/url_constants.h"
@@ -44,4 +43,4 @@
 };
 
 }  // namespace glic
-#endif  // CHROME_BROWSER_UI_WEBUI_GLIC_GLIC_UI_H_
+#endif  // CHROME_BROWSER_GLIC_GLIC_UI_H_
diff --git a/chrome/browser/glic/glic_web_client_access.h b/chrome/browser/glic/glic_web_client_access.h
index fe98ca3..53b96bf 100644
--- a/chrome/browser/glic/glic_web_client_access.h
+++ b/chrome/browser/glic/glic_web_client_access.h
@@ -7,7 +7,7 @@
 
 // Interface to the glic web client, provided by the glic WebUI.
 #include "base/functional/callback_forward.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom-forward.h"
+#include "chrome/browser/glic/glic.mojom-forward.h"
 
 namespace glic {
 
diff --git a/chrome/browser/glic/glic_window_controller.cc b/chrome/browser/glic/glic_window_controller.cc
index 12f9fbb..e056b21 100644
--- a/chrome/browser/glic/glic_window_controller.cc
+++ b/chrome/browser/glic/glic_window_controller.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/glic/glic_window_controller.h"
 
 #include "base/check.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "chrome/browser/glic/glic_view.h"
 #include "chrome/browser/media/audio_ducker.h"
 #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
@@ -18,7 +19,6 @@
 #include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
 #include "chrome/browser/ui/views/tabs/glic_button.h"
 #include "chrome/browser/ui/views/tabs/tab_strip_action_container.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
 #include "chrome/common/webui_url_constants.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_delegate.h"
@@ -114,9 +114,9 @@
 
     gfx::Point mouse_location = event_monitor_->GetLastMouseLocation();
     views::View::ConvertPointFromScreen(glic_view_, &mouse_location);
-    if (event.type() == ui::EventType::kMousePressed &&
-        glic_view_->IsPointWithinDraggableArea(mouse_location)) {
-      mouse_down_in_draggable_area_ = true;
+    if (event.type() == ui::EventType::kMousePressed) {
+      mouse_down_in_draggable_area_ =
+          glic_view_->IsPointWithinDraggableArea(mouse_location);
     }
 
     if (event.type() == ui::EventType::kMouseReleased ||
diff --git a/chrome/browser/glic/glic_window_controller.h b/chrome/browser/glic/glic_window_controller.h
index 605a011..cf1c3a7 100644
--- a/chrome/browser/glic/glic_window_controller.h
+++ b/chrome/browser/glic/glic_window_controller.h
@@ -10,10 +10,10 @@
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
 #include "base/observer_list_types.h"
+#include "chrome/browser/glic/glic.mojom.h"
 #include "chrome/browser/glic/glic_web_client_access.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
-#include "chrome/browser/ui/webui/glic/glic.mojom.h"
 #include "content/public/browser/web_contents.h"
 #include "ui/views/widget/unique_widget_ptr.h"
 
diff --git a/chrome/browser/glic/glic_window_controller_ui_test.cc b/chrome/browser/glic/glic_window_controller_ui_test.cc
index c41415b..9523031 100644
--- a/chrome/browser/glic/glic_window_controller_ui_test.cc
+++ b/chrome/browser/glic/glic_window_controller_ui_test.cc
@@ -62,40 +62,31 @@
     return glic_service()->window_controller();
   }
 
-  glic::GlicView* glic_view() { return window_controller().GetGlicView(); }
-
  private:
   base::test::ScopedFeatureList features_;
 };
 
 IN_PROC_BROWSER_TEST_F(GlicWindowControllerTest, DoNotCrashOnBrowserClose) {
-  RunTestSequence(PressButton(kGlicButtonElementId));
-
-  RunTestSequence(
-      InContext(views::ElementTrackerViews::GetContextForView(glic_view()),
-                MoveMouseTo(kGlicViewElementId)),
-      InContext(views::ElementTrackerViews::GetContextForView(glic_view()),
-                ActivateSurface(kGlicViewElementId)));
+  RunTestSequence(PressButton(kGlicButtonElementId),
+                  InAnyContext(WaitForShow(kGlicViewElementId)),
+                  InSameContext(Steps(MoveMouseTo(kGlicViewElementId),
+                                      ActivateSurface(kGlicViewElementId))));
   chrome::CloseAllBrowsers();
   ui_test_utils::WaitForBrowserToClose();
 }
 
 IN_PROC_BROWSER_TEST_F(GlicWindowControllerTest, DoNotCrashWhenReopening) {
-  RunTestSequence(PressButton(kGlicButtonElementId));
   RunTestSequence(
-      InContext(views::ElementTrackerViews::GetContextForView(glic_view()),
-                MoveMouseTo(kGlicViewElementId)),
-      InContext(views::ElementTrackerViews::GetContextForView(glic_view()),
-                ActivateSurface(kGlicViewElementId)));
-
-  window_controller().Close();
-
-  RunTestSequence(PressButton(kGlicButtonElementId));
-  RunTestSequence(
-      InContext(views::ElementTrackerViews::GetContextForView(glic_view()),
-                MoveMouseTo(kGlicViewElementId)),
-      InContext(views::ElementTrackerViews::GetContextForView(glic_view()),
-                ActivateSurface(kGlicViewElementId)));
+      PressButton(kGlicButtonElementId),
+      // TODO(crbug.com/389729273): observe web client initialization directly.
+      InAnyContext(WaitForShow(kGlicViewElementId)),
+      InSameContext(Steps(MoveMouseTo(kGlicViewElementId),
+                          ActivateSurface(kGlicViewElementId))),
+      Do([this]() { window_controller().Close(); }),
+      PressButton(kGlicButtonElementId),
+      InAnyContext(WaitForShow(kGlicViewElementId)),
+      InSameContext(Steps(MoveMouseTo(kGlicViewElementId),
+                          ActivateSurface(kGlicViewElementId))));
 }
 
 }  // namespace
diff --git a/chrome/browser/glic/launcher/glic_status_icon.cc b/chrome/browser/glic/launcher/glic_status_icon.cc
index 9da6612..f5c01a28 100644
--- a/chrome/browser/glic/launcher/glic_status_icon.cc
+++ b/chrome/browser/glic/launcher/glic_status_icon.cc
@@ -45,6 +45,7 @@
   if (features::kGlicStatusIconOpenMenuWithSecondaryClick.Get()) {
     status_icon_->SetOpenMenuWithSecondaryClick(true);
   }
+  status_icon_->SetImageTemplate(true);
 #endif
   status_icon_->AddObserver(this);
 
diff --git a/chrome/browser/headless/headless_mode_protocol_browsertest.cc b/chrome/browser/headless/headless_mode_protocol_browsertest.cc
index 310c77af..8468de2 100644
--- a/chrome/browser/headless/headless_mode_protocol_browsertest.cc
+++ b/chrome/browser/headless/headless_mode_protocol_browsertest.cc
@@ -310,4 +310,7 @@
 HEADLESS_MODE_PROTOCOL_TEST(CreateTargetPosition,
                             "sanity/create-target-position.js")
 
+HEADLESS_MODE_PROTOCOL_TEST(CreateTargetWindowState,
+                            "sanity/create-target-window-state.js")
+
 }  // namespace headless
diff --git a/chrome/browser/headless/test/data/protocol/sanity/create-target-window-state-expected.txt b/chrome/browser/headless/test/data/protocol/sanity/create-target-window-state-expected.txt
new file mode 100644
index 0000000..f096d356
--- /dev/null
+++ b/chrome/browser/headless/test/data/protocol/sanity/create-target-window-state-expected.txt
@@ -0,0 +1,5 @@
+Tests Target.createTarget() window state handling.
+Expected: normal, actual: normal
+Expected: maximized, actual: maximized
+Expected: minimized, actual: minimized
+Expected: fullscreen, actual: fullscreen
\ No newline at end of file
diff --git a/chrome/browser/headless/test/data/protocol/sanity/create-target-window-state.js b/chrome/browser/headless/test/data/protocol/sanity/create-target-window-state.js
new file mode 100644
index 0000000..18babd46
--- /dev/null
+++ b/chrome/browser/headless/test/data/protocol/sanity/create-target-window-state.js
@@ -0,0 +1,24 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+(async function(testRunner) {
+  const {session, dp} = await testRunner.startBlank(
+      `Tests Target.createTarget() window state handling.`);
+
+  async function tryCreateTarget(windowState) {
+    const {targetId} =
+        (await session.protocol.Target.createTarget(
+             {'url': 'about:blank', windowState, 'newWindow': true}))
+            .result;
+
+    const {bounds} = (await dp.Browser.getWindowForTarget({targetId})).result;
+    testRunner.log(`Expected: ${windowState}, actual: ${bounds.windowState}`);
+  }
+
+  await tryCreateTarget('normal');
+  await tryCreateTarget('maximized');
+  await tryCreateTarget('minimized');
+  await tryCreateTarget('fullscreen');
+
+  testRunner.completeTest();
+})
diff --git a/chrome/browser/media/chromeos_login_and_lock_media_access_handler.cc b/chrome/browser/media/chromeos_login_and_lock_media_access_handler.cc
index 1681e01..9d6ec81 100644
--- a/chrome/browser/media/chromeos_login_and_lock_media_access_handler.cc
+++ b/chrome/browser/media/chromeos_login_and_lock_media_access_handler.cc
@@ -13,7 +13,9 @@
 #include "chromeos/ash/components/settings/cros_settings.h"
 #include "chromeos/ash/components/settings/cros_settings_names.h"
 #include "components/content_settings/core/common/content_settings_pattern.h"
+#include "components/guest_view/browser/guest_view_base.h"
 #include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
 #include "url/gurl.h"
 
 ChromeOSLoginAndLockMediaAccessHandler::
@@ -23,9 +25,12 @@
     ~ChromeOSLoginAndLockMediaAccessHandler() = default;
 
 bool ChromeOSLoginAndLockMediaAccessHandler::SupportsStreamType(
-    content::WebContents* web_contents,
+    content::RenderFrameHost* render_frame_host,
     const blink::mojom::MediaStreamType type,
     const extensions::Extension* extension) {
+  auto* web_contents = guest_view::GuestViewBase::GetTopLevelWebContents(
+      content::WebContents::FromRenderFrameHost(render_frame_host));
+
   if (!web_contents)
     return false;
   // Check if the `web_contents` corresponds to the login screen.
diff --git a/chrome/browser/media/chromeos_login_and_lock_media_access_handler.h b/chrome/browser/media/chromeos_login_and_lock_media_access_handler.h
index 3b08b94..c1fc55c1 100644
--- a/chrome/browser/media/chromeos_login_and_lock_media_access_handler.h
+++ b/chrome/browser/media/chromeos_login_and_lock_media_access_handler.h
@@ -15,7 +15,7 @@
   ~ChromeOSLoginAndLockMediaAccessHandler() override;
 
   // MediaAccessHandler implementation.
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType type,
                           const extensions::Extension* extension) override;
   bool CheckMediaAccessPermission(
diff --git a/chrome/browser/media/extension_media_access_handler.cc b/chrome/browser/media/extension_media_access_handler.cc
index 5692957f..8946078 100644
--- a/chrome/browser/media/extension_media_access_handler.cc
+++ b/chrome/browser/media/extension_media_access_handler.cc
@@ -47,7 +47,7 @@
 ExtensionMediaAccessHandler::~ExtensionMediaAccessHandler() = default;
 
 bool ExtensionMediaAccessHandler::SupportsStreamType(
-    content::WebContents* web_contents,
+    content::RenderFrameHost* render_frame_host,
     const blink::mojom::MediaStreamType type,
     const extensions::Extension* extension) {
   return extension &&
diff --git a/chrome/browser/media/extension_media_access_handler.h b/chrome/browser/media/extension_media_access_handler.h
index 88c12d08..138dc5a 100644
--- a/chrome/browser/media/extension_media_access_handler.h
+++ b/chrome/browser/media/extension_media_access_handler.h
@@ -14,7 +14,7 @@
   ~ExtensionMediaAccessHandler() override;
 
   // MediaAccessHandler implementation.
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType type,
                           const extensions::Extension* extension) override;
   bool CheckMediaAccessPermission(
diff --git a/chrome/browser/media/media_access_handler.h b/chrome/browser/media/media_access_handler.h
index 11d450e4..04abff2 100644
--- a/chrome/browser/media/media_access_handler.h
+++ b/chrome/browser/media/media_access_handler.h
@@ -27,7 +27,7 @@
   virtual ~MediaAccessHandler() = default;
 
   // Check if the media stream type is supported by MediaAccessHandler.
-  virtual bool SupportsStreamType(content::WebContents* web_contents,
+  virtual bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                                   const blink::mojom::MediaStreamType type,
                                   const extensions::Extension* extension) = 0;
 
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler.cc b/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
index 6511054..babd6409 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
@@ -310,7 +310,7 @@
 }
 
 bool DesktopCaptureAccessHandler::SupportsStreamType(
-    content::WebContents* web_contents,
+    content::RenderFrameHost* render_frame_host,
     const blink::mojom::MediaStreamType type,
     const extensions::Extension* extension) {
   return type == blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE ||
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler.h b/chrome/browser/media/webrtc/desktop_capture_access_handler.h
index 5448276f7..cb2ca13 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler.h
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler.h
@@ -50,7 +50,7 @@
   ~DesktopCaptureAccessHandler() override;
 
   // MediaAccessHandler implementation.
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType type,
                           const extensions::Extension* extension) override;
   bool CheckMediaAccessPermission(
diff --git a/chrome/browser/media/webrtc/display_media_access_handler.cc b/chrome/browser/media/webrtc/display_media_access_handler.cc
index 540989ef..2b1abef4 100644
--- a/chrome/browser/media/webrtc/display_media_access_handler.cc
+++ b/chrome/browser/media/webrtc/display_media_access_handler.cc
@@ -99,7 +99,7 @@
 DisplayMediaAccessHandler::~DisplayMediaAccessHandler() = default;
 
 bool DisplayMediaAccessHandler::SupportsStreamType(
-    content::WebContents* web_contents,
+    content::RenderFrameHost* render_frame_host,
     const blink::mojom::MediaStreamType stream_type,
     const extensions::Extension* extension) {
   return stream_type == blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE ||
diff --git a/chrome/browser/media/webrtc/display_media_access_handler.h b/chrome/browser/media/webrtc/display_media_access_handler.h
index 455e932..6bec565 100644
--- a/chrome/browser/media/webrtc/display_media_access_handler.h
+++ b/chrome/browser/media/webrtc/display_media_access_handler.h
@@ -41,7 +41,7 @@
   ~DisplayMediaAccessHandler() override;
 
   // MediaAccessHandler implementation.
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType stream_type,
                           const extensions::Extension* extension) override;
   bool CheckMediaAccessPermission(
diff --git a/chrome/browser/media/webrtc/media_capture_devices_dispatcher.cc b/chrome/browser/media/webrtc/media_capture_devices_dispatcher.cc
index 46adc85..3a84af8 100644
--- a/chrome/browser/media/webrtc/media_capture_devices_dispatcher.cc
+++ b/chrome/browser/media/webrtc/media_capture_devices_dispatcher.cc
@@ -68,18 +68,6 @@
 using content::BrowserThread;
 using content::MediaCaptureDevices;
 
-namespace {
-
-content::WebContents* WebContentsFromIds(int render_process_id,
-                                         int render_frame_id) {
-  content::WebContents* web_contents =
-      content::WebContents::FromRenderFrameHost(
-          content::RenderFrameHost::FromID(render_process_id, render_frame_id));
-  return web_contents;
-}
-
-}  // namespace
-
 MediaCaptureDevicesDispatcher* MediaCaptureDevicesDispatcher::GetInstance() {
   return base::Singleton<MediaCaptureDevicesDispatcher>::get();
 }
@@ -158,10 +146,13 @@
   }
 #endif
 
+  auto* render_frame_host = content::RenderFrameHost::FromID(
+      request.render_process_id, request.render_frame_id);
+
   for (const auto& handler : media_access_handlers_) {
-    if (handler->SupportsStreamType(web_contents, request.video_type,
+    if (handler->SupportsStreamType(render_frame_host, request.video_type,
                                     extension) ||
-        handler->SupportsStreamType(web_contents, request.audio_type,
+        handler->SupportsStreamType(render_frame_host, request.audio_type,
                                     extension)) {
       handler->HandleRequest(web_contents, request, std::move(callback),
                              extension);
@@ -200,9 +191,7 @@
     const extensions::Extension* extension) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   for (const auto& handler : media_access_handlers_) {
-    if (handler->SupportsStreamType(
-            content::WebContents::FromRenderFrameHost(render_frame_host), type,
-            extension)) {
+    if (handler->SupportsStreamType(render_frame_host, type, extension)) {
       return handler->CheckMediaAccessPermission(
           render_frame_host, security_origin, type, extension);
     }
@@ -349,9 +338,9 @@
     content::MediaRequestState state) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   for (const auto& handler : media_access_handlers_) {
-    if (handler->SupportsStreamType(
-            WebContentsFromIds(render_process_id, render_frame_id), stream_type,
-            nullptr)) {
+    if (handler->SupportsStreamType(content::RenderFrameHost::FromID(
+                                        render_process_id, render_frame_id),
+                                    stream_type, nullptr)) {
       handler->UpdateMediaRequestState(render_process_id, render_frame_id,
                                        page_request_id, stream_type, state);
       break;
@@ -424,9 +413,9 @@
   DCHECK(blink::IsVideoScreenCaptureMediaType(stream_type));
 
   for (const auto& handler : media_access_handlers_) {
-    if (handler->SupportsStreamType(
-            WebContentsFromIds(render_process_id, render_frame_id), stream_type,
-            nullptr)) {
+    if (handler->SupportsStreamType(content::RenderFrameHost::FromID(
+                                        render_process_id, render_frame_id),
+                                    stream_type, nullptr)) {
       handler->UpdateVideoScreenCaptureStatus(
           render_process_id, render_frame_id, page_request_id, is_secure);
       break;
diff --git a/chrome/browser/media/webrtc/media_capture_devices_dispatcher_unittest.cc b/chrome/browser/media/webrtc/media_capture_devices_dispatcher_unittest.cc
index 3f5fa90c..745af70 100644
--- a/chrome/browser/media/webrtc/media_capture_devices_dispatcher_unittest.cc
+++ b/chrome/browser/media/webrtc/media_capture_devices_dispatcher_unittest.cc
@@ -25,7 +25,7 @@
       const blink::mojom::MediaStreamType supported_type)
       : supported_type_(supported_type) {}
 
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType type,
                           const extensions::Extension* extension) override {
     return supported_type_ == type;
diff --git a/chrome/browser/media/webrtc/permission_bubble_media_access_handler.cc b/chrome/browser/media/webrtc/permission_bubble_media_access_handler.cc
index 93b0c32f..9e8c846 100644
--- a/chrome/browser/media/webrtc/permission_bubble_media_access_handler.cc
+++ b/chrome/browser/media/webrtc/permission_bubble_media_access_handler.cc
@@ -131,7 +131,7 @@
     default;
 
 bool PermissionBubbleMediaAccessHandler::SupportsStreamType(
-    content::WebContents* web_contents,
+    content::RenderFrameHost* render_frame_host,
     const blink::mojom::MediaStreamType type,
     const extensions::Extension* extension) {
   return type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
diff --git a/chrome/browser/media/webrtc/permission_bubble_media_access_handler.h b/chrome/browser/media/webrtc/permission_bubble_media_access_handler.h
index ec7e7fb..9e8147e 100644
--- a/chrome/browser/media/webrtc/permission_bubble_media_access_handler.h
+++ b/chrome/browser/media/webrtc/permission_bubble_media_access_handler.h
@@ -29,7 +29,7 @@
   ~PermissionBubbleMediaAccessHandler() override;
 
   // MediaAccessHandler implementation.
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType type,
                           const extensions::Extension* extension) override;
   bool CheckMediaAccessPermission(
diff --git a/chrome/browser/media/webrtc/tab_capture_access_handler.cc b/chrome/browser/media/webrtc/tab_capture_access_handler.cc
index 18f51c60..f3cb86c 100644
--- a/chrome/browser/media/webrtc/tab_capture_access_handler.cc
+++ b/chrome/browser/media/webrtc/tab_capture_access_handler.cc
@@ -99,7 +99,7 @@
 TabCaptureAccessHandler::~TabCaptureAccessHandler() = default;
 
 bool TabCaptureAccessHandler::SupportsStreamType(
-    content::WebContents* web_contents,
+    content::RenderFrameHost* render_frame_host,
     const blink::mojom::MediaStreamType type,
     const extensions::Extension* extension) {
   return type == blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE ||
diff --git a/chrome/browser/media/webrtc/tab_capture_access_handler.h b/chrome/browser/media/webrtc/tab_capture_access_handler.h
index 178cec9..e91fd234 100644
--- a/chrome/browser/media/webrtc/tab_capture_access_handler.h
+++ b/chrome/browser/media/webrtc/tab_capture_access_handler.h
@@ -20,7 +20,7 @@
   ~TabCaptureAccessHandler() override;
 
   // MediaAccessHandler implementation.
-  bool SupportsStreamType(content::WebContents* web_contents,
+  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                           const blink::mojom::MediaStreamType type,
                           const extensions::Extension* extension) override;
   bool CheckMediaAccessPermission(
diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 3537e020..1aa443f 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -779,7 +779,7 @@
   if (password_change_service &&
       password_change_service->GetPasswordChangeDelegate(web_contents())) {
     password_change_service->GetPasswordChangeDelegate(web_contents())
-        ->SuccessfulSubmissionDetected(web_contents());
+        ->OnPasswordFormSubmission(web_contents());
   }
 #endif  // BUILDFLAG(IS_ANDROID)
 }
diff --git a/chrome/browser/password_manager/password_change_browsertest.cc b/chrome/browser/password_manager/password_change_browsertest.cc
index 82516ca..bdde248 100644
--- a/chrome/browser/password_manager/password_change_browsertest.cc
+++ b/chrome/browser/password_manager/password_change_browsertest.cc
@@ -2,9 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <utility>
+
 #include "base/callback_list.h"
+#include "base/test/gmock_callback_support.h"
 #include "base/test/run_until.h"
 #include "chrome/browser/affiliations/affiliation_service_factory.h"
+#include "chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
 #include "chrome/browser/password_manager/chrome_password_change_service.h"
 #include "chrome/browser/password_manager/password_change_delegate_impl.h"
 #include "chrome/browser/password_manager/password_change_service_factory.h"
@@ -21,6 +27,9 @@
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/affiliations/core/browser/mock_affiliation_service.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/optimization_guide/core/mock_optimization_guide_model_executor.h"
+#include "components/optimization_guide/core/optimization_guide_proto_util.h"
+#include "components/password_manager/core/browser/password_store/test_password_store.h"
 #include "components/password_manager/core/common/password_manager_pref_names.h"
 #include "components/prefs/pref_service.h"
 #include "components/url_formatter/elide_url.h"
@@ -31,6 +40,15 @@
 
 using affiliations::AffiliationService;
 using affiliations::MockAffiliationService;
+using PasswordChangeOutcome = ::optimization_guide::proto::
+    PasswordChangeSubmissionData_PasswordChangeOutcome;
+using ::testing::_;
+using ::testing::An;
+using ::testing::Contains;
+using ::testing::Invoke;
+using ::testing::NiceMock;
+using ::testing::Return;
+using ::testing::WithArg;
 
 namespace {
 
@@ -52,6 +70,12 @@
   return std::make_unique<testing::NiceMock<MockAffiliationService>>();
 }
 
+std::unique_ptr<KeyedService> CreateOptimizationService(
+    content::BrowserContext* context) {
+  return std::make_unique<
+      testing::NiceMock<MockOptimizationGuideKeyedService>>();
+}
+
 content::WebContents* OpenNewTabInBackground(base::WeakPtr<Browser> browser,
                                              const GURL& url,
                                              content::WebContents*) {
@@ -81,6 +105,10 @@
                   AffiliationServiceFactory::GetInstance()->SetTestingFactory(
                       context,
                       base::BindRepeating(&CreateTestAffiliationService));
+                  OptimizationGuideKeyedServiceFactory::GetInstance()
+                      ->SetTestingFactory(
+                          context,
+                          base::BindRepeating(&CreateOptimizationService));
                 }));
   }
 
@@ -98,9 +126,14 @@
         AffiliationServiceFactory::GetForProfile(browser()->profile()));
   }
 
+  MockOptimizationGuideKeyedService* mock_optimization_guide_keyed_service() {
+    return static_cast<MockOptimizationGuideKeyedService*>(
+        OptimizationGuideKeyedServiceFactory::GetForProfile(
+            browser()->profile()));
+  }
+
   ChromePasswordChangeService* password_change_service() {
     return PasswordChangeServiceFactory::GetForProfile(browser()->profile());
-    ;
   }
 
   // This allows to attach a custom ManagePasswordsUIController to intercept UI
@@ -112,6 +145,54 @@
         base::BindRepeating(&OpenNewTabInBackground, browser()->AsWeakPtr()));
   }
 
+  void MockPasswordChangeOutcome(PasswordChangeOutcome outcome) {
+    optimization_guide::proto::PasswordChangeResponse response;
+    response.mutable_outcome_data()->set_submission_outcome(outcome);
+
+    EXPECT_CALL(*mock_optimization_guide_keyed_service(),
+                ExecuteModel(optimization_guide::ModelBasedCapabilityKey::
+                                 kPasswordChangeSubmission,
+                             _, _, _))
+        .WillOnce(WithArg<3>(Invoke([response](auto callback) {
+          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+              FROM_HERE,
+              base::BindOnce(
+                  std::move(callback),
+                  optimization_guide::OptimizationGuideModelExecutionResult(
+                      optimization_guide::AnyWrapProto(response),
+                      /*execution_info=*/nullptr),
+                  /*log_entry=*/nullptr));
+        })));
+  }
+
+  void CheckPasswordsSavedOnFailure(const std::string& username,
+                                    const std::string& new_password) {
+    scoped_refptr<password_manager::TestPasswordStore> password_store =
+        static_cast<password_manager::TestPasswordStore*>(
+            ProfilePasswordStoreFactory::GetForProfile(
+                browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
+                .get());
+    const std::vector<password_manager::PasswordForm>& passwords_vector =
+        password_store->stored_passwords().begin()->second;
+    // Check if |username| + |new password| is stored
+    bool found_username_with_new_password = false;
+    // Check if |empty username| + |new password| is stored
+    bool found_empty_username_with_new_password = false;
+
+    for (const auto& form : passwords_vector) {
+      if (form.username_value == base::ASCIIToUTF16(username) &&
+          form.password_value == base::ASCIIToUTF16(new_password)) {
+        found_username_with_new_password = true;
+      } else if (form.username_value.empty() &&
+                 form.password_value == base::ASCIIToUTF16(new_password)) {
+        found_empty_username_with_new_password = true;
+      }
+    }
+
+    EXPECT_FALSE(found_username_with_new_password);
+    EXPECT_TRUE(found_empty_username_with_new_password);
+  }
+
  private:
   base::CallbackListSubscription create_services_subscription_;
   base::WeakPtrFactory<PasswordChangeBrowserTest> weak_ptr_factory_{this};
@@ -328,20 +409,24 @@
   auto generated_password = base::UTF16ToUTF8(delegate->GetGeneratedPassword());
   EXPECT_EQ(generated_password,
             GetElementValue(/*iframe_id=*/"null", "new_password_1"));
-
+  MockPasswordChangeOutcome(
+      PasswordChangeOutcome::
+          PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
   // Emulate a navigation as an indication of successful submission.
   PasswordsNavigationObserver new_page_observer(WebContents());
   ASSERT_TRUE(ui_test_utils::NavigateToURL(
       browser(),
       embedded_test_server()->GetURL("example.com", "/password/done.html")));
   EXPECT_TRUE(new_page_observer.Wait());
-
-  // Verify generated password is saved.
-  WaitForPasswordStore();
-  CheckThatCredentialsStored("test", generated_password);
   // Verify the success state.
   ASSERT_EQ(delegate->GetCurrentState(),
             PasswordChangeDelegate::State::kPasswordSuccessfullyChanged);
+  // Verify the success state.
+  ASSERT_EQ(delegate->GetCurrentState(),
+            PasswordChangeDelegate::State::kPasswordSuccessfullyChanged);
+  // Verify generated password is saved.
+  WaitForPasswordStore();
+  CheckThatCredentialsStored(/*username=*/"test", generated_password);
 }
 
 IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, OldPasswordIsUpdated) {
@@ -372,10 +457,12 @@
   PasswordsNavigationObserver password_change_page_observer(WebContents());
   EXPECT_TRUE(password_change_page_observer.Wait());
   WaitForElementValue("password", "pa$$word");
-
   std::string new_password =
       GetElementValue(/*iframe_id=*/"null", "new_password_1");
 
+  MockPasswordChangeOutcome(
+      PasswordChangeOutcome::
+          PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
   // Emulate a navigation as an indication of successful submission.
   PasswordsNavigationObserver new_page_observer(WebContents());
   ASSERT_TRUE(ui_test_utils::NavigateToURL(
@@ -390,6 +477,62 @@
 }
 
 IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
+                       PasswordChangeSubmissionFailed) {
+  SetPrivacyNoticeAcceptedPref();
+  password_manager::PasswordStoreInterface* password_store =
+      ProfilePasswordStoreFactory::GetForProfile(
+          browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
+          .get();
+  GURL origin = embedded_test_server()->GetURL("example.com", "/");
+  password_manager::PasswordForm form;
+  form.signon_realm = origin.spec();
+  form.url = origin;
+  form.username_value = u"test";
+  form.password_value = u"pa$$word";
+  password_store->AddLogin(form);
+  WaitForPasswordStore();
+
+  EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(origin))
+      .WillOnce(testing::Return(embedded_test_server()->GetURL(
+          "example.com", "/password/update_form_empty_fields.html")));
+
+  password_change_service()->StartPasswordChange(
+      origin, form.username_value, form.password_value, WebContents());
+  // Activate tab with password change to simplify testing.
+  SetWebContents(browser()->tab_strip_model()->GetWebContentsAt(1));
+
+  PasswordsNavigationObserver password_change_page_observer(WebContents());
+  EXPECT_TRUE(password_change_page_observer.Wait());
+  WaitForElementValue("password", "pa$$word");
+
+  MockPasswordChangeOutcome(
+      PasswordChangeOutcome::
+          PasswordChangeSubmissionData_PasswordChangeOutcome_UNSUCCESSFUL_OUTCOME);
+
+  std::string new_password =
+      GetElementValue(/*iframe_id=*/"null", "new_password_1");
+
+  // Emulate a navigation as an indication of successful submission.
+  PasswordsNavigationObserver new_page_observer(WebContents());
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(),
+      embedded_test_server()->GetURL("example.com", "/password/done.html")));
+  EXPECT_TRUE(new_page_observer.Wait());
+
+  // Verify state is updated to failure and new password is not stored.
+  auto* delegate = password_change_service()->GetPasswordChangeDelegate(
+      browser()->tab_strip_model()->GetWebContentsAt(0));
+  ASSERT_TRUE(delegate);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return delegate->GetCurrentState() ==
+           PasswordChangeDelegate::State::kPasswordChangeFailed;
+  }));
+  WaitForPasswordStore();
+  CheckPasswordsSavedOnFailure(base::UTF16ToUTF8(form.username_value),
+                               new_password);
+}
+
+IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
                        SignInCheckBubgehbleIsHiddenWhenStateIsUpdated) {
   SetPrivacyNoticeAcceptedPref();
   GURL main_url("https://example.com/");
diff --git a/chrome/browser/password_manager/password_change_delegate.h b/chrome/browser/password_manager/password_change_delegate.h
index a0361c9..0439798 100644
--- a/chrome/browser/password_manager/password_change_delegate.h
+++ b/chrome/browser/password_manager/password_change_delegate.h
@@ -66,10 +66,8 @@
   // doesn't exist anymore.
   virtual void OpenPasswordChangeTab() = 0;
 #endif
-
-  // Informs delegate about successful form submission.
-  virtual void SuccessfulSubmissionDetected(
-      content::WebContents* web_contents) = 0;
+  // To be executed after a password form was submitted
+  virtual void OnPasswordFormSubmission(content::WebContents* web_contents) = 0;
 
   virtual void OnPrivacyNoticeAccepted() = 0;
 
diff --git a/chrome/browser/password_manager/password_change_delegate_impl.cc b/chrome/browser/password_manager/password_change_delegate_impl.cc
index 6d64c72..47ba981 100644
--- a/chrome/browser/password_manager/password_change_delegate_impl.cc
+++ b/chrome/browser/password_manager/password_change_delegate_impl.cc
@@ -4,8 +4,16 @@
 
 #include "chrome/browser/password_manager/password_change_delegate_impl.h"
 
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
 #include "chrome/browser/password_manager/chrome_password_manager_client.h"
 #include "chrome/browser/profiles/profile.h"
+#include "components/optimization_guide/core/model_quality/model_execution_logging_wrappers.h"
+#include "components/optimization_guide/core/model_quality/model_quality_log_entry.h"
+#include "components/optimization_guide/core/optimization_guide_features.h"
+#include "components/optimization_guide/core/optimization_guide_model_executor.h"
+#include "components/optimization_guide/core/optimization_guide_proto_util.h"
+#include "components/optimization_guide/proto/model_execution.pb.h"
 #include "components/password_manager/content/browser/content_password_manager_driver.h"
 #include "components/password_manager/core/browser/generation/password_generator.h"
 #include "components/password_manager/core/browser/password_form_manager.h"
@@ -16,6 +24,7 @@
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/referrer.h"
+#include "ui/accessibility/ax_tree_update.h"
 #include "ui/base/window_open_disposition.h"
 #include "url/gurl.h"
 
@@ -30,6 +39,12 @@
 using password_manager::PasswordForm;
 using password_manager::PasswordFormCache;
 using password_manager::PasswordFormManager;
+using PasswordChangeOutcome = optimization_guide::proto ::
+    PasswordChangeSubmissionData_PasswordChangeOutcome;
+using ProtoTreeUpdate = optimization_guide::proto::AXTreeUpdate;
+
+// Max numbers of nodes for the AX Tree Update Snapshot.
+constexpr int kMaxNodesInAXTreeSnapshot = 5000;
 
 PasswordFormCache& GetFormCache(content::WebContents* web_contents) {
   auto* client = static_cast<password_manager::PasswordManagerClient*>(
@@ -152,6 +167,68 @@
                     this);
 }
 
+void PasswordChangeDelegateImpl::OnPasswordFormSubmission(
+    content::WebContents* web_contents) {
+  if (executor_ && executor_.get() == web_contents && form_manager_ &&
+      !submission_detected_) {
+    submission_detected_ = true;
+    web_contents->RequestAXTreeSnapshot(
+        base::BindOnce(&PasswordChangeDelegateImpl::ProcessTree,
+                       weak_ptr_factory_.GetWeakPtr()),
+        ui::AXMode::kWebContents, kMaxNodesInAXTreeSnapshot,
+        /* timeout= */ {}, content::WebContents::AXTreeSnapshotPolicy::kAll);
+  }
+}
+
+void PasswordChangeDelegateImpl::ProcessTree(ui::AXTreeUpdate& ax_tree_update) {
+  ProtoTreeUpdate ax_tree_proto;
+  optimization_guide::PopulateAXTreeUpdateProto(ax_tree_update, &ax_tree_proto);
+  // Construct request.
+  optimization_guide::proto::PasswordChangeRequest request;
+  optimization_guide::proto::PageContext* page_context =
+      request.mutable_page_context();
+  *page_context->mutable_ax_tree_data() = std::move(ax_tree_proto);
+
+  OptimizationGuideKeyedService* optimization_executor =
+      OptimizationGuideKeyedServiceFactory::GetForProfile(
+          Profile::FromBrowserContext(executor_->GetBrowserContext()));
+  optimization_executor->ExecuteModel(
+      optimization_guide::ModelBasedCapabilityKey::kPasswordChangeSubmission,
+      request,
+      /*execution_timeout=*/std::nullopt,
+      base::BindOnce(&PasswordChangeDelegateImpl::OnExecutionResponseCallback,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PasswordChangeDelegateImpl::OnExecutionResponseCallback(
+    optimization_guide::OptimizationGuideModelExecutionResult execution_result,
+    std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry) {
+  if (!execution_result.response.has_value()) {
+    UpdateState(State::kPasswordChangeFailed);
+    return;
+  }
+  std::optional<optimization_guide::proto::PasswordChangeResponse> response =
+      optimization_guide::ParsedAnyMetadata<
+          optimization_guide::proto::PasswordChangeResponse>(
+          execution_result.response.value());
+  if (!response) {
+    UpdateState(State::kPasswordChangeFailed);
+    return;
+  }
+  PasswordChangeOutcome outcome =
+      response.value().outcome_data().submission_outcome();
+  if (outcome ==
+          PasswordChangeOutcome::
+              PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME ||
+      outcome ==
+          PasswordChangeOutcome::
+              PasswordChangeSubmissionData_PasswordChangeOutcome_UNKNOWN_OUTCOME) {
+    SuccessfulSubmissionDetected();
+  } else {
+    UpdateState(State::kPasswordChangeFailed);
+  }
+}
+
 #if !BUILDFLAG(IS_ANDROID)
 void PasswordChangeDelegateImpl::OpenPasswordChangeTab() {
   if (executor_) {
@@ -166,11 +243,8 @@
 }
 #endif
 
-void PasswordChangeDelegateImpl::SuccessfulSubmissionDetected(
-    content::WebContents* web_contents) {
-  if (executor_ && executor_.get() == web_contents && form_manager_) {
-    // TODO(crbug.com/377878716): Do it only after verification of successful
-    // update.
+void PasswordChangeDelegateImpl::SuccessfulSubmissionDetected() {
+  if (form_manager_) {
     form_manager_->OnUpdateUsernameFromPrompt(username_);
     form_manager_->Save();
     UpdateState(State::kPasswordSuccessfullyChanged);
diff --git a/chrome/browser/password_manager/password_change_delegate_impl.h b/chrome/browser/password_manager/password_change_delegate_impl.h
index 5de40702..3807744 100644
--- a/chrome/browser/password_manager/password_change_delegate_impl.h
+++ b/chrome/browser/password_manager/password_change_delegate_impl.h
@@ -12,8 +12,11 @@
 #include "base/observer_list.h"
 #include "chrome/browser/password_manager/password_change_delegate.h"
 #include "components/autofill/core/common/form_data.h"
+#include "components/optimization_guide/core/model_quality/model_quality_log_entry.h"
+#include "components/optimization_guide/core/optimization_guide_model_executor.h"
 #include "components/password_manager/core/browser/password_form_cache.h"
 #include "content/public/browser/web_contents_observer.h"
+#include "ui/accessibility/ax_tree_update.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -56,9 +59,14 @@
 #if !BUILDFLAG(IS_ANDROID)
   void OpenPasswordChangeTab() override;
 #endif
-  void SuccessfulSubmissionDetected(
-      content::WebContents* web_contents) override;
+  void SuccessfulSubmissionDetected();
+  void OnPasswordFormSubmission(content::WebContents* web_contents) override;
+  void ProcessTree(ui::AXTreeUpdate& ax_tree_update);
   void OnPrivacyNoticeAccepted() override;
+  void OnExecutionResponseCallback(
+      optimization_guide::OptimizationGuideModelExecutionResult
+          execution_result,
+      std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry);
   void AddObserver(Observer* observer) override;
   void RemoveObserver(Observer* observer) override;
   std::u16string GetDisplayOrigin() const override;
@@ -85,6 +93,7 @@
   const GURL change_password_url_;
   const std::u16string username_;
   const std::u16string original_password_;
+  bool submission_detected_ = false;
 
   std::u16string generated_password_;
 
diff --git a/chrome/browser/password_manager/password_change_delegate_mock.h b/chrome/browser/password_manager/password_change_delegate_mock.h
index b342c4a..70867a4 100644
--- a/chrome/browser/password_manager/password_change_delegate_mock.h
+++ b/chrome/browser/password_manager/password_change_delegate_mock.h
@@ -31,7 +31,7 @@
   MOCK_METHOD(void, Stop, (), (override));
   MOCK_METHOD(void, OpenPasswordChangeTab, (), (override));
   MOCK_METHOD(void,
-              SuccessfulSubmissionDetected,
+              OnPasswordFormSubmission,
               (content::WebContents*),
               (override));
   MOCK_METHOD(void, OnPrivacyNoticeAccepted, (), (override));
diff --git a/chrome/browser/performance_manager/chrome_browser_main_extra_parts_performance_manager.cc b/chrome/browser/performance_manager/chrome_browser_main_extra_parts_performance_manager.cc
index 2d71cbd..bb2bce3 100644
--- a/chrome/browser/performance_manager/chrome_browser_main_extra_parts_performance_manager.cc
+++ b/chrome/browser/performance_manager/chrome_browser_main_extra_parts_performance_manager.cc
@@ -18,6 +18,7 @@
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/performance_manager/decorators/helpers/page_live_state_decorator_helper.h"
+#include "chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.h"
 #include "chrome/browser/performance_manager/metrics/metrics_provider_desktop.h"
 #include "chrome/browser/performance_manager/observers/page_load_metrics_observer.h"
 #include "chrome/browser/performance_manager/policies/background_tab_loading_policy.h"
@@ -38,6 +39,7 @@
 #include "components/performance_manager/public/decorators/page_live_state_decorator.h"
 #include "components/performance_manager/public/decorators/page_load_tracker_decorator_helper.h"
 #include "components/performance_manager/public/decorators/process_metrics_decorator.h"
+#include "components/performance_manager/public/execution_context_priority/priority_voting_system.h"
 #include "components/performance_manager/public/features.h"
 #include "components/performance_manager/public/graph/graph.h"
 #include "components/performance_manager/public/metrics/page_resource_monitor.h"
@@ -263,6 +265,18 @@
     }
   }
 #endif  // BUILDFLAG(IS_WIN)
+
+#if !BUILDFLAG(IS_ANDROID)
+  if (auto* voting_system = graph->GetRegisteredObjectAs<
+                            performance_manager::execution_context_priority::
+                                PriorityVotingSystem>()) {
+    // Ensures the contents of a Side Panel loads at a high priority, even when
+    // it is not visible.
+    voting_system
+        ->AddPriorityVoter<performance_manager::execution_context_priority::
+                               SidePanelLoadingVoter>();
+  }
+#endif  // !BUILDFLAG(IS_ANDROID)
 }
 
 content::FeatureObserverClient*
diff --git a/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.cc b/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.cc
new file mode 100644
index 0000000..072bc61
--- /dev/null
+++ b/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.cc
@@ -0,0 +1,130 @@
+// Copyright 2025 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/performance_manager/execution_context_priority/side_panel_loading_voter.h"
+
+#include "base/not_fatal_until.h"
+#include "components/performance_manager/public/execution_context/execution_context_registry.h"
+#include "components/performance_manager/public/graph/graph.h"
+#include "url/gurl.h"
+
+namespace performance_manager::execution_context_priority {
+
+namespace {
+
+const execution_context::ExecutionContext* GetExecutionContext(
+    const FrameNode* frame_node) {
+  auto* registry = execution_context::ExecutionContextRegistry::GetFromGraph(
+      frame_node->GetGraph());
+  CHECK(registry);
+  return registry->GetExecutionContextForFrameNode(frame_node);
+}
+
+}  // namespace
+
+// static
+const char SidePanelLoadingVoter::kSidePanelLoadingReason[] =
+    "Side Panel loading";
+
+SidePanelLoadingVoter::SidePanelLoadingVoter() = default;
+
+SidePanelLoadingVoter::~SidePanelLoadingVoter() = default;
+
+void SidePanelLoadingVoter::MarkAsSidePanel(const PageNode* page_node) {
+  CHECK(page_node->GetMainFrameNode());
+
+  if (!page_node->GetMainFrameUrl().is_empty()) {
+    // This is possible for a preloaded Side Panel. The navigation has already
+    // committed and the page is visible.
+    CHECK(page_node->IsVisible(), base::NotFatalUntil::M135);
+    return;
+  }
+
+  bool inserted = side_panel_pages_.insert(page_node).second;
+  CHECK(inserted);
+}
+
+void SidePanelLoadingVoter::InitializeOnGraph(Graph* graph,
+                                              VotingChannel voting_channel) {
+  voting_channel_ = std::move(voting_channel);
+
+  graph->RegisterObject(this);
+  graph->AddPageNodeObserver(this);
+  graph->AddFrameNodeObserver(this);
+}
+
+void SidePanelLoadingVoter::TearDownOnGraph(Graph* graph) {
+  // Clean up outstanding votes, which is possible if the graph is tore down
+  // while a Side Panel is loading.
+  for (const FrameNode* frame_node : frames_with_vote_) {
+    voting_channel_.InvalidateVote(GetExecutionContext(frame_node));
+  }
+  frames_with_vote_.clear();
+
+  graph->RemoveFrameNodeObserver(this);
+  graph->RemovePageNodeObserver(this);
+  graph->UnregisterObject(this);
+
+  voting_channel_.Reset();
+}
+
+void SidePanelLoadingVoter::OnBeforePageNodeRemoved(const PageNode* page_node) {
+  side_panel_pages_.erase(page_node);
+}
+
+void SidePanelLoadingVoter::OnMainFrameDocumentChanged(
+    const PageNode* page_node) {
+  // Check if a navigation committed for a Side Panel. The `page_node` is
+  // removed from the set as we only increase the priority for the initial load.
+  size_t removed = side_panel_pages_.erase(page_node);
+  if (removed) {
+    // A Side Panel just started loading. Increase its priority until it is made
+    // visible.
+    if (!page_node->IsVisible()) {
+      SubmitVoteForPage(page_node);
+    }
+    return;
+  }
+}
+
+void SidePanelLoadingVoter::OnBeforeFrameNodeRemoved(
+    const FrameNode* frame_node) {
+  // Check if a frame with an outstanding vote is being removed.
+  size_t removed = frames_with_vote_.erase(frame_node);
+  if (removed) {
+    voting_channel_.InvalidateVote(GetExecutionContext(frame_node));
+  }
+}
+
+void SidePanelLoadingVoter::OnFrameVisibilityChanged(
+    const FrameNode* frame_node,
+    FrameNode::Visibility previous_value) {
+  // Ignore visibility changed events where the frame is not visible.
+  if (frame_node->GetVisibility() == FrameNode::Visibility::kNotVisible) {
+    return;
+  }
+
+  // Check if a frame with an outstanding vote just became visible.
+  size_t removed = frames_with_vote_.erase(frame_node);
+  if (removed) {
+    // The side panel is visible, no longer need to increase priority.
+    voting_channel_.InvalidateVote(GetExecutionContext(frame_node));
+  }
+}
+
+void SidePanelLoadingVoter::SubmitVoteForPage(const PageNode* page_node) {
+  CHECK(!page_node->IsVisible());
+
+  // We only need to increase the priority of the main frame.
+  const FrameNode* frame_node = page_node->GetMainFrameNode();
+
+  auto [_, inserted] = frames_with_vote_.insert(frame_node);
+  CHECK(inserted);
+
+  voting_channel_.SubmitVote(
+      GetExecutionContext(page_node->GetMainFrameNode()),
+      Vote(base::TaskPriority::USER_BLOCKING, kSidePanelLoadingReason));
+}
+
+}  // namespace performance_manager::execution_context_priority
diff --git a/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.h b/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.h
new file mode 100644
index 0000000..d5a351b4
--- /dev/null
+++ b/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.h
@@ -0,0 +1,68 @@
+// Copyright 2025 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_PERFORMANCE_MANAGER_EXECUTION_CONTEXT_PRIORITY_SIDE_PANEL_LOADING_VOTER_H_
+#define CHROME_BROWSER_PERFORMANCE_MANAGER_EXECUTION_CONTEXT_PRIORITY_SIDE_PANEL_LOADING_VOTER_H_
+
+#include "base/containers/flat_set.h"
+#include "components/performance_manager/public/execution_context_priority/priority_voting_system.h"
+#include "components/performance_manager/public/graph/frame_node.h"
+#include "components/performance_manager/public/graph/graph_registered.h"
+#include "components/performance_manager/public/graph/page_node.h"
+
+namespace performance_manager::execution_context_priority {
+
+// This voter is responsible for making sure that a Side Panel loads at a high
+// priority, even while it is not visible.
+//
+// Note that an external call to `MarkAsSidePanel` is necessary to tag pages
+// that are associated with a Side Panel.
+class SidePanelLoadingVoter : public GraphRegisteredImpl<SidePanelLoadingVoter>,
+                              public PriorityVoter,
+                              public PageNodeObserver,
+                              public FrameNodeObserver {
+ public:
+  static const char kSidePanelLoadingReason[];
+
+  SidePanelLoadingVoter();
+  ~SidePanelLoadingVoter() override;
+
+  // This marks `page_node` as a Side Panel contents, so that this class knows
+  // to increase its priority during its initial load.
+  void MarkAsSidePanel(const PageNode* page_node);
+
+  // Voter:
+  void InitializeOnGraph(Graph* graph, VotingChannel voting_channel) override;
+  void TearDownOnGraph(Graph* graph) override;
+
+  // PageNodeObserver:
+  void OnPageNodeAdded(const PageNode* page_node) override {}
+  void OnBeforePageNodeRemoved(const PageNode* page_node) override;
+  void OnMainFrameDocumentChanged(const PageNode* page_node) override;
+
+  // FrameNodeObserver:
+  void OnBeforeFrameNodeRemoved(const FrameNode* frame_node) override;
+  void OnFrameVisibilityChanged(const FrameNode* frame_node,
+                                FrameNode::Visibility previous_value) override;
+
+  VoterId voter_id() const { return voting_channel_.voter_id(); }
+
+ private:
+  void SubmitVoteForPage(const PageNode* page_node);
+
+  // The voting channel where votes are submitted.
+  VotingChannel voting_channel_;
+
+  // The set of page nodes that represent Side Panel contents that has not yet
+  // loaded. Once they start loading, they are removed from this set as the
+  // content will only get its priority increased for the initial load.
+  base::flat_set<const PageNode*> side_panel_pages_;
+
+  // The set of frame nodes that have an active vote.
+  base::flat_set<const FrameNode*> frames_with_vote_;
+};
+
+}  // namespace performance_manager::execution_context_priority
+
+#endif  // CHROME_BROWSER_PERFORMANCE_MANAGER_EXECUTION_CONTEXT_PRIORITY_SIDE_PANEL_LOADING_VOTER_H_
diff --git a/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter_unittest.cc b/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter_unittest.cc
new file mode 100644
index 0000000..b791eb0
--- /dev/null
+++ b/chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter_unittest.cc
@@ -0,0 +1,111 @@
+// Copyright 2025 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/performance_manager/execution_context_priority/side_panel_loading_voter.h"
+
+#include "base/memory/raw_ptr.h"
+#include "components/performance_manager/decorators/frame_visibility_decorator.h"
+#include "components/performance_manager/public/execution_context/execution_context.h"
+#include "components/performance_manager/public/graph/graph.h"
+#include "components/performance_manager/test_support/graph_test_harness.h"
+#include "components/performance_manager/test_support/mock_graphs.h"
+#include "components/performance_manager/test_support/voting.h"
+
+namespace performance_manager::execution_context_priority {
+
+namespace {
+
+const execution_context::ExecutionContext* GetExecutionContext(
+    const FrameNode* frame_node) {
+  return execution_context::ExecutionContext::From(frame_node);
+}
+
+}  // namespace
+
+using DummyVoteObserver = voting::test::DummyVoteObserver<Vote>;
+
+class SidePanelLoadingVoterTest : public GraphTestHarness {
+ public:
+  using Super = GraphTestHarness;
+
+  SidePanelLoadingVoterTest() = default;
+  ~SidePanelLoadingVoterTest() override = default;
+
+  SidePanelLoadingVoterTest(const SidePanelLoadingVoterTest&) = delete;
+  SidePanelLoadingVoterTest& operator=(const SidePanelLoadingVoterTest&) =
+      delete;
+
+  void SetUp() override {
+    Super::SetUp();
+    graph()->PassToGraph(std::make_unique<FrameVisibilityDecorator>());
+    side_panel_loading_voter_.InitializeOnGraph(graph(),
+                                                observer_.BuildVotingChannel());
+  }
+
+  void TearDown() override {
+    side_panel_loading_voter_.TearDownOnGraph(graph());
+    Super::TearDown();
+  }
+
+  // Exposes the DummyVoteObserver to validate expectations.
+  const DummyVoteObserver& observer() const { return observer_; }
+
+  VoterId voter_id() const { return side_panel_loading_voter_.voter_id(); }
+
+  void MarkAsSidePanel(const PageNode* page_node) {
+    side_panel_loading_voter_.MarkAsSidePanel(page_node);
+  }
+
+ private:
+  DummyVoteObserver observer_;
+  SidePanelLoadingVoter side_panel_loading_voter_;
+};
+
+TEST_F(SidePanelLoadingVoterTest, IncreasePriority) {
+  auto process(TestNodeWrapper<TestProcessNodeImpl>::Create(graph()));
+  auto page(TestNodeWrapper<PageNodeImpl>::Create(graph()));
+  auto frame(graph()->CreateFrameNodeAutoId(process.get(), page.get()));
+
+  MarkAsSidePanel(page.get());
+
+  // Pretend a navigation committed to mock the Side Panel loading.
+  page->OnMainFrameNavigationCommitted(
+      /*same_document=*/false, base::TimeTicks::Now(),
+      /*navigation_id=*/1u, GURL("asd"), "text/html", std::nullopt);
+
+  EXPECT_TRUE(
+      observer().HasVote(voter_id(), GetExecutionContext(frame.get()),
+                         base::TaskPriority::USER_BLOCKING,
+                         SidePanelLoadingVoter::kSidePanelLoadingReason));
+
+  // Making the page visible should invalidate the vote.
+  page->SetIsVisible(true);
+
+  EXPECT_FALSE(
+      observer().HasVote(voter_id(), GetExecutionContext(frame.get())));
+}
+
+TEST_F(SidePanelLoadingVoterTest, NotMarked) {
+  auto process(TestNodeWrapper<TestProcessNodeImpl>::Create(graph()));
+  auto page(TestNodeWrapper<PageNodeImpl>::Create(graph()));
+  auto frame(graph()->CreateFrameNodeAutoId(process.get(), page.get()));
+
+  // Pretend a navigation committed to mock the Side Panel loading.
+  page->OnMainFrameNavigationCommitted(
+      /*same_document=*/false, base::TimeTicks::Now(),
+      /*navigation_id=*/1u, GURL("asd"), "text/html", std::nullopt);
+
+  // Because the MarkAsSidePanel() was not called. the main frame's priority was
+  // not increased.
+  EXPECT_FALSE(
+      observer().HasVote(voter_id(), GetExecutionContext(frame.get())));
+
+  // Making the page visible.
+  page->SetIsVisible(true);
+
+  EXPECT_FALSE(
+      observer().HasVote(voter_id(), GetExecutionContext(frame.get())));
+}
+
+}  // namespace performance_manager::execution_context_priority
diff --git a/chrome/browser/performance_manager/public/side_panel_loading_policy.h b/chrome/browser/performance_manager/public/side_panel_loading_policy.h
new file mode 100644
index 0000000..944a761
--- /dev/null
+++ b/chrome/browser/performance_manager/public/side_panel_loading_policy.h
@@ -0,0 +1,20 @@
+// Copyright 2025 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_PERFORMANCE_MANAGER_PUBLIC_SIDE_PANEL_LOADING_POLICY_H_
+#define CHROME_BROWSER_PERFORMANCE_MANAGER_PUBLIC_SIDE_PANEL_LOADING_POLICY_H_
+
+namespace content {
+class WebContents;
+}
+
+namespace performance_manager::execution_context_priority {
+
+// Marks `web_contents` as the main contents of a Side Panel, which will ensure
+// that it loads at high priority, even if it is not visible.
+void MarkAsSidePanel(content::WebContents* web_contents);
+
+}  // namespace performance_manager::execution_context_priority
+
+#endif  // CHROME_BROWSER_PERFORMANCE_MANAGER_PUBLIC_SIDE_PANEL_LOADING_POLICY_H_
diff --git a/chrome/browser/performance_manager/side_panel_loading_policy.cc b/chrome/browser/performance_manager/side_panel_loading_policy.cc
new file mode 100644
index 0000000..756e8941
--- /dev/null
+++ b/chrome/browser/performance_manager/side_panel_loading_policy.cc
@@ -0,0 +1,30 @@
+// Copyright 2025 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/performance_manager/public/side_panel_loading_policy.h"
+
+#include "base/functional/bind.h"
+#include "chrome/browser/performance_manager/execution_context_priority/side_panel_loading_voter.h"
+#include "components/performance_manager/public/graph/graph.h"
+#include "components/performance_manager/public/performance_manager.h"
+#include "content/public/browser/web_contents.h"
+
+namespace performance_manager::execution_context_priority {
+
+void MarkAsSidePanel(content::WebContents* web_contents) {
+  PerformanceManager::CallOnGraph(
+      FROM_HERE,
+      base::BindOnce(
+          [](base::WeakPtr<PageNode> page_node, Graph* graph) {
+            CHECK(page_node);
+            auto* voter = graph->GetRegisteredObjectAs<
+                execution_context_priority::SidePanelLoadingVoter>();
+            CHECK(voter);
+
+            voter->MarkAsSidePanel(page_node.get());
+          },
+          PerformanceManager::GetPrimaryPageNodeForWebContents(web_contents)));
+}
+
+}  // namespace performance_manager::execution_context_priority
diff --git a/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java b/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java
index 4ad5571c..b5a27de9 100644
--- a/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java
+++ b/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java
@@ -99,6 +99,10 @@
     public static final String AUXILIARY_SEARCH_IS_SCHEMA_SET =
             "Chrome.AuxiliarySearch.IsSchemaSet";
 
+    /** Whether the consumer schema for Tabs sharing exists. */
+    public static final String AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND =
+            "Chrome.AuxiliarySearch.ConsumerSchemaFound";
+
     /** The total times that the opt in card was shown to the user. */
     public static final String AUXILIARY_SEARCH_MODULE_IMPRESSION =
             "Chrome.AuxiliarySearchModule.Impression";
@@ -919,6 +923,7 @@
                 ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS,
                 AUTOFILL_ASSISTANT_FIRST_TIME_LITE_SCRIPT_USER,
                 AUTOFILL_ASSISTANT_PROACTIVE_HELP_ENABLED,
+                AUXILIARY_SEARCH_CONSUMER_SCHEMA_FOUND,
                 AUXILIARY_SEARCH_MODULE_USER_RESPONDED,
                 AUXILIARY_SEARCH_MODULE_IMPRESSION,
                 AUXILIARY_SEARCH_IS_SCHEMA_SET,
diff --git a/chrome/browser/resources/ash/settings/crostini_page/bruschetta_subpage.ts b/chrome/browser/resources/ash/settings/crostini_page/bruschetta_subpage.ts
index a5babe68..84f15c17 100644
--- a/chrome/browser/resources/ash/settings/crostini_page/bruschetta_subpage.ts
+++ b/chrome/browser/resources/ash/settings/crostini_page/bruschetta_subpage.ts
@@ -10,6 +10,7 @@
 
 import 'chrome://resources/ash/common/cr_elements/cr_link_row/cr_link_row.js';
 import '../settings_shared.css.js';
+import '../guest_os/guest_os_confirmation_dialog.js';
 
 import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
diff --git a/chrome/browser/resources/ash/settings/crostini_page/crostini_export_import.ts b/chrome/browser/resources/ash/settings/crostini_page/crostini_export_import.ts
index a3246745..5314803 100644
--- a/chrome/browser/resources/ash/settings/crostini_page/crostini_export_import.ts
+++ b/chrome/browser/resources/ash/settings/crostini_page/crostini_export_import.ts
@@ -11,6 +11,7 @@
 import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
 import './crostini_import_confirmation_dialog.js';
 import '../settings_shared.css.js';
+import '../guest_os/guest_os_container_select.js';
 
 import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
diff --git a/chrome/browser/resources/ash/settings/crostini_page/crostini_subpage.ts b/chrome/browser/resources/ash/settings/crostini_page/crostini_subpage.ts
index 462f383..a6944a5 100644
--- a/chrome/browser/resources/ash/settings/crostini_page/crostini_subpage.ts
+++ b/chrome/browser/resources/ash/settings/crostini_page/crostini_subpage.ts
@@ -15,8 +15,6 @@
 import '../guest_os/guest_os_confirmation_dialog.js';
 import './crostini_disk_resize_dialog.js';
 import './crostini_disk_resize_confirmation_dialog.js';
-import './crostini_port_forwarding.js';
-import './crostini_extra_containers.js';
 
 import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
 import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
diff --git a/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_paths.ts b/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_paths.ts
index 5399f87..1572421e 100644
--- a/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_paths.ts
+++ b/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_paths.ts
@@ -36,7 +36,6 @@
 
 const SettingsGuestOsSharedPathsElementBase = I18nMixin(PolymerElement);
 
-/** @polymer */
 export class SettingsGuestOsSharedPathsElement extends
     SettingsGuestOsSharedPathsElementBase {
   static get is() {
diff --git a/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_usb_devices.ts b/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_usb_devices.ts
index ebbabc2a..e6f5e20 100644
--- a/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_usb_devices.ts
+++ b/chrome/browser/resources/ash/settings/guest_os/guest_os_shared_usb_devices.ts
@@ -10,6 +10,7 @@
 
 import 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
 import './guest_os_shared_usb_devices_add_dialog.js';
+import './guest_os_confirmation_dialog.js';
 
 import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
 import {CrToggleElement} from 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
diff --git a/chrome/browser/resources/ash/settings/lazy_load.ts b/chrome/browser/resources/ash/settings/lazy_load.ts
index 804124e3..7aeb8b2 100644
--- a/chrome/browser/resources/ash/settings/lazy_load.ts
+++ b/chrome/browser/resources/ash/settings/lazy_load.ts
@@ -16,6 +16,13 @@
 
 import '/strings.m.js';
 /** Subpages */
+import './crostini_page/bruschetta_subpage.js';
+import './crostini_page/crostini_arc_adb.js';
+import './crostini_page/crostini_export_import.js';
+import './crostini_page/crostini_extra_containers.js';
+import './crostini_page/crostini_port_forwarding.js';
+import './crostini_page/crostini_shared_usb_devices.js';
+import './crostini_page/crostini_subpage.js';
 import './date_time_page/timezone_subpage.js';
 import './device_page/audio.js';
 import './device_page/customize_mouse_buttons_subpage.js';
@@ -34,6 +41,8 @@
 import './device_page/storage.js';
 import './device_page/storage_external.js';
 import './device_page/stylus.js';
+import './guest_os/guest_os_shared_paths.js';
+import './guest_os/guest_os_shared_usb_devices.js';
 import './internal/storybook/storybook_subpage.js';
 import './internet_page/apn_subpage.js';
 import './internet_page/hotspot_subpage.js';
@@ -90,26 +99,6 @@
 import './os_privacy_page/privacy_hub_microphone_subpage.js';
 import './os_privacy_page/privacy_hub_subpage.js';
 import './os_privacy_page/smart_privacy_subpage.js';
-// TODO(b/263414034) Determine if elements below adhere to the lazy loading
-// criteria and are needed here
-import './crostini_page/bruschetta_subpage.js';
-import './crostini_page/crostini_arc_adb.js';
-import './crostini_page/crostini_arc_adb_confirmation_dialog.js';
-import './crostini_page/crostini_disk_resize_confirmation_dialog.js';
-import './crostini_page/crostini_disk_resize_dialog.js';
-import './crostini_page/crostini_export_import.js';
-import './crostini_page/crostini_extra_containers.js';
-import './crostini_page/crostini_extra_containers_create_dialog.js';
-import './crostini_page/crostini_import_confirmation_dialog.js';
-import './crostini_page/crostini_port_forwarding.js';
-import './crostini_page/crostini_port_forwarding_add_port_dialog.js';
-import './crostini_page/crostini_shared_usb_devices.js';
-import './crostini_page/crostini_subpage.js';
-import './guest_os/guest_os_confirmation_dialog.js';
-import './guest_os/guest_os_container_select.js';
-import './guest_os/guest_os_shared_paths.js';
-import './guest_os/guest_os_shared_usb_devices.js';
-import './guest_os/guest_os_shared_usb_devices_add_dialog.js';
 
 export {ScreenAiInstallStatus} from '/shared/settings/a11y_page/ax_annotations_browser_proxy.js';
 export {CaptionsBrowserProxy, CaptionsBrowserProxyImpl, LiveCaptionLanguage, LiveCaptionLanguageList} from '/shared/settings/a11y_page/captions_browser_proxy.js';
diff --git a/chrome/browser/resources/ash/settings/os_apps_page/os_apps_page.ts b/chrome/browser/resources/ash/settings/os_apps_page/os_apps_page.ts
index 66ff66a..a668e64 100644
--- a/chrome/browser/resources/ash/settings/os_apps_page/os_apps_page.ts
+++ b/chrome/browser/resources/ash/settings/os_apps_page/os_apps_page.ts
@@ -18,8 +18,6 @@
 import '../os_settings_page/settings_card.js';
 import '../settings_shared.css.js';
 import '../settings_shared.css.js';
-import '../guest_os/guest_os_shared_usb_devices.js';
-import '../guest_os/guest_os_shared_paths.js';
 import './app_management_page/app_management_cros_shared_vars.css.js';
 import './app_management_page/uninstall_button.js';
 import './app_parental_controls/app_setup_pin_dialog.js';
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/BUILD.gn b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/BUILD.gn
new file mode 100644
index 0000000..63df3ec6c5
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/BUILD.gn
@@ -0,0 +1,218 @@
+# Copyright 2017 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/chromeos/ui_mode.gni")
+import("//build/config/features.gni")
+import(
+    "//chrome/browser/resources/chromeos/accessibility/tools/run_jsbundler.gni")
+import("//chrome/common/features.gni")
+import("//chrome/test/base/ash/js2gtest.gni")
+import("//testing/test.gni")
+import("//tools/typescript/ts_library.gni")
+
+assert(is_chromeos_ash)
+
+group("build") {
+  deps = [ ":copied_files" ]
+}
+
+switch_access_dir =
+    "$root_out_dir/resources/chromeos/accessibility/switch_access"
+
+# Directory where typescript build will occur.
+ts_build_staging_dir = "$target_gen_dir/ts_build_staging"
+
+tsc_out_dir = "$target_gen_dir/tsc"
+
+common_tsc_dir =
+    "$root_gen_dir/chrome/browser/resources/chromeos/accessibility/common/tsc"
+
+# TS files to compile.
+ts_modules = [
+  "action_manager.ts",
+  "auto_scan_manager.ts",
+  "cache.ts",
+  "commands.ts",
+  "focus_ring_manager.ts",
+  "history.ts",
+  "item_scan_manager.ts",
+  "menu_manager.ts",
+  "metrics.ts",
+  "navigator.ts",
+  "navigator_interfaces.ts",
+  "nodes/back_button_node.ts",
+  "nodes/basic_node.ts",
+  "nodes/combo_box_node.ts",
+  "nodes/desktop_node.ts",
+  "nodes/editable_text_node.ts",
+  "nodes/group_node.ts",
+  "nodes/keyboard_node.ts",
+  "nodes/modal_dialog_node.ts",
+  "nodes/slider_node.ts",
+  "nodes/switch_access_node.ts",
+  "nodes/tab_node.ts",
+  "nodes/window_node.ts",
+  "point_scan_manager.ts",
+  "settings_manager.ts",
+  "switch_access.ts",
+  "switch_access_constants.ts",
+  "switch_access_loader.ts",
+  "switch_access_predicate.ts",
+  "text_navigation_manager.ts",
+]
+
+# Root dir must be the parent directory so it can reach common.
+ts_library("ts_build") {
+  root_dir = "$ts_build_staging_dir"
+  out_dir = tsc_out_dir
+
+  composite = true
+
+  deps = [ "../../common:ts_build" ]
+
+  extra_deps = [ ":stage_ts_build" ]
+
+  path_mappings =
+      [ "/common/*|" + rebase_path("$common_tsc_dir/*", target_gen_dir) ]
+
+  definitions = [
+    "../../definitions/accessibility_private_mv2.d.ts",
+    "../../definitions/automation.d.ts",
+    "../../definitions/clipboard_mv2.d.ts",
+    "../../definitions/command_line_private.d.ts",
+    "../../definitions/extension_types.d.ts",
+    "../../definitions/extensions.d.ts",
+    "../../definitions/runtime.d.ts",
+    "../../definitions/settings_private_mv2.d.ts",
+    "../../definitions/tabs.d.ts",
+    "../../definitions/windows.d.ts",
+    "//tools/typescript/definitions/chrome_event.d.ts",
+    "//tools/typescript/definitions/metrics_private.d.ts",
+  ]
+
+  in_files = ts_modules
+
+  tsconfig_base = "../../tsconfig.base.json"
+}
+
+# Instead of setting up one copy target for each subdirectory, use a script
+# to copy all files.
+run_jsbundler("copied_files") {
+  mode = "copy"
+  dest_dir = switch_access_dir
+  clear_dest_dirs = [ "." ]
+  deps = [
+    ":ts_build",
+    "../../common:copied_files",
+  ]
+  sources = [
+    "background.html",
+    "icons/back.svg",
+    "icons/copy.svg",
+    "icons/cut.svg",
+    "icons/decrement.svg",
+    "icons/dictation.svg",
+    "icons/increment.svg",
+    "icons/jumpToBeginningOfText.svg",
+    "icons/jumpToEndOfText.svg",
+    "icons/keyboard.svg",
+    "icons/moveBackwardOneCharOfText.svg",
+    "icons/moveBackwardOneWordOfText.svg",
+    "icons/moveCursor.svg",
+    "icons/moveDownOneLineOfText.svg",
+    "icons/moveForwardOneCharOfText.svg",
+    "icons/moveForwardOneWordOfText.svg",
+    "icons/moveUpOneLineOfText.svg",
+    "icons/paste.svg",
+    "icons/scrollDownOrForward.svg",
+    "icons/scrollLeft.svg",
+    "icons/scrollRight.svg",
+    "icons/scrollUpOrBackward.svg",
+    "icons/select.svg",
+    "icons/settings.svg",
+    "icons/showContextMenu.svg",
+    "icons/textSelectionEnd.svg",
+    "icons/textSelectionStart.svg",
+  ]
+  sources += filter_include(get_target_outputs(":ts_build"), [ "*.js" ])
+
+  rewrite_rules = [
+    rebase_path("$tsc_out_dir", root_build_dir) + ":",
+    rebase_path(".", root_build_dir) + ":",
+    rebase_path(closure_library_dir, root_build_dir) + ":closure",
+  ]
+}
+
+# Copy all JS and TS sources to a staging folder. All generated TS/JS files
+# will also be copied into this folder, which will allow us to support a TS
+# build that uses both checked-in and generated files.
+copy("stage_ts_build") {
+  sources = ts_modules
+  outputs = [ "$ts_build_staging_dir/{{source_target_relative}}" ]
+}
+
+source_set("browser_tests") {
+  testonly = true
+  assert(enable_extensions)
+
+  deps = [
+    ":switch_access_extjs_tests",
+    ":switch_access_mv3_extjs_tests",
+  ]
+
+  data = [
+    "//chrome/browser/resources/chromeos/accessibility/common/",
+    "//chrome/browser/resources/chromeos/accessibility/switch_access/",
+  ]
+  data += js2gtest_js_libraries
+}
+
+test_includes = [
+  "../../common/testing/accessibility_test_base.js",
+  "../../common/testing/assert_additions.js",
+  "../../common/testing/callback_helper.js",
+  "../../common/testing/e2e_test_base.js",
+  "switch_access_e2e_test_base.js",
+  "test_utility.js",
+]
+
+# The test base classes generate C++ code with these deps.
+test_deps = [
+  "//ash",
+  "//ash/keyboard/ui",
+  "//base",
+  "//chrome/browser/ash/accessibility",
+  "//chrome/browser/ash/crosapi",
+  "//chrome/browser/ash/system_web_apps",
+  "//chrome/common",
+  "//chromeos",
+]
+
+js2gtest("switch_access_mv3_extjs_tests") {
+  test_type = "extension"
+  parameterized = "true"
+  sources = [
+    "focus_ring_manager_test.js",
+    "item_scan_manager_test.js",
+    "nodes/basic_node_test.js",
+    "nodes/desktop_node_test.js",
+    "nodes/group_node_test.js",
+    "nodes/tab_node_test.js",
+    "point_scan_manager_test.js",
+    "switch_access_predicate_test.js",
+    "switch_access_test.js",
+    "text_navigation_manager_test.js",
+  ]
+  gen_include_files = test_includes
+  deps = test_deps
+  defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
+}
+
+js2gtest("switch_access_extjs_tests") {
+  test_type = "extension"
+  sources = [ "auto_scan_manager_test.js" ]
+  gen_include_files = test_includes
+  deps = test_deps
+  defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/COMMON_METADATA b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/COMMON_METADATA
new file mode 100644
index 0000000..be2b8b3
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/COMMON_METADATA
@@ -0,0 +1,7 @@
+team_email: "chromium-accessibility@chromium.org"
+buganizer {
+  component_id: 1272756
+}
+buganizer_public {
+  component_id: 1272578
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/DIR_METADATA b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/DIR_METADATA
new file mode 100644
index 0000000..fca80477
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/DIR_METADATA
@@ -0,0 +1 @@
+mixins: "//chrome/browser/resources/chromeos/accessibility/switch_access/mv3/COMMON_METADATA"
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/action_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/action_manager.ts
new file mode 100644
index 0000000..53024c0
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/action_manager.ts
@@ -0,0 +1,307 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {FocusRingManager} from './focus_ring_manager.js';
+import {MenuManager} from './menu_manager.js';
+import {SwitchAccessMetrics} from './metrics.js';
+import {Navigator} from './navigator.js';
+import {SAChildNode} from './nodes/switch_access_node.js';
+import {SwitchAccess} from './switch_access.js';
+import {ActionResponse, ErrorType, MenuType, Mode} from './switch_access_constants.js';
+
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+import Rect = chrome.automation.Rect;
+
+/**
+ * Class to handle performing actions with Switch Access, including determining
+ * which actions are available in the given context.
+ */
+export class ActionManager {
+  /**
+   * The node on which actions are currently being performed.
+   * Null if the menu is closed.
+   */
+  private actionNode_?: SAChildNode|null;
+  private menuManager_: MenuManager;
+  private menuStack_: MenuType[] = [];
+
+  static instance?: ActionManager;
+
+  private constructor() {
+    this.menuManager_ = MenuManager.create();
+  }
+
+  static init(): void {
+    if (ActionManager.instance) {
+      throw SwitchAccess.error(
+          ErrorType.DUPLICATE_INITIALIZATION,
+          'Cannot call ActionManager.init() more than once.');
+    }
+    ActionManager.instance = new ActionManager();
+  }
+
+  // ================= Static Methods ==================
+
+  /**
+   * Exits all of the open menus and unconditionally closes the menu window.
+   */
+  static exitAllMenus(): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    ActionManager.instance!.menuStack_ = [];
+    ActionManager.instance!.actionNode_ = null;
+    ActionManager.instance!.menuManager_.close();
+    if (SwitchAccess.mode === Mode.POINT_SCAN) {
+      Navigator.byPoint.start();
+    } else {
+      Navigator.byPoint.stop();
+    }
+  }
+
+  /**
+   * Exits the current menu. If there are no menus on the stack, closes the
+   * menu.
+   */
+  static exitCurrentMenu(): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    ActionManager.instance!.menuStack_.pop();
+    if (ActionManager.instance!.menuStack_.length > 0) {
+      ActionManager.instance!.openCurrentMenu_();
+    } else {
+      ActionManager.exitAllMenus();
+    }
+  }
+
+  /**
+   * Handles what to do when the user presses 'select'.
+   * If multiple actions are available for the currently highlighted node,
+   * opens the action menu. Otherwise performs the node's default action.
+   */
+  static onSelect(): void {
+    const node = Navigator.byItem.currentNode;
+    if (MenuManager.isMenuOpen() || node.actions.length <= 1 ||
+        !node.location) {
+      node.doDefaultAction();
+      return;
+    }
+
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    ActionManager.instance!.menuStack_ = [];
+    ActionManager.instance!.menuStack_.push(MenuType.MAIN_MENU);
+    ActionManager.instance!.actionNode_ = node;
+    ActionManager.instance!.openCurrentMenu_();
+  }
+
+  static openMenu(menu: MenuType): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    ActionManager.instance!.menuStack_.push(menu);
+    ActionManager.instance!.openCurrentMenu_();
+  }
+
+  /** Given the action to be performed, appropriately handles performing it. */
+  static performAction(action: MenuAction): void {
+    SwitchAccessMetrics.recordMenuAction(action);
+
+    switch (action) {
+      // Global actions:
+      case MenuAction.SETTINGS:
+        chrome.accessibilityPrivate.openSettingsSubpage(
+            'manageAccessibility/switchAccess');
+        ActionManager.exitCurrentMenu();
+        break;
+      case MenuAction.POINT_SCAN:
+        ActionManager.exitCurrentMenu();
+        Navigator.byPoint.start();
+        break;
+      case MenuAction.ITEM_SCAN:
+        Navigator.byItem.restart();
+        ActionManager.exitAllMenus();
+        break;
+      // Point scan actions:
+      case MenuAction.LEFT_CLICK:
+      case MenuAction.RIGHT_CLICK:
+        // Exit menu, then click (so the action will hit the desired target,
+        // instead of the menu).
+        FocusRingManager.clearAll();
+        ActionManager.exitCurrentMenu();
+        Navigator.byPoint.performMouseAction(action);
+        break;
+      // Item scan actions:
+      default:
+        // TODO(b/314203187): Not null asserted, check that this is correct.
+        ActionManager.instance!.performActionOnCurrentNode_(action);
+    }
+  }
+
+  /** Refreshes the current menu, if needed. */
+  static refreshMenuUnconditionally(): void {
+    if (!MenuManager.isMenuOpen()) {
+      return;
+    }
+
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    ActionManager.instance!.openCurrentMenu_();
+  }
+
+  /**
+   * Refreshes the current menu, if the current action node matches the node
+   * provided.
+   */
+  static refreshMenuForNode(node: SAChildNode): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    const actionNode = ActionManager.instance!.actionNode_;
+    if (actionNode && node.equals(actionNode)) {
+      ActionManager.refreshMenuUnconditionally();
+    }
+  }
+
+  // ================= Private Methods ==================
+
+  /** Returns all possible actions for the provided menu type */
+  private actionsForType_(type: MenuType): MenuAction[] {
+    switch (type) {
+      case MenuType.MAIN_MENU:
+        return [
+          MenuAction.COPY,
+          MenuAction.CUT,
+          MenuAction.DECREMENT,
+          MenuAction.DICTATION,
+          MenuAction.DRILL_DOWN,
+          MenuAction.INCREMENT,
+          MenuAction.KEYBOARD,
+          MenuAction.MOVE_CURSOR,
+          MenuAction.PASTE,
+          MenuAction.SCROLL_DOWN,
+          MenuAction.SCROLL_LEFT,
+          MenuAction.SCROLL_RIGHT,
+          MenuAction.SCROLL_UP,
+          MenuAction.SELECT,
+          MenuAction.START_TEXT_SELECTION,
+        ];
+
+      case MenuType.TEXT_NAVIGATION:
+        return [
+          MenuAction.JUMP_TO_BEGINNING_OF_TEXT,
+          MenuAction.JUMP_TO_END_OF_TEXT,
+          MenuAction.MOVE_UP_ONE_LINE_OF_TEXT,
+          MenuAction.MOVE_DOWN_ONE_LINE_OF_TEXT,
+          MenuAction.MOVE_BACKWARD_ONE_WORD_OF_TEXT,
+          MenuAction.MOVE_FORWARD_ONE_WORD_OF_TEXT,
+          MenuAction.MOVE_BACKWARD_ONE_CHAR_OF_TEXT,
+          MenuAction.MOVE_FORWARD_ONE_CHAR_OF_TEXT,
+          MenuAction.END_TEXT_SELECTION,
+        ];
+      case MenuType.POINT_SCAN_MENU:
+        return [
+          MenuAction.LEFT_CLICK,
+          MenuAction.RIGHT_CLICK,
+        ];
+      default:
+        return [];
+    }
+  }
+
+  private addGlobalActions_(actions: MenuAction[]): MenuAction[] {
+    if (SwitchAccess.mode === Mode.POINT_SCAN) {
+      actions.push(MenuAction.ITEM_SCAN);
+    } else {
+      actions.push(MenuAction.POINT_SCAN);
+    }
+    actions.push(MenuAction.SETTINGS);
+    return actions;
+  }
+
+  private get currentMenuType_(): MenuType {
+    return this.menuStack_[this.menuStack_.length - 1];
+  }
+
+  private getActionsForCurrentMenuAndNode_(): MenuAction[] {
+    if (this.currentMenuType_ === MenuType.POINT_SCAN_MENU) {
+      let actions = this.actionsForType_(MenuType.POINT_SCAN_MENU);
+      actions = this.addGlobalActions_(actions);
+      return actions;
+    }
+
+    if (!this.actionNode_ || !this.actionNode_.isValidAndVisible()) {
+      return [];
+    }
+    let actions = this.actionNode_.actions as MenuAction[];
+    const possibleActions = this.actionsForType_(this.currentMenuType_);
+    actions = actions.filter(a => possibleActions.includes(a));
+    if (this.currentMenuType_ === MenuType.MAIN_MENU) {
+      actions = this.addGlobalActions_(actions);
+    }
+    return actions;
+  }
+
+  private getLocationForCurrentMenuAndNode_(): Rect|undefined {
+    if (this.currentMenuType_ === MenuType.POINT_SCAN_MENU) {
+      return {
+        left: Math.floor(Navigator.byPoint.currentPoint.x),
+        top: Math.floor(Navigator.byPoint.currentPoint.y),
+        width: 1,
+        height: 1,
+      };
+    }
+
+    if (this.actionNode_) {
+      return this.actionNode_.location;
+    }
+
+    return undefined;
+  }
+
+  private openCurrentMenu_(): void {
+    const actions = this.getActionsForCurrentMenuAndNode_();
+    const location = this.getLocationForCurrentMenuAndNode_();
+
+    if (actions.length < 2) {
+      ActionManager.exitCurrentMenu();
+    }
+    this.menuManager_.open(actions, location);
+  }
+
+  private performActionOnCurrentNode_(action: MenuAction): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (!this.actionNode_!.hasAction(action)) {
+      ActionManager.refreshMenuUnconditionally();
+      return;
+    }
+
+    // We exit the menu before asking the node to perform the action, because
+    // having the menu on the group stack interferes with some actions. We do
+    // not close the menu bubble until we receive the ActionResponse CLOSE_MENU.
+    // If we receive a different response, we re-enter the menu.
+    Navigator.byItem.suspendCurrentGroup();
+
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    const response = this.actionNode_!.performAction(action);
+
+    switch (response) {
+      case ActionResponse.CLOSE_MENU:
+        ActionManager.exitAllMenus();
+        return;
+      case ActionResponse.EXIT_SUBMENU:
+        ActionManager.exitCurrentMenu();
+        return;
+      case ActionResponse.REMAIN_OPEN:
+        Navigator.byItem.restoreSuspendedGroup();
+        return;
+      case ActionResponse.RELOAD_MENU:
+        ActionManager.refreshMenuUnconditionally();
+        return;
+      case ActionResponse.OPEN_TEXT_NAVIGATION_MENU:
+        if (SwitchAccess.improvedTextInputEnabled()) {
+          this.menuStack_.push(MenuType.TEXT_NAVIGATION);
+        }
+        this.openCurrentMenu_();
+    }
+  }
+}
+
+/** @type {ActionManager} */
+ActionManager.instance;
+
+TestImportManager.exportForTesting(ActionManager);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/auto_scan_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/auto_scan_manager.ts
new file mode 100644
index 0000000..cbd0a03f
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/auto_scan_manager.ts
@@ -0,0 +1,136 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {Navigator} from './navigator.js';
+import {SwitchAccess} from './switch_access.js';
+import {ErrorType, Mode} from './switch_access_constants.js';
+
+/**
+ * Class to handle auto-scan behavior.
+ */
+export class AutoScanManager {
+  private intervalID_?: number;
+  private isEnabled_ = false;
+  /** Whether the current node is within the virtual keyboard. */
+  private inKeyboard_ = false;
+  /** Auto-scan interval for the on-screen keyboard in milliseconds. */
+  private keyboardScanTime_ = NOT_INITIALIZED;
+  /** Length of the auto-scan interval for most contexts, in milliseconds. */
+  private primaryScanTime_ = NOT_INITIALIZED;
+
+  static instance?: AutoScanManager;
+
+  private constructor() {}
+
+  // ============== Static Methods ================
+
+  static init(): void {
+    if (AutoScanManager.instance) {
+      throw SwitchAccess.error(
+          ErrorType.DUPLICATE_INITIALIZATION,
+          'Cannot call AutoScanManager.init() more than once.');
+    }
+    AutoScanManager.instance = new AutoScanManager();
+  }
+
+  /** Restart auto-scan under current settings if it is currently running. */
+  static restartIfRunning(): void {
+    if (AutoScanManager.instance?.isRunning_()) {
+      AutoScanManager.instance.stop_();
+      AutoScanManager.instance.start_();
+    }
+  }
+
+  /**
+   * Stop auto-scan if it is currently running. Then, if |enabled| is true,
+   * turn on auto-scan. Otherwise leave it off.
+   */
+  static setEnabled(enabled: boolean): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (AutoScanManager.instance!.isRunning_()) {
+      AutoScanManager.instance!.stop_();
+    }
+    AutoScanManager.instance!.isEnabled_ = enabled;
+    if (enabled) {
+      AutoScanManager.instance!.start_();
+    }
+  }
+
+  /** Sets whether the keyboard scan time is used. */
+  static setInKeyboard(inKeyboard: boolean): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    AutoScanManager.instance!.inKeyboard_ = inKeyboard;
+  }
+
+  /** Update this.keyboardScanTime_ to |scanTime|, in milliseconds. */
+  static setKeyboardScanTime(scanTime: number): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    AutoScanManager.instance!.keyboardScanTime_ = scanTime;
+    if (AutoScanManager.instance!.inKeyboard_) {
+      AutoScanManager.restartIfRunning();
+    }
+  }
+
+  /**
+   * Update this.primaryScanTime_ to |scanTime|. Then, if auto-scan is currently
+   * running, restart it.
+   * @param scanTime Auto-scan interval time in milliseconds.
+   */
+  static setPrimaryScanTime(scanTime: number): void {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    AutoScanManager.instance!.primaryScanTime_ = scanTime;
+    AutoScanManager.restartIfRunning();
+  }
+
+  // ============== Private Methods ================
+
+  /** Return true if auto-scan is currently running. Otherwise return false. */
+  private isRunning_(): boolean {
+    return this.isEnabled_;
+  }
+
+  /**
+   * Set the window to move to the next node at an interval in milliseconds
+   * depending on where the user is navigating. Currently,
+   * this.keyboardScanTime_ is used as the interval if the user is
+   * navigating in the virtual keyboard, and this.primaryScanTime_ is used
+   * otherwise. Does not do anything if AutoScanManager is already scanning.
+   */
+  private start_(): void {
+    if (this.primaryScanTime_ === NOT_INITIALIZED || this.intervalID_ ||
+        SwitchAccess.mode === Mode.POINT_SCAN) {
+      return;
+    }
+
+    let currentScanTime = this.primaryScanTime_;
+
+    if (SwitchAccess.improvedTextInputEnabled() && this.inKeyboard_ &&
+        this.keyboardScanTime_ !== NOT_INITIALIZED) {
+      currentScanTime = this.keyboardScanTime_;
+    }
+
+    this.intervalID_ = setInterval(() => {
+      if (SwitchAccess.mode === Mode.POINT_SCAN) {
+        this.stop_();
+        return;
+      }
+      Navigator.byItem.moveForward();
+    }, currentScanTime);
+  }
+
+  /** Stop the window from moving to the next node at a fixed interval. */
+  private stop_(): void {
+    clearInterval(this.intervalID_);
+    this.intervalID_ = undefined;
+  }
+}
+
+// Private to module.
+
+/** Sentinel value that indicates an uninitialized scan time. */
+const NOT_INITIALIZED = -1;
+
+TestImportManager.exportForTesting(AutoScanManager);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/auto_scan_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/auto_scan_manager_test.js
new file mode 100644
index 0000000..c83b4f9
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/auto_scan_manager_test.js
@@ -0,0 +1,223 @@
+// 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.
+
+GEN_INCLUDE(['switch_access_e2e_test_base.js']);
+
+UNDEFINED_INTERVAL_DELAY = -1;
+
+/** Test fixture for auto scan manager. */
+SwitchAccessAutoScanManagerTest = class extends SwitchAccessE2ETest {
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    AutoScanManager.instance.primaryScanTime_ = 1000;
+    // Use intervalCount and intervalDelay to check how many intervals are
+    // currently running (should be no more than 1) and the current delay.
+    globalThis.intervalCount = 0;
+    globalThis.intervalDelay = UNDEFINED_INTERVAL_DELAY;
+    globalThis.defaultSetInterval = setInterval;
+    globalThis.defaultClearInterval = clearInterval;
+    this.defaultMoveForward =
+        Navigator.byItem.moveForward.bind(Navigator.byItem);
+    this.moveForwardCount = 0;
+
+    setInterval = function(func, delay) {
+      globalThis.intervalCount++;
+      globalThis.intervalDelay = delay;
+
+      // Override the delay for testing.
+      return globalThis.defaultSetInterval(func, 0);
+    };
+
+    clearInterval = function(intervalId) {
+      if (intervalId) {
+        globalThis.intervalCount--;
+      }
+      globalThis.defaultClearInterval(intervalId);
+    };
+
+    Navigator.byItem.moveForward = () => {
+      this.moveForwardCount++;
+      this.onMoveForward_ && this.onMoveForward_();
+      this.defaultMoveForward();
+    };
+
+    this.onMoveForward_ = null;
+  }
+};
+
+// https://crbug.com/1452024: Flaky on linux-chromeos-rel/linux-chromeos-dbg
+TEST_F('SwitchAccessAutoScanManagerTest', 'DISABLED_SetEnabled', function() {
+  this.runWithLoadedDesktop(() => {
+    assertFalse(
+        AutoScanManager.instance.isRunning_(),
+        'Auto scan manager is running prematurely');
+    assertEquals(
+        0, this.moveForwardCount,
+        'Incorrect initialization of moveForwardCount');
+    assertEquals(0, intervalCount, 'Incorrect initialization of intervalCount');
+
+    this.onMoveForward_ = this.newCallback(() => {
+      assertTrue(
+          AutoScanManager.instance.isRunning_(),
+          'Auto scan manager has stopped running');
+      assertGT(this.moveForwardCount, 0, 'Switch Access has not moved forward');
+      assertEquals(
+          1, intervalCount, 'The number of intervals is no longer exactly 1');
+    });
+
+    AutoScanManager.setEnabled(true);
+    assertTrue(
+        AutoScanManager.instance.isRunning_(),
+        'Auto scan manager is not running');
+    assertEquals(1, intervalCount, 'There is not exactly 1 interval');
+  });
+});
+
+// https://crbug.com/1408940: Flaky on linux-chromeos-dbg
+GEN('#ifndef NDEBUG');
+GEN('#define MAYBE_SetEnabledMultiple DISABLED_SetEnabledMultiple');
+GEN('#else');
+GEN('#define MAYBE_SetEnabledMultiple SetEnabledMultiple');
+GEN('#endif');
+TEST_F(
+    'SwitchAccessAutoScanManagerTest', 'MAYBE_SetEnabledMultiple', function() {
+      this.runWithLoadedDesktop(() => {
+        assertFalse(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager is running prematurely');
+        assertEquals(
+            0, intervalCount, 'Incorrect initialization of intervalCount');
+
+        AutoScanManager.setEnabled(true);
+        AutoScanManager.setEnabled(true);
+        AutoScanManager.setEnabled(true);
+
+        assertTrue(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager is not running');
+        assertEquals(1, intervalCount, 'There is not exactly 1 interval');
+      });
+    });
+
+// TODO(crbug.com/40888769): Test is flaky.
+TEST_F(
+    'SwitchAccessAutoScanManagerTest', 'DISABLED_EnableAndDisable', function() {
+      this.runWithLoadedDesktop(() => {
+        assertFalse(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager is running prematurely');
+        assertEquals(
+            0, intervalCount, 'Incorrect initialization of intervalCount');
+
+        AutoScanManager.setEnabled(true);
+        assertTrue(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager is not running');
+        assertEquals(1, intervalCount, 'There is not exactly 1 interval');
+
+        AutoScanManager.setEnabled(false);
+        assertFalse(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager did not stop running');
+        assertEquals(0, intervalCount, 'Interval was not removed');
+      });
+    });
+
+// https://crbug.com/1408940: Flaky on linux-chromeos-dbg
+GEN('#ifndef NDEBUG');
+GEN('#define MAYBE_RestartIfRunningMultiple DISABLED_RestartIfRunningMultiple');
+GEN('#else');
+GEN('#define MAYBE_RestartIfRunningMultiple RestartIfRunningMultiple');
+GEN('#endif');
+
+TEST_F(
+    'SwitchAccessAutoScanManagerTest', 'MAYBE_RestartIfRunningMultiple',
+    function() {
+      this.runWithLoadedDesktop(() => {
+        assertFalse(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager is running prematurely');
+        assertEquals(
+            0, this.moveForwardCount,
+            'Incorrect initialization of moveForwardCount');
+        assertEquals(
+            0, intervalCount, 'Incorrect initialization of intervalCount');
+
+        AutoScanManager.setEnabled(true);
+        AutoScanManager.restartIfRunning();
+        AutoScanManager.restartIfRunning();
+        AutoScanManager.restartIfRunning();
+
+        assertTrue(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager is not running');
+        assertEquals(1, intervalCount, 'There is not exactly 1 interval');
+      });
+    });
+
+// https://crbug.com/1408940: Flaky on linux-chromeos-dbg
+GEN('#ifndef NDEBUG');
+GEN('#define MAYBE_RestartIfRunningWhenOff DISABLED_RestartIfRunningWhenOff');
+GEN('#else');
+GEN('#define MAYBE_RestartIfRunningWhenOff RestartIfRunningWhenOff');
+GEN('#endif');
+
+TEST_F(
+    'SwitchAccessAutoScanManagerTest', 'MAYBE_RestartIfRunningWhenOff',
+    function() {
+      this.runWithLoadedDesktop(() => {
+        assertFalse(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager is running at start.');
+        AutoScanManager.restartIfRunning();
+        assertFalse(
+            AutoScanManager.instance.isRunning_(),
+            'Auto scan manager enabled by restartIfRunning');
+      });
+    });
+
+// https://crbug.com/1408940: Flaky on linux-chromeos-dbg
+GEN('#ifndef NDEBUG');
+GEN('#define MAYBE_SetPrimaryScanTime DISABLED_SetPrimaryScanTime');
+GEN('#else');
+GEN('#define MAYBE_SetPrimaryScanTime SetPrimaryScanTime');
+GEN('#endif');
+
+TEST_F('SwitchAccessAutoScanManagerTest', 'SetPrimaryScanTime', function() {
+  this.runWithLoadedDesktop(() => {
+    assertFalse(
+        AutoScanManager.instance.isRunning_(),
+        'Auto scan manager is running prematurely');
+    assertEquals(
+        UNDEFINED_INTERVAL_DELAY, intervalDelay,
+        'Interval delay improperly initialized');
+
+    AutoScanManager.setPrimaryScanTime(2);
+    assertFalse(
+        AutoScanManager.instance.isRunning_(),
+        'Setting default scan time started auto-scanning');
+    assertEquals(
+        2, AutoScanManager.instance.primaryScanTime_,
+        'Default scan time set improperly');
+    assertEquals(
+        UNDEFINED_INTERVAL_DELAY, intervalDelay,
+        'Interval delay set prematurely');
+
+    AutoScanManager.setEnabled(true);
+    assertTrue(
+        AutoScanManager.instance.isRunning_(), 'Auto scan did not start');
+    assertEquals(
+        2, AutoScanManager.instance.primaryScanTime_,
+        'Default scan time has changed');
+    assertEquals(2, intervalDelay, 'Interval delay not set');
+
+    AutoScanManager.setPrimaryScanTime(5);
+    assertTrue(AutoScanManager.instance.isRunning_(), 'Auto scan stopped');
+    assertEquals(
+        5, AutoScanManager.instance.primaryScanTime_,
+        'Default scan time did not change when set a second time');
+    assertEquals(5, intervalDelay, 'Interval delay did not update');
+  });
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/background.html b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/background.html
new file mode 100644
index 0000000..6a934618
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/background.html
@@ -0,0 +1,2 @@
+<!-- Module entry point. -->
+<script type="module" src="switch_access_loader.js"></script>
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/cache.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/cache.ts
new file mode 100644
index 0000000..61c4195
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/cache.ts
@@ -0,0 +1,22 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+type CacheMap = Map<AutomationNode, boolean>;
+
+/**
+ * Saves computed values to avoid recalculating them repeatedly.
+ *
+ * Caches are single-use, and abandoned after the top-level question is answered
+ * (e.g. what are all the interesting descendants of this node?)
+ */
+export class SACache {
+  readonly isActionable: CacheMap = new Map();
+  readonly isGroup: CacheMap = new Map();
+  readonly isInterestingSubtree: CacheMap = new Map();
+}
+
+TestImportManager.exportForTesting(SACache);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/commands.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/commands.ts
new file mode 100644
index 0000000..b861064
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/commands.ts
@@ -0,0 +1,45 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {ActionManager} from './action_manager.js';
+import {AutoScanManager} from './auto_scan_manager.js';
+import {Navigator} from './navigator.js';
+import {SwitchAccess} from './switch_access.js';
+import {ErrorType} from './switch_access_constants.js';
+
+import Command = chrome.accessibilityPrivate.SwitchAccessCommand;
+
+/** Runs user commands. */
+export class SACommands {
+  static instance?: SACommands;
+
+  private commandMap_ = new Map<Command, () => void>([
+    [Command.SELECT, () => ActionManager.onSelect()],
+    [Command.NEXT, () => Navigator.byItem.moveForward()],
+    [Command.PREVIOUS, () => Navigator.byItem.moveBackward()],
+  ]);
+
+  constructor() {
+    chrome.accessibilityPrivate.onSwitchAccessCommand.addListener(
+        (command: Command) => this.runCommand_(command));
+  }
+
+  static init(): void {
+    if (SACommands.instance) {
+      throw SwitchAccess.error(
+          ErrorType.DUPLICATE_INITIALIZATION,
+          'Cannot create more than one SACommands instance.');
+    }
+    SACommands.instance = new SACommands();
+  }
+
+  private runCommand_(command: Command): void {
+    this.commandMap_.get(command)!();
+    AutoScanManager.restartIfRunning();
+  }
+}
+
+TestImportManager.exportForTesting(SACommands);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/focus_ring_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/focus_ring_manager.ts
new file mode 100644
index 0000000..5d62a6b
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/focus_ring_manager.ts
@@ -0,0 +1,272 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {RectUtil} from '/common/rect_util.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {MenuManager} from './menu_manager.js';
+import {SAChildNode, SANode} from './nodes/switch_access_node.js';
+import {SwitchAccess} from './switch_access.js';
+import {ErrorType, Mode} from './switch_access_constants.js';
+
+import FocusRingInfo = chrome.accessibilityPrivate.FocusRingInfo;
+import FocusType = chrome.accessibilityPrivate.FocusType;
+import ScreenRect = chrome.accessibilityPrivate.ScreenRect;
+
+type Observer = (primary: SANode|null, preview: SANode|null) => void;
+
+/** Class to handle focus rings. */
+export class FocusRingManager {
+  private observer_?: Observer;
+  /** A map of all the focus rings. */
+  private rings_: Record<RingId, FocusRingInfo>;
+  private ringNodesForTesting_: Record<RingId, SANode|null> = {
+    [RingId.PRIMARY]: null,
+    [RingId.PREVIEW]: null,
+  };
+
+  private static instance_?: FocusRingManager;
+
+  private constructor() {
+    this.rings_ = this.createRings_();
+  }
+
+  static init(): void {
+    if (FocusRingManager.instance_) {
+      throw SwitchAccess.error(
+          ErrorType.DUPLICATE_INITIALIZATION,
+          'Cannot initialize focus ring manager twice.');
+    }
+    FocusRingManager.instance_ = new FocusRingManager();
+  }
+
+  static get instance(): FocusRingManager {
+    if (!FocusRingManager.instance_) {
+      throw SwitchAccess.error(
+          ErrorType.UNINITIALIZED,
+          'FocusRingManager cannot be accessed before being initialized');
+    }
+    return FocusRingManager.instance_;
+  }
+
+  /** Sets the focus ring color. */
+  static setColor(color: string): void {
+    if (!COLOR_PATTERN.test(color)) {
+      console.error(SwitchAccess.error(
+          ErrorType.INVALID_COLOR,
+          'Problem setting focus ring color: ' + color + ' is not' +
+              'a valid CSS color string.'));
+      return;
+    }
+    FocusRingManager.instance.setColorValidated_(color);
+  }
+
+  /** Sets the primary and preview focus rings based on the provided node. */
+  static setFocusedNode(node: SAChildNode): void {
+    if (node.ignoreWhenComputingUnionOfBoundingBoxes()) {
+      FocusRingManager.instance.setFocusedNodeIgnorePrimary_(node);
+      return;
+    }
+
+    if (!node.location) {
+      throw SwitchAccess.error(
+          ErrorType.MISSING_LOCATION,
+          'Cannot set focus rings if node location is undefined',
+          true /* shouldRecover */);
+    }
+
+    // If the primary node is a group, show its first child as the "preview"
+    // focus.
+    if (node.isGroup()) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      const firstChild = node.asRootNode()!.firstChild;
+      FocusRingManager.instance.setFocusedNodeGroup_(node, firstChild);
+      return;
+    }
+
+    FocusRingManager.instance.setFocusedNodeLeaf_(node);
+  }
+
+  /** Clears all focus rings. */
+  static clearAll(): void {
+    FocusRingManager.instance.clearAll_();
+  }
+
+  /**
+   * Set an observer that will be called every time the focus rings
+   * are updated. It will be called with two arguments: the node for
+   * the primary ring, and the node for the preview ring. Either may
+   * be null.
+   */
+  static setObserver(observer: Observer): void {
+    FocusRingManager.instance.observer_ = observer;
+  }
+
+  // ======== Private methods ========
+
+  private clearAll_(): void {
+    this.forEachRing_(ring => ring.rects = []);
+    this.updateNodesForTesting_(null, null);
+    this.updateFocusRings_();
+  }
+
+  /** Creates the map of focus rings. */
+  private createRings_(): Record<RingId, FocusRingInfo> {
+    const primaryRing = {
+      id: RingId.PRIMARY,
+      rects: [],
+      type: FocusType.SOLID,
+      color: PRIMARY_COLOR,
+      secondaryColor: OUTER_COLOR,
+    };
+
+    const previewRing = {
+      id: RingId.PREVIEW,
+      rects: [],
+      type: FocusType.DASHED,
+      color: PREVIEW_COLOR,
+      secondaryColor: OUTER_COLOR,
+    };
+
+    return {
+      [RingId.PRIMARY]: primaryRing,
+      [RingId.PREVIEW]: previewRing,
+    };
+  }
+
+  /** Calls a function for each focus ring. */
+  private forEachRing_(callback: (info: FocusRingInfo) => void): void {
+    Object.values(this.rings_).forEach(ring => callback(ring));
+  }
+
+  /** Sets the focus ring color. Assumes the color has been validated. */
+  private setColorValidated_(color: string): void {
+    this.forEachRing_(ring => ring.color = color);
+  }
+
+  /**
+   * Sets the primary focus ring to |node|, and the preview focus ring to
+   * |firstChild|.
+   */
+  private setFocusedNodeGroup_(group: SAChildNode, firstChild: SAChildNode):
+      void {
+    // Clear the dashed ring between transitions, as the animation is
+    // distracting.
+    this.rings_[RingId.PREVIEW].rects = [];
+
+    let focusRect: ScreenRect = group.location!;
+    const childRect = firstChild ? firstChild.location : null;
+    if (childRect) {
+      // If the current element is not specialized in location handling, e.g.
+      // the back button, the focus rect should expand to contain the child
+      // rect.
+      focusRect =
+          RectUtil.expandToFitWithPadding(GROUP_BUFFER, focusRect, childRect)!;
+      this.rings_[RingId.PREVIEW].rects = [childRect];
+    }
+    this.rings_[RingId.PRIMARY].rects = [focusRect];
+    this.updateNodesForTesting_(group, firstChild);
+    this.updateFocusRings_();
+  }
+
+  /**
+   * Clears the primary focus ring and sets the preview focus ring based on the
+   * provided node.
+   */
+  private setFocusedNodeIgnorePrimary_(node: SAChildNode): void {
+    // Nodes of this type, e.g. the back button node, handles setting its own
+    // focus, as it has special requirements (a round focus ring that has no
+    // gap with the edges of the view).
+    this.rings_[RingId.PRIMARY].rects = [];
+    // Clear the dashed ring between transitions, as the animation is
+    // distracting.
+    this.rings_[RingId.PREVIEW].rects = [];
+    this.updateFocusRings_();
+
+    // Show the preview focus ring unless the menu is open (it has a custom exit
+    // button).
+    if (!MenuManager.isMenuOpen()) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      this.rings_[RingId.PREVIEW].rects = [node.group!.location];
+    }
+    this.updateNodesForTesting_(node, node.group);
+    this.updateFocusRings_();
+  }
+
+  /** Sets the primary focus to |node| and clears the secondary focus. */
+  private setFocusedNodeLeaf_(node: SAChildNode): void {
+    // TODO(b/314203187): Not nulls asserted, check these to make sure
+    // this is correct.
+    this.rings_[RingId.PRIMARY].rects = [node.location!];
+    this.rings_[RingId.PREVIEW].rects = [];
+    this.updateNodesForTesting_(node, null);
+    this.updateFocusRings_();
+  }
+
+  /**
+   * Updates all focus rings to reflect new location, color, style, or other
+   * changes. Enables observers to monitor what's focused.
+   */
+  private updateFocusRings_(): void {
+    if (SwitchAccess.mode === Mode.POINT_SCAN && !MenuManager.isMenuOpen()) {
+      return;
+    }
+
+    const focusRings = Object.values(this.rings_);
+    chrome.accessibilityPrivate.setFocusRings(
+        focusRings,
+        chrome.accessibilityPrivate.AssistiveTechnologyType.SWITCH_ACCESS);
+  }
+
+  /** Saves the primary/preview focus for testing. */
+  private updateNodesForTesting_(primary: SANode|null, preview: SANode|null):
+      void {
+    // Keep track of the nodes associated with each focus ring for testing
+    // purposes, since focus ring locations are not guaranteed to exactly match
+    // node locations.
+    this.ringNodesForTesting_[RingId.PRIMARY] = primary;
+    this.ringNodesForTesting_[RingId.PREVIEW] = preview;
+
+    const observer = FocusRingManager.instance.observer_;
+    if (observer) {
+      observer(primary, preview);
+    }
+  }
+}
+
+/**
+ * Regex pattern to verify valid colors. Checks that the first character
+ * is '#', followed by 3, 4, 6, or 8 valid hex characters, and no other
+ * characters (ignoring case).
+ */
+const COLOR_PATTERN = /^#([0-9A-F]{3,4}|[0-9A-F]{6}|[0-9A-F]{8})$/i;
+
+/**
+ * The buffer (in dip) between a child's focus ring and its parent's focus
+ * ring.
+ */
+const GROUP_BUFFER = 2;
+
+/**
+ * The focus ring IDs used by Switch Access.
+ * Exported for testing.
+ */
+export enum RingId {
+  // The ID for the ring showing the user's current focus.
+  PRIMARY = 'primary',
+  // The ID for the ring showing a preview of the next focus, if the user
+  // selects the current element.
+  PREVIEW = 'preview',
+}
+
+/** The secondary color for both rings. */
+const OUTER_COLOR = '#174EA6';  // Google Blue 900
+
+/** The inner color of the preview focus ring. */
+const PREVIEW_COLOR = '#8AB4F880';  // Google Blue 300, 50% opacity
+
+/** The inner color of the primary focus ring. */
+const PRIMARY_COLOR = '#8AB4F8';  // Google Blue 300
+
+TestImportManager.exportForTesting(FocusRingManager, ['RingId', RingId]);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/focus_ring_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/focus_ring_manager_test.js
new file mode 100644
index 0000000..7939c242
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/focus_ring_manager_test.js
@@ -0,0 +1,122 @@
+// 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.
+
+GEN_INCLUDE(['switch_access_e2e_test_base.js', 'test_utility.js']);
+
+/** Test fixture for the focus ring manager. */
+SwitchAccessFocusRingManagerTest = class extends SwitchAccessE2ETest {
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    await TestUtility.setup();
+  }
+};
+
+TEST_F('SwitchAccessFocusRingManagerTest', 'BackButtonFocus', function() {
+  this.runWithLoadedDesktop(desktop => {
+    // Focus the back button.
+    Navigator.byItem.moveTo_(
+        desktop.find({role: chrome.automation.RoleType.TAB}));
+    BackButtonNode
+        .locationForTesting = {top: 10, left: 10, width: 10, height: 10};
+
+    TestUtility.pressSelectSwitch();
+    TestUtility.pressNextSwitch();
+    TestUtility.pressNextSwitch();
+    assertTrue(
+        Navigator.byItem.node_ instanceof BackButtonNode,
+        'Third node should be a BackButtonNode');
+
+    const rings = FocusRingManager.instance.rings_;
+    const primary = rings['primary'];
+    const preview = rings['preview'];
+    assertEquals(RingId.PRIMARY, primary.id);
+    assertEquals(RingId.PREVIEW, preview.id);
+    assertEquals('solid', primary.type);
+    assertEquals('dashed', preview.type);
+
+    // Primary focus should be empty, preview focus should contain one element.
+    assertEquals(0, primary.rects.length);
+    assertEquals(1, preview.rects.length);
+  });
+});
+
+AX_TEST_F(
+    'SwitchAccessFocusRingManagerTest', 'BackButtonForMenuFocus',
+    async function() {
+      const site = '<input type="text">';
+      const rootWebArea = await this.runWithLoadedTree(site);
+      // Open the menu and focus the back button.
+      TestUtility.startFocusInside(rootWebArea);
+      // Check the current node directly, as the TestUtility relies on the
+      // FocusManager to identify the current focus.
+      assertEquals(
+          chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role);
+      TestUtility.pressSelectSwitch();
+
+      let found = false;
+      while (!found) {
+        TestUtility.pressNextSwitch();
+        if (Navigator.byItem.node_ instanceof BackButtonNode) {
+          found = true;
+        }
+      }
+
+      const rings = FocusRingManager.instance.rings_;
+      const primary = rings[RingId.PRIMARY];
+      const preview = rings[RingId.PREVIEW];
+      // Primary and preview focus should be empty.
+      assertEquals(0, primary.rects.length);
+      assertEquals(0, preview.rects.length);
+    });
+
+AX_TEST_F('SwitchAccessFocusRingManagerTest', 'ButtonFocus', async function() {
+  const site = '<button>Test</button>';
+  const rootWebArea = await this.runWithLoadedTree(site);
+  const button = rootWebArea.find({role: chrome.automation.RoleType.BUTTON});
+  Navigator.byItem.moveTo_(button);
+
+  const rings = FocusRingManager.instance.rings_;
+  const primary = rings[RingId.PRIMARY];
+  const preview = rings[RingId.PREVIEW];
+  assertEquals(1, primary.rects.length);
+  assertEquals(0, preview.rects.length);
+  // Primary focus should be on the button.
+  const focusLocation = primary.rects[0];
+  const buttonLocation = button.location;
+  assertTrue(RectUtil.equal(buttonLocation, focusLocation));
+});
+
+AX_TEST_F('SwitchAccessFocusRingManagerTest', 'GroupFocus', async function() {
+  const site = `
+    <div role="menu">
+      <div role="menuitem">Dog</div>
+      <div role="menuitem">Cat</div>
+    </div>
+  `;
+  const rootWebArea = await this.runWithLoadedTree(site);
+  const menu = rootWebArea.find({role: chrome.automation.RoleType.MENU});
+  const menuItem = rootWebArea.find(
+      {role: chrome.automation.RoleType.MENU_ITEM, attributes: {name: 'Dog'}});
+  assertNotNullNorUndefined(menu);
+  assertNotNullNorUndefined(menuItem);
+  Navigator.byItem.moveTo_(menu);
+
+  // Verify the number of rings.
+  const rings = FocusRingManager.instance.rings_;
+  const primary = rings[RingId.PRIMARY];
+  const preview = rings[RingId.PREVIEW];
+  assertEquals(1, primary.rects.length);
+  assertEquals(1, preview.rects.length);
+
+  // Use ringNodesForTesting_ to verify the underlying nodes.
+  const ringNodes = FocusRingManager.instance.ringNodesForTesting_;
+  const primaryNode = ringNodes[RingId.PRIMARY].automationNode;
+  const previewNode = ringNodes[RingId.PREVIEW].automationNode;
+
+  assertEquals(
+      menu, primaryNode, 'primary focus should be around the group (the menu)');
+  assertEquals(
+      menuItem, previewNode, 'preview focus should be around the menu item');
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/history.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/history.ts
new file mode 100644
index 0000000..b06c88b4
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/history.ts
@@ -0,0 +1,135 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {SACache} from './cache.js';
+import {Navigator} from './navigator.js';
+import {DesktopNode} from './nodes/desktop_node.js';
+import {SAChildNode, SARootNode} from './nodes/switch_access_node.js';
+import {SwitchAccessPredicate} from './switch_access_predicate.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+
+/** This class is a structure to store previous state for restoration. */
+export class FocusData {
+  group: SARootNode;
+  focus: SAChildNode;
+
+  /** |focus| Must be a child of |group|. */
+  constructor(group: SARootNode, focus: SAChildNode) {
+    this.group = group;
+    this.focus = focus;
+  }
+
+  isValid(): boolean {
+    if (this.group.isValidGroup()) {
+      // Ensure it is still valid. Some nodes may have been added
+      // or removed since this was last used.
+      this.group.refreshChildren();
+    }
+    return this.group.isValidGroup();
+  }
+}
+
+/** This class handles saving and retrieving FocusData. */
+export class FocusHistory {
+  private dataStack: FocusData[] = [];
+
+  /**
+   * Creates the restore data to get from the desktop node to the specified
+   * automation node.
+   * Erases the current history and replaces with the new data.
+   * @return Whether the history was rebuilt from the given node.
+   */
+  buildFromAutomationNode(node: AutomationNode): boolean {
+    if (!node.parent) {
+      // No ancestors, cannot create stack.
+      return false;
+    }
+    const cache = new SACache();
+    // Create a list of ancestors.
+    const ancestorStack: AutomationNode[] = [node];
+    while (node.parent) {
+      ancestorStack.push(node.parent);
+      node = node.parent;
+    }
+
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    let group: SARootNode = DesktopNode.build(ancestorStack.pop()!);
+    const firstAncestor = ancestorStack[ancestorStack.length - 1];
+    if (!SwitchAccessPredicate.isInterestingSubtree(firstAncestor, cache)) {
+      // If the topmost ancestor (other than the desktop) is entirely
+      // uninteresting, we leave the history as is.
+      return false;
+    }
+
+    const newDataStack: FocusData[] = [];
+    while (ancestorStack.length > 0) {
+      const candidate = ancestorStack.pop();
+      if (!SwitchAccessPredicate.isInteresting(candidate, group, cache)) {
+        continue;
+      }
+
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      const focus = group.findChild(candidate!);
+      if (!focus) {
+        continue;
+      }
+      newDataStack.push(new FocusData(group, focus));
+
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      group = focus.asRootNode()!;
+      if (!group) {
+        break;
+      }
+    }
+
+    if (newDataStack.length === 0) {
+      return false;
+    }
+    this.dataStack = newDataStack;
+    return true;
+  }
+
+  containsDataMatchingPredicate(predicate: (data: FocusData) => boolean):
+      boolean {
+    for (const data of this.dataStack) {
+      if (predicate(data)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns the most proximal restore data, but does not remove it from the
+   * history.
+   */
+  peek(): FocusData|null {
+    return this.dataStack[this.dataStack.length - 1] || null;
+  }
+
+  retrieve(): FocusData {
+    let data = this.dataStack.pop();
+    while (data && !data.isValid()) {
+      data = this.dataStack.pop();
+    }
+
+    if (data) {
+      return data;
+    }
+
+    // If we don't have any valid history entries, fallback to the desktop node.
+    const desktop = new DesktopNode(Navigator.byItem.desktopNode);
+    return new FocusData(desktop, desktop.firstChild);
+  }
+
+  save(data: FocusData): void {
+    this.dataStack.push(data);
+  }
+
+  /** Support for this type being used in for..of loops. */
+  [Symbol.iterator](): IterableIterator<FocusData> {
+    return this.dataStack[Symbol.iterator]();
+  }
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/back.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/back.svg
new file mode 100644
index 0000000..f4bedd7
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/back.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#E8EAED"><path fill="none" d="M0 0h24v24H0V0z"/><path d="m19 15-6 6-1.42-1.42L15.17 16H4V4h2v10h9.17l-3.59-3.58L13 9l6 6z"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/copy.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/copy.svg
new file mode 100644
index 0000000..e997dd9
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/copy.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M13 16H5V8H3v8c0 1.1.9 2 2 2h8zm5-4V4c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h7c1.1 0 2-.9 2-2zm-2 0H9V4h7z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/cut.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/cut.svg
new file mode 100644
index 0000000..8bc30436
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/cut.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M7.71 6.29 17.5 16v1.5H16l-6.018-6.068-2.274 2.275a3 3 0 1 1-1.414-1.414l2.28-2.28L6.288 7.71a3 3 0 1 1 1.42-1.42zM5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM16 2.5h2v1l-5.45 5.377-1.408-1.423zm-6 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/decrement.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/decrement.svg
new file mode 100644
index 0000000..f44823e
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/decrement.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M14 9v2H6V9zm-4-7c-4.424 0-8 3.576-8 8s3.576 8 8 8 8-3.576 8-8-3.576-8-8-8zm0 14c-3.308 0-6-2.692-6-6s2.692-6 6-6 6 2.692 6 6-2.692 6-6 6z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/dictation.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/dictation.svg
new file mode 100644
index 0000000..c2673dd56
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/dictation.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M12.563 9.5c0 1.383-1.14 2.5-2.563 2.5-1.423 0-2.571-1.117-2.571-2.5v-5C7.429 3.117 8.577 2 10 2c1.423 0 2.571 1.117 2.571 2.5zm-2.508-6c-.554 0-1.004.446-1.008 1l-.04 5a.992.992 0 0 0 .993 1c.554 0 1.003-.446 1.008-1l.039-5v-.008a.992.992 0 0 0-.992-.992zM10 13.874c-2.366 0-4.543-1.769-4.543-4.295H4c0 2.88 2.331 5.246 5.143 5.659V18h1.714v-2.762C13.67 14.834 16 12.458 16 9.578h-1.457c0 2.527-2.177 4.296-4.543 4.296z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/increment.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/increment.svg
new file mode 100644
index 0000000..1ec772f
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/increment.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M14 9v2h-3v3H9v-3H6V9h3V6h2v3zm-4-7c-4.424 0-8 3.576-8 8s3.576 8 8 8 8-3.576 8-8-3.576-8-8-8zm0 14c-3.308 0-6-2.692-6-6s2.692-6 6-6 6 2.692 6 6-2.692 6-6 6z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/jumpToBeginningOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/jumpToBeginningOfText.svg
new file mode 100644
index 0000000..c152137
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/jumpToBeginningOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M9 3v2H7V3zm3.414 6H17v2h-4.586l1.293 1.293-1.414 1.414L9 10l3.293-3.707 1.414 1.414zM9 15v2H7v-2zM5 3v14H3V3zm8 12v2h-2v-2zm4 0v2h-2v-2zm0-12v2h-2V3zm-4 0v2h-2V3z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/jumpToEndOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/jumpToEndOfText.svg
new file mode 100644
index 0000000..a059712
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/jumpToEndOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M11 3h2v2h-2zM7.586 9 6.293 7.707l1.414-1.414L11 10l-3.293 3.707-1.414-1.414L7.586 11H3V9zM11 15h2v2h-2zm4-12h2v14h-2zM7 15h2v2H7zm-4 0h2v2H3zM3 3h2v2H3zm4 0h2v2H7z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/keyboard.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/keyboard.svg
new file mode 100644
index 0000000..efbe2bf
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/keyboard.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0zm0 0h24v24H0V0z"/><path fill="#E8EAED" d="M20 7v10H4V7h16m0-2H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2zm0 3h2v2h-2zM8 8h2v2H8zm0 3h2v2H8zm-3 0h2v2H5zm0-3h2v2H5zm3 6h8v2H8zm6-3h2v2h-2zm0-3h2v2h-2zm3 3h2v2h-2zm0-3h2v2h-2z"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveBackwardOneCharOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveBackwardOneCharOfText.svg
new file mode 100644
index 0000000..2f6f0cd
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveBackwardOneCharOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M13 5v10h-2v2h6v-2h-2V5h2V3h-6v2zM9 3H7v2h2zM5 3H3v2h2zm0 12H3v2h2zm1.414-6 1.293-1.293-1.414-1.414L3 10l3.293 3.707 1.414-1.414L6.414 11H11V9zM9 15H7v2h2z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveBackwardOneWordOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveBackwardOneWordOfText.svg
new file mode 100644
index 0000000..7c3be67f
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveBackwardOneWordOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m6.414 9 1.293-1.293-1.414-1.414L3 10l3.293 3.707 1.414-1.414L6.414 11H9V9zM17 3h-6v14h6zm-2 2v10h-2V5zM3 5h6V3H3zm0 10v2h6v-2z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveCursor.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveCursor.svg
new file mode 100644
index 0000000..b7b6046
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveCursor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="#E8EAED" fill-rule="evenodd" d="M9 11H4.29l1.955 1.872L5.091 14 1 10l4.091-4 1.154 1.128L4.371 9H9V4.29L7.128 6.245 6 5.091 10 1l4 4.091-1.128 1.154L11 4.371V9h4.71l-1.955-1.872L14.909 6 19 10l-4.091 4-1.154-1.128L15.629 11H11v4.71l1.872-1.955L14 14.909 10 19l-4-4.091 1.128-1.154L9 15.629z"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveDownOneLineOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveDownOneLineOfText.svg
new file mode 100644
index 0000000..70b5d1c8
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveDownOneLineOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M17 5H3V3h14zm-6 5.586 1.293-1.293 1.414 1.414L10 14l-3.707-3.293 1.414-1.414L9 10.586V6h2zM17 17H3v-2h14z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveForwardOneCharOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveForwardOneCharOfText.svg
new file mode 100644
index 0000000..917bb3c3
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveForwardOneCharOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M7 5v10h2v2H3v-2h2V5H3V3h6v2zm4-2h2v2h-2zm4 0h2v2h-2zm0 12h2v2h-2zm-1.414-6-1.293-1.293 1.414-1.414L17 10l-3.293 3.707-1.414-1.414L13.586 11H9V9zM11 15h2v2h-2z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveForwardOneWordOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveForwardOneWordOfText.svg
new file mode 100644
index 0000000..f80da35
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveForwardOneWordOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m13.586 9-1.293-1.293 1.414-1.414L17 10l-3.293 3.707-1.414-1.414L13.586 11H11V9zM3 3h6v14H3zm2 2v10h2V5zm12 0h-6V3h6zm0 10v2h-6v-2z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveUpOneLineOfText.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveUpOneLineOfText.svg
new file mode 100644
index 0000000..9c2eadaf
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/moveUpOneLineOfText.svg
@@ -0,0 +1 @@
+<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M3 15h14v2H3zm6-5.586-1.293 1.293-1.414-1.414L10 6l3.707 3.293-1.414 1.414L11 9.414V14H9zM3 3h14v2H3z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/paste.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/paste.svg
new file mode 100644
index 0000000..c5c9c01
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/paste.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M16 3h-3.18C12.4 1.84 11.3 1 10 1s-2.4.84-2.82 2H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-6 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm6 13H4V5h3v2h6V5h3z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollDownOrForward.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollDownOrForward.svg
new file mode 100644
index 0000000..39bfa9c
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollDownOrForward.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m11 9.71 1.872-1.955L14 8.91 10 13 6 8.91l1.128-1.155L9 9.63V3h2zM14 17H6v-2h8z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollLeft.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollLeft.svg
new file mode 100644
index 0000000..d80bc88
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollLeft.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m10.29 11 1.955 1.872L11.09 14 7 10l4.09-4 1.155 1.128L10.37 9H17v2zM3 14V6h2v8z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollRight.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollRight.svg
new file mode 100644
index 0000000..0cc1ffe
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollRight.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M9.71 9 7.755 7.128 8.91 6 13 10l-4.09 4-1.155-1.128L9.63 11H3V9zM17 6v8h-2V6z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollUpOrBackward.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollUpOrBackward.svg
new file mode 100644
index 0000000..bb70a96
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/scrollUpOrBackward.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m9 10.29-1.872 1.955L6 11.09 10 7l4 4.09-1.128 1.155L11 10.37V17H9zM6 3h8v2H6z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/select.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/select.svg
new file mode 100644
index 0000000..5a72f3b
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/select.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h2v2H3zm0 4h2v2H3zm0 4h2v2H3zm0 4h2v2H3zm4 0h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2zm0-4h2v2h-2zm0-4h2v2h-2zm0-4h2v2h-2zm-4 0h2v2h-2zM7 3h2v2H7z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/settings.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/settings.svg
new file mode 100644
index 0000000..414e893
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/settings.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M10 14a2 2 0 1 1-.001 4.001A2 2 0 0 1 10 14zm0-2a2 2 0 1 1-.001-3.999A2 2 0 0 1 10 12zm0-6a2 2 0 1 1-.001-3.999A2 2 0 0 1 10 6z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/showContextMenu.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/showContextMenu.svg
new file mode 100644
index 0000000..3aa5f47
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/showContextMenu.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M5 2h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 2v12h10V4zm2 2h6v2H7zm0 3h6v2H7zm0 3h4v2H7z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/textSelectionEnd.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/textSelectionEnd.svg
new file mode 100644
index 0000000..05b0776
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/textSelectionEnd.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M13 5h-2V3h6v2h-2v10h2v2h-6v-2h2zM9 3v2H7V3zM5 3v2H3V3zm0 4v2H3V7zm0 4v2H3v-2zm0 4v2H3v-2zm4 0v2H7v-2z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/textSelectionStart.svg b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/textSelectionStart.svg
new file mode 100644
index 0000000..de543a75
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/icons/textSelectionStart.svg
@@ -0,0 +1 @@
+<svg height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M7 5v10h2v2H3v-2h2V5H3V3h6v2zm4-2h2v2h-2zm4 0h2v2h-2zm0 4h2v2h-2zm0 4h2v2h-2zm0 4h2v2h-2zm-4 0h2v2h-2z" fill="#E8EAED" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/item_scan_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/item_scan_manager.ts
new file mode 100644
index 0000000..021d056
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/item_scan_manager.ts
@@ -0,0 +1,524 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import './nodes/editable_text_node.js';
+import './nodes/slider_node.js';
+import './nodes/tab_node.js';
+
+import {AsyncUtil} from '/common/async_util.js';
+import {AutomationUtil} from '/common/automation_util.js';
+import {EventHandler} from '/common/event_handler.js';
+import {RectUtil} from '/common/rect_util.js';
+import {RepeatedEventHandler} from '/common/repeated_event_handler.js';
+import {RepeatedTreeChangeHandler} from '/common/repeated_tree_change_handler.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {ActionManager} from './action_manager.js';
+import {AutoScanManager} from './auto_scan_manager.js';
+import {FocusRingManager} from './focus_ring_manager.js';
+import {FocusData, FocusHistory} from './history.js';
+import {MenuManager} from './menu_manager.js';
+import {Navigator} from './navigator.js';
+import {ItemNavigatorInterface} from './navigator_interfaces.js';
+import {BackButtonNode} from './nodes/back_button_node.js';
+import {BasicNode, BasicRootNode} from './nodes/basic_node.js';
+import {DesktopNode} from './nodes/desktop_node.js';
+import {KeyboardRootNode} from './nodes/keyboard_node.js';
+import {ModalDialogRootNode} from './nodes/modal_dialog_node.js';
+import {SAChildNode, SANode, SARootNode} from './nodes/switch_access_node.js';
+import {SwitchAccess} from './switch_access.js';
+import {Mode} from './switch_access_constants.js';
+import {SwitchAccessPredicate} from './switch_access_predicate.js';
+
+type AutomationEvent = chrome.automation.AutomationEvent;
+type AutomationNode = chrome.automation.AutomationNode;
+const EventType = chrome.automation.EventType;
+const RoleType = chrome.automation.RoleType;
+type TreeChange = chrome.automation.TreeChange;
+const TreeChangeObserverFilter = chrome.automation.TreeChangeObserverFilter;
+const TreeChangeType = chrome.automation.TreeChangeType;
+
+/** This class handles navigation amongst the elements onscreen. */
+export class ItemScanManager extends ItemNavigatorInterface {
+  private desktop_: AutomationNode;
+  private group_: SARootNode;
+  private node_: SAChildNode;
+  private history_: FocusHistory;
+  private suspendedGroup_: FocusData|null = null;
+  private ignoreFocusInKeyboard_ = false;
+
+  constructor(desktop: AutomationNode) {
+    super();
+
+    this.desktop_ = desktop;
+    this.group_ = DesktopNode.build(this.desktop_);
+    // TODO(crbug.com/40706137): It is possible for the firstChild to be a
+    // window which is occluded, for example if Switch Access is turned on
+    // when the user has several browser windows opened. We should either
+    // dynamically pick this.node_'s initial value based on an occlusion check,
+    // or ensure that we move away from occluded children as quickly as soon
+    // as they are detected using an interval set in DesktopNode.
+    this.node_ = this.group_.firstChild;
+    this.history_ = new FocusHistory();
+  }
+
+  // =============== ItemNavigatorInterface implementation ==============
+
+  override currentGroupHasChild(node: SAChildNode): boolean {
+    return this.group_.children.includes(node);
+  }
+
+  override enterGroup(): void {
+    if (!this.node_.isGroup()) {
+      return;
+    }
+
+    const newGroup = this.node_.asRootNode();
+    if (newGroup) {
+      this.history_.save(new FocusData(this.group_, this.node_));
+      this.setGroup_(newGroup);
+    }
+  }
+
+  override enterKeyboard(): void {
+    this.ignoreFocusInKeyboard_ = true;
+    this.node_.automationNode.focus();
+    const keyboard = KeyboardRootNode.buildTree();
+    this.jumpTo_(keyboard);
+  }
+
+  override exitGroupUnconditionally(): void {
+    this.exitGroup_();
+  }
+
+  override exitIfInGroup(node: SANode|AutomationNode|null): void {
+    if (this.group_.isEquivalentTo(node)) {
+      this.exitGroup_();
+    }
+  }
+
+  override async exitKeyboard(): Promise<void> {
+    this.ignoreFocusInKeyboard_ = false;
+    const isKeyboard = (data: FocusData): boolean =>
+        data.group instanceof KeyboardRootNode;
+    // If we are not in the keyboard, do nothing.
+    if (!(this.group_ instanceof KeyboardRootNode) &&
+        !this.history_.containsDataMatchingPredicate(isKeyboard)) {
+      return;
+    }
+
+    while (this.history_.peek() !== null) {
+      if (this.group_ instanceof KeyboardRootNode) {
+        this.exitGroup_();
+        break;
+      }
+      this.exitGroup_();
+    }
+
+    const focus = await AsyncUtil.getFocus();
+    // First, try to move back to the focused node.
+    if (focus) {
+      this.moveTo_(focus);
+    } else {
+      // Otherwise, move to anything that's valid based on the above history.
+      this.moveToValidNode();
+    }
+  }
+
+  override forceFocusedNode(node: SAChildNode): void {
+    // Check if they are exactly the same instance. Checking contents
+    // equality is not sufficient in case the node has been repopulated
+    // after a refresh.
+    if (this.node_ !== node) {
+      this.setNode_(node);
+    }
+  }
+
+  override getTreeForDebugging(wholeTree = true): SARootNode {
+    if (!wholeTree) {
+      console.log(this.group_.debugString(wholeTree));
+      return this.group_;
+    }
+
+    const desktopRoot = DesktopNode.build(this.desktop_);
+    console.log(desktopRoot.debugString(wholeTree, '', this.node_));
+    return desktopRoot;
+  }
+
+  override jumpTo(automationNode: AutomationNode): void {
+    if (!automationNode) {
+      return;
+    }
+    const node = BasicRootNode.buildTree(automationNode);
+    this.jumpTo_(node, false /* shouldExitMenu */);
+  }
+
+  override moveBackward(): void {
+    if (this.node_.isValidAndVisible()) {
+      this.tryMoving(this.node_.previous, node => node.previous, this.node_);
+    } else {
+      this.moveToValidNode();
+    }
+  }
+
+  override moveForward(): void {
+    if (this.node_.isValidAndVisible()) {
+      this.tryMoving(this.node_.next, node => node.next, this.node_);
+    } else {
+      this.moveToValidNode();
+    }
+  }
+
+  override async tryMoving(
+      node: SAChildNode, getNext: (node: SAChildNode) => SAChildNode,
+      startingNode: SAChildNode): Promise<void> {
+    if (node === startingNode) {
+      // This should only happen if the desktop contains exactly one interesting
+      // child and all other children are windows which are occluded.
+      // Unlikely to happen since we can always access the shelf.
+      return;
+    }
+
+    if (!(node instanceof BasicNode)) {
+      this.setNode_(node);
+      return;
+    }
+    if (!SwitchAccessPredicate.isWindow(node.automationNode)) {
+      this.setNode_(node);
+      return;
+    }
+    const location = node.location;
+    if (!location) {
+      // Closure compiler doesn't realize we already checked isValidAndVisible
+      // before calling tryMoving, so we need to explicitly check location here
+      // so that RectUtil.center does not cause a closure error.
+      this.moveToValidNode();
+      return;
+    }
+    const center = RectUtil.center(location);
+    // Check if the top center is visible as a proxy for occlusion. It's
+    // possible that other parts of the window are occluded, but in Chrome we
+    // can't drag windows off the top of the screen.
+    const hitNode: AutomationNode = await new Promise(
+        resolve =>
+            this.desktop_.hitTestWithReply(center.x, location.top, resolve));
+    if (AutomationUtil.isDescendantOf(hitNode, node.automationNode)) {
+      this.setNode_(node);
+    } else if (node.isValidAndVisible()) {
+      this.tryMoving(getNext(node), getNext, startingNode);
+    } else {
+      this.moveToValidNode();
+    }
+  }
+
+  override moveToValidNode(): void {
+    const nodeIsValid = this.node_.isValidAndVisible();
+    const groupIsValid = this.group_.isValidGroup();
+
+    if (nodeIsValid && groupIsValid) {
+      return;
+    }
+
+    if (nodeIsValid && !(this.node_ instanceof BackButtonNode)) {
+      // Our group has been invalidated. Move to this node to repair the
+      // group stack.
+      this.moveTo_(this.node_.automationNode);
+      return;
+    }
+
+    const child = this.group_.firstValidChild();
+    if (groupIsValid && child) {
+      this.setNode_(child);
+      return;
+    }
+
+    this.restoreFromHistory_();
+
+    // Make sure the menu isn't open unless we're still in the menu.
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (!this.group_.isEquivalentTo(MenuManager.menuAutomationNode!)) {
+      ActionManager.exitAllMenus();
+    }
+  }
+
+  override restart(): void {
+    const point = Navigator.byPoint.currentPoint;
+    SwitchAccess.mode = Mode.ITEM_SCAN;
+    this.desktop_.hitTestWithReply(
+        point.x, point.y, node => this.moveTo_(node));
+  }
+
+  override restoreSuspendedGroup(): void {
+    if (this.suspendedGroup_) {
+      // Clearing the focus rings avoids having them re-animate to the same
+      // position.
+      FocusRingManager.clearAll();
+      this.history_.save(new FocusData(this.group_, this.node_));
+      this.loadFromData_(this.suspendedGroup_);
+    }
+  }
+
+  override suspendCurrentGroup(): void {
+    const data = new FocusData(this.group_, this.node_);
+    this.exitGroup_();
+    this.suspendedGroup_ = data;
+  }
+
+  override get currentNode(): SAChildNode {
+    this.moveToValidNode();
+    return this.node_;
+  }
+
+  override get desktopNode(): AutomationNode {
+    return this.desktop_;
+  }
+
+  // =============== Event Handlers ==============
+
+  /**
+   * When focus shifts, move to the element. Find the closest interesting
+   *     element to engage with.
+   */
+  private onFocusChange_(event: AutomationEvent): void {
+    if (SwitchAccess.mode === Mode.POINT_SCAN) {
+      return;
+    }
+
+    // Ignore focus changes from our own actions.
+    if (event.eventFrom === 'action') {
+      return;
+    }
+
+    // To be safe, let's ignore focus when we're in the SA menu or over the
+    // keyboard.
+    if (this.ignoreFocusInKeyboard_ ||
+        this.group_ instanceof KeyboardRootNode || MenuManager.isMenuOpen()) {
+      return;
+    }
+
+    if (this.node_.isEquivalentTo(event.target)) {
+      return;
+    }
+    this.moveTo_(event.target);
+  }
+
+  /**
+   * When scroll position changes, ensure that the focus ring is in the
+   * correct place and that the focused node / node group are valid.
+   */
+  private onScrollChange_(): void {
+    if (SwitchAccess.mode === Mode.POINT_SCAN) {
+      return;
+    }
+
+    if (this.node_.isValidAndVisible()) {
+      // Update focus ring.
+      FocusRingManager.setFocusedNode(this.node_);
+    }
+    this.group_.refresh();
+    ActionManager.refreshMenuUnconditionally();
+  }
+
+  /** When a menu is opened, jump focus to the menu. */
+  private onModalDialog_(event: AutomationEvent): void {
+    if (SwitchAccess.mode === Mode.POINT_SCAN) {
+      return;
+    }
+
+    const modalRoot = ModalDialogRootNode.buildTree(event.target);
+    if (modalRoot.isValidGroup()) {
+      this.jumpTo_(modalRoot);
+    }
+  }
+
+  /**
+   * When the automation tree changes, ensure the group and node we are
+   * currently listening to are fresh. This is only called when the tree change
+   * occurred on the node or group which are currently active.
+   */
+  private onTreeChange_(treeChange: TreeChange): void {
+    if (SwitchAccess.mode === Mode.POINT_SCAN) {
+      return;
+    }
+
+    if (treeChange.type === TreeChangeType.NODE_REMOVED) {
+      this.group_.refresh();
+      this.moveToValidNode();
+    } else if (treeChange.type === TreeChangeType.SUBTREE_UPDATE_END) {
+      this.group_.refresh();
+    }
+  }
+
+  // =============== Private Methods ==============
+
+  private exitGroup_(): void {
+    this.group_.onExit();
+    this.restoreFromHistory_();
+  }
+
+  override start(): void {
+    chrome.automation.getFocus((focus: AutomationNode) => {
+      if (focus && this.history_.buildFromAutomationNode(focus)) {
+        this.restoreFromHistory_();
+      } else {
+        this.group_.onFocus();
+        this.node_.onFocus();
+      }
+    });
+
+    new RepeatedEventHandler(
+        this.desktop_, EventType.FOCUS, event => this.onFocusChange_(event));
+
+    // ARC++ fires SCROLL_POSITION_CHANGED.
+    new RepeatedEventHandler(
+        this.desktop_, EventType.SCROLL_POSITION_CHANGED,
+        () => this.onScrollChange_());
+
+    // Web and Views use AXEventGenerator, which fires
+    // separate horizontal and vertical events.
+    new RepeatedEventHandler(
+        this.desktop_, EventType.SCROLL_HORIZONTAL_POSITION_CHANGED,
+        () => this.onScrollChange_());
+    new RepeatedEventHandler(
+        this.desktop_, EventType.SCROLL_VERTICAL_POSITION_CHANGED,
+        () => this.onScrollChange_());
+
+    new RepeatedTreeChangeHandler(
+        TreeChangeObserverFilter.ALL_TREE_CHANGES,
+        treeChange => this.onTreeChange_(treeChange), {
+          predicate: treeChange =>
+              this.group_.findChild(treeChange.target) != null ||
+              this.group_.isEquivalentTo(treeChange.target),
+        });
+
+    // The status tray fires a SHOW event when it opens.
+    new EventHandler(
+        this.desktop_, [EventType.MENU_START, EventType.SHOW],
+        event => this.onModalDialog_(event))
+        .start();
+  }
+
+  /**
+   * Jumps Switch Access focus to a specified node, such as when opening a menu
+   * or the keyboard. Does not modify the groups already in the group stack.
+   */
+  private jumpTo_(group: SARootNode, shouldExitMenu = true): void {
+    if (shouldExitMenu) {
+      ActionManager.exitAllMenus();
+    }
+
+    this.history_.save(new FocusData(this.group_, this.node_));
+    this.setGroup_(group);
+  }
+
+  /**
+   * Moves Switch Access focus to a specified node, based on a focus shift or
+   *     tree change event. Reconstructs the group stack to center on that node.
+   *
+   * This is a "permanent" move, while |jumpTo_| is a "temporary" move.
+   */
+  private moveTo_(automationNode: AutomationNode): void {
+    ActionManager.exitAllMenus();
+    if (this.history_.buildFromAutomationNode(automationNode)) {
+      this.restoreFromHistory_();
+    }
+  }
+
+  /** Restores the most proximal state that is still valid from the history. */
+  private restoreFromHistory_(): void {
+    // retrieve() guarantees that the data's group is valid.
+    this.loadFromData_(this.history_.retrieve());
+  }
+
+  /** Extracts the focus and group from save data. */
+  private loadFromData_(data: FocusData): void {
+    if (!data.group.isValidGroup()) {
+      return;
+    }
+
+    // |data.focus| may not be a child of |data.group| anymore since
+    // |data.group| updates when retrieving the history record. So |data.focus|
+    // should not be used as the preferred focus node. Instead, we should find
+    // the equivalent node in the group's children.
+    let focusTarget: SAChildNode|null = null;
+    for (const child of data.group.children) {
+      if (child.isEquivalentTo(data.focus)) {
+        focusTarget = child;
+        break;
+      }
+    }
+
+    if (focusTarget && focusTarget.isValidAndVisible()) {
+      this.setGroup_(data.group, focusTarget);
+    } else {
+      this.setGroup_(data.group);
+    }
+  }
+
+  /**
+   * Set |this.group_| to |group|, and sets |this.node_| to either |opt_focus|
+   * or |group.firstChild|.
+   */
+  private setGroup_(group: SARootNode, focus?: SAChildNode): void {
+    // Clear the suspended group, as it's only valid in its original context.
+    this.suspendedGroup_ = null;
+
+    this.group_.onUnfocus();
+    this.group_ = group;
+    this.group_.onFocus();
+
+    const node = focus || this.group_.firstValidChild();
+    if (!node) {
+      this.moveToValidNode();
+      return;
+    }
+
+    // Check to see if the new node requires we try and focus a new window.
+    chrome.automation.getFocus((currentAutomationFocus: AutomationNode) => {
+      const newAutomationNode = node.automationNode;
+      if (!newAutomationNode || !currentAutomationFocus) {
+        return;
+      }
+
+      // First, if the current focus is a descendant of the new node or vice
+      // versa, then we're done here.
+      if (AutomationUtil.isDescendantOf(
+              currentAutomationFocus, newAutomationNode) ||
+          AutomationUtil.isDescendantOf(
+              newAutomationNode, currentAutomationFocus)) {
+        return;
+      }
+
+      // The current focus and new node do not have one another in their
+      // ancestry; try to focus an ancestor window of the new node. In
+      // particular, the parenting aura::Window of the views::Widget.
+      let widget: AutomationNode|undefined = newAutomationNode;
+      while (
+          widget &&
+          (widget.role !== RoleType.WINDOW || widget.className !== 'Widget')) {
+        widget = widget.parent;
+      }
+
+      if (widget && widget.parent) {
+        widget.parent.focus();
+      }
+    });
+
+    this.setNode_(node);
+  }
+
+  /** Set |this.node_| to |node|, and update what is displayed onscreen. */
+  private setNode_(node: SAChildNode): void {
+    if (!node.isValidAndVisible()) {
+      this.moveToValidNode();
+      return;
+    }
+    this.node_.onUnfocus();
+    this.node_ = node;
+    this.node_.onFocus();
+    AutoScanManager.restartIfRunning();
+  }
+}
+
+TestImportManager.exportForTesting(ItemScanManager);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/item_scan_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/item_scan_manager_test.js
new file mode 100644
index 0000000..a549a20
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/item_scan_manager_test.js
@@ -0,0 +1,597 @@
+// 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.
+
+GEN_INCLUDE(['switch_access_e2e_test_base.js']);
+
+/** Test fixture for the item scan manager. */
+SwitchAccessItemScanManagerTest = class extends SwitchAccessE2ETest {
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    globalThis.MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+
+    BackButtonNode
+        .locationForTesting = {top: 10, left: 10, width: 20, height: 20};
+  }
+
+  moveToPageContents(pageContents) {
+    const cache = new SACache();
+    if (!SwitchAccessPredicate.isGroup(pageContents, null, cache)) {
+      pageContents =
+          new AutomationTreeWalker(
+              pageContents, constants.Dir.FORWARD,
+              {visit: node => SwitchAccessPredicate.isGroup(node, null, cache)})
+              .next()
+              .node;
+    }
+    assertNotNullNorUndefined(
+        pageContents, 'Could not find group corresponding to page contents');
+    Navigator.byItem.moveTo_(pageContents);
+    Navigator.byItem.enterGroup();
+  }
+};
+
+function currentNode() {
+  return Navigator.byItem.node_;
+}
+
+AX_TEST_F('SwitchAccessItemScanManagerTest', 'MoveTo', async function() {
+  const website = `<div id="outerGroup">
+                     <div id="group">
+                       <input type="text">
+                       <input type="range">
+                     </div>
+                     <button></button>
+                   </div>`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const desktop = rootWebArea.parent.root;
+  const textFields =
+      desktop.findAll({role: chrome.automation.RoleType.TEXT_FIELD});
+  assertTrue(
+      textFields.length === 2 || textFields.length === 3,
+      'Should be exactly 2 or 3 text fields.');
+  const omnibar = textFields[0];
+  const textInput = textFields[textFields.length - 1];
+  const sliders = desktop.findAll({role: chrome.automation.RoleType.SLIDER});
+  assertEquals(1, sliders.length, 'Should be exactly 1 slider.');
+  const slider = sliders[0];
+  const group = this.findNodeById('group');
+  const outerGroup = this.findNodeById('outerGroup');
+
+  Navigator.byItem.moveTo_(omnibar);
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
+      'Did not successfully move to the omnibar');
+  assertFalse(
+      Navigator.byItem.group_.isEquivalentTo(group),
+      'Omnibar is in the group in page contents somehow');
+  assertFalse(
+      Navigator.byItem.group_.isEquivalentTo(outerGroup),
+      'Omnibar is in the outer group in page contents somehow');
+  const grandGroup = Navigator.byItem.history_.peek().group;
+  assertFalse(
+      grandGroup.isEquivalentTo(group),
+      'Group stack contains the group from page contents');
+  assertFalse(
+      grandGroup.isEquivalentTo(outerGroup),
+      'Group stack contains the outer group from page contents');
+
+  Navigator.byItem.moveTo_(textInput);
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
+      'Did not successfully move to the text input');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group),
+      'Group node was not successfully populated');
+  assertTrue(
+      Navigator.byItem.history_.peek().group.isEquivalentTo(outerGroup),
+      'History was not built properly');
+
+  Navigator.byItem.moveTo_(slider);
+  assertEquals(
+      chrome.automation.RoleType.SLIDER, Navigator.byItem.node_.role,
+      'Did not successfully move to the slider');
+
+  Navigator.byItem.moveTo_(group);
+  assertTrue(Navigator.byItem.node_.isGroup(), 'Current node is not a group');
+  assertTrue(
+      Navigator.byItem.node_.isEquivalentTo(group),
+      'Did not find the right group');
+});
+
+AX_TEST_F('SwitchAccessItemScanManagerTest', 'JumpTo', async function() {
+  const website = `<div id="group1">
+                     <input id="testinput" type="text">
+                     <button></button>
+                   </div>
+                   <div id="group2">
+                     <button></button>
+                     <button></button>
+                   </div>`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const desktop = rootWebArea.parent.root;
+  const textInput = this.findNodeById('testinput');
+  assertNotNullNorUndefined(textInput, 'Text field is undefined');
+  const group1 = this.findNodeById('group1');
+  const group2 = this.findNodeById('group2');
+
+  Navigator.byItem.moveTo_(textInput);
+  const textInputNode = Navigator.byItem.node_;
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, textInputNode.role,
+      'Did not successfully move to the text input');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group1),
+      'Group is initialized in an unexpected manner');
+
+  Navigator.byItem.jumpTo_(BasicRootNode.buildTree(group2));
+  assertFalse(
+      Navigator.byItem.group_.isEquivalentTo(group1), 'Jump did nothing');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group2),
+      'Jumped to the wrong group');
+
+  Navigator.byItem.exitGroup_();
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group1),
+      'Did not jump back to the right group.');
+});
+
+AX_TEST_F('SwitchAccessItemScanManagerTest', 'SelectButton', async function() {
+  const website = `<button id="test" aria-pressed=false>First Button</button>
+      <button>Second Button</button>
+      <script>
+        let state = false;
+        let button = document.getElementById("test");
+        button.onclick = () => {
+          state = !state;
+          button.setAttribute("aria-pressed", state);
+        };
+      </script>`;
+
+  const pageContents = await this.runWithLoadedTree(website);
+  this.moveToPageContents(pageContents);
+
+  const node = currentNode().automationNode;
+  assertNotNullNorUndefined(node, 'Node is invalid');
+  assertEquals(node.name, 'First Button', 'Did not find the right node');
+
+  node.addEventListener(
+      chrome.automation.EventType.CHECKED_STATE_CHANGED,
+      this.newCallback(event => {
+        assertEquals(
+            node.name, event.target.name,
+            'Checked state changed on unexpected node');
+      }));
+
+  Navigator.byItem.node_.performAction('select');
+});
+
+AX_TEST_F('SwitchAccessItemScanManagerTest', 'EnterGroup', async function() {
+  const website = `<div id="group">
+                     <button></button>
+                     <button></button>
+                   </div>
+                   <input type="range">`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const targetGroup = this.findNodeById('group');
+  Navigator.byItem.moveTo_(targetGroup);
+
+  const originalGroup = Navigator.byItem.group_;
+  assertEquals(
+      Navigator.byItem.node_.automationNode.htmlId, 'group',
+      'Did not move to group properly');
+
+  Navigator.byItem.enterGroup();
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, Navigator.byItem.node_.role,
+      'Current node is not a button');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(targetGroup),
+      'Target group was not entered');
+
+  Navigator.byItem.exitGroup_();
+  assertTrue(
+      originalGroup.equals(Navigator.byItem.group_),
+      'Did not move back to the original group');
+});
+
+AX_TEST_F('SwitchAccessItemScanManagerTest', 'MoveForward', async function() {
+  const website = `<div>
+                     <button id="button1"></button>
+                     <button id="button2"></button>
+                     <button id="button3"></button>
+                   </div>`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  Navigator.byItem.moveTo_(this.findNodeById('button1'));
+  const button1 = Navigator.byItem.node_;
+  assertFalse(
+      button1 instanceof BackButtonNode,
+      'button1 should not be a BackButtonNode');
+  assertEquals(
+      'button1', button1.automationNode.htmlId, 'Current node is not button1');
+
+  Navigator.byItem.moveForward();
+  assertFalse(
+      button1.equals(Navigator.byItem.node_),
+      'Still on button1 after moveForward()');
+  const button2 = Navigator.byItem.node_;
+  assertFalse(
+      button2 instanceof BackButtonNode,
+      'button2 should not be a BackButtonNode');
+  assertEquals(
+      'button2', button2.automationNode.htmlId, 'Current node is not button2');
+
+  Navigator.byItem.moveForward();
+  assertFalse(
+      button1.equals(Navigator.byItem.node_),
+      'Unexpected navigation to button1');
+  assertFalse(
+      button2.equals(Navigator.byItem.node_),
+      'Still on button2 after moveForward()');
+  const button3 = Navigator.byItem.node_;
+  assertFalse(
+      button3 instanceof BackButtonNode,
+      'button3 should not be a BackButtonNode');
+  assertEquals(
+      'button3', button3.automationNode.htmlId, 'Current node is not button3');
+
+  Navigator.byItem.moveForward();
+  assertTrue(
+      Navigator.byItem.node_ instanceof BackButtonNode,
+      'BackButtonNode should come after button3');
+
+  Navigator.byItem.moveForward();
+  assertTrue(
+      button1.equals(Navigator.byItem.node_),
+      'button1 should come after the BackButtonNode');
+});
+
+AX_TEST_F('SwitchAccessItemScanManagerTest', 'MoveBackward', async function() {
+  const website = `<div>
+                     <button id="button1"></button>
+                     <button id="button2"></button>
+                     <button id="button3"></button>
+                   </div>`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  Navigator.byItem.moveTo_(this.findNodeById('button1'));
+  const button1 = Navigator.byItem.node_;
+  assertFalse(
+      button1 instanceof BackButtonNode,
+      'button1 should not be a BackButtonNode');
+  assertEquals(
+      'button1', button1.automationNode.htmlId, 'Current node is not button1');
+
+  Navigator.byItem.moveBackward();
+  assertTrue(
+      Navigator.byItem.node_ instanceof BackButtonNode,
+      'BackButtonNode should come before button1');
+
+  Navigator.byItem.moveBackward();
+  assertFalse(
+      button1.equals(Navigator.byItem.node_),
+      'Unexpected navigation to button1');
+  const button3 = Navigator.byItem.node_;
+  assertFalse(
+      button3 instanceof BackButtonNode,
+      'button3 should not be a BackButtonNode');
+  assertEquals(
+      'button3', button3.automationNode.htmlId, 'Current node is not button3');
+
+  Navigator.byItem.moveBackward();
+  assertFalse(
+      button3.equals(Navigator.byItem.node_),
+      'Still on button3 after moveBackward()');
+  assertFalse(button1.equals(Navigator.byItem.node_), 'Skipped button2');
+  const button2 = Navigator.byItem.node_;
+  assertFalse(
+      button2 instanceof BackButtonNode,
+      'button2 should not be a BackButtonNode');
+  assertEquals(
+      'button2', button2.automationNode.htmlId, 'Current node is not button2');
+
+  Navigator.byItem.moveBackward();
+  assertTrue(
+      button1.equals(Navigator.byItem.node_),
+      'button1 should come before button2');
+});
+
+AX_TEST_F(
+    'SwitchAccessItemScanManagerTest', 'NodeUndefinedBeforeTreeChangeRemoved',
+    async function() {
+      const website = `<div>
+                     <button id="button1"></button>
+                   </div>`;
+      const rootWebArea = await this.runWithLoadedTree(website);
+      Navigator.byItem.moveTo_(this.findNodeById('button1'));
+      const button1 = Navigator.byItem.node_;
+      assertFalse(
+          button1 instanceof BackButtonNode,
+          'button1 should not be a BackButtonNode');
+      assertEquals(
+          'button1', button1.automationNode.htmlId,
+          'Current node is not button1');
+
+      // Simulate the underlying node's deletion. Note that this is different
+      // than an orphaned node (which can have a valid AutomationNode
+      // instance, but no backing C++ object, so attributes returned like role
+      // are undefined).
+      Navigator.byItem.node_.baseNode_ = undefined;
+
+      // Tree change removed gets sent by C++ after the tree has already
+      // applied changes (so this comes after the above clearing).
+      Navigator.byItem.onTreeChange_(
+          {type: chrome.automation.TreeChangeType.NODE_REMOVED});
+    });
+
+// TODO(crbug.com/336827654): Investigate failures.
+AX_TEST_F(
+    'SwitchAccessItemScanManagerTest', 'DISABLED_ScanAndTypeVirtualKeyboard',
+    async function() {
+      const website = `<input type="text" id="testinput"></input>`;
+      const rootWebArea = await this.runWithLoadedTree(website);
+
+      // Move to the text field.
+      Navigator.byItem.moveTo_(this.findNodeById('testinput'));
+      const input = Navigator.byItem.node_;
+      assertEquals(
+          'testinput', input.automationNode.htmlId,
+          'Current node is not input');
+      input.performAction(MenuAction.KEYBOARD);
+
+      const keyboard =
+          await this.untilFocusIs({role: chrome.automation.RoleType.KEYBOARD});
+      keyboard.performAction('select');
+
+      const key = await this.untilFocusIs({instance: KeyboardNode});
+
+      key.performAction('select');
+
+      if (input.automationNode.value !== 'q') {
+        // Wait for the potential value change.
+        await new Promise(resolve => {
+          input.automationNode.addEventListener(
+              chrome.automation.EventType.VALUE_CHANGED, event => {
+                if (event.target.value === 'q') {
+                  resolve();
+                }
+              });
+        });
+      }
+    });
+
+// TODO(crbug.com/40946640): Test is flaky.
+AX_TEST_F(
+    'SwitchAccessItemScanManagerTest', 'DISABLED_DismissVirtualKeyboard',
+    async function() {
+      const website =
+          `<input type="text" id="testinput"></input><button>ok</button>`;
+      const rootWebArea = await this.runWithLoadedTree(website);
+
+      // Move to the text field.
+      Navigator.byItem.moveTo_(this.findNodeById('testinput'));
+      const input = Navigator.byItem.node_;
+      assertEquals(
+          'testinput', input.automationNode.htmlId,
+          'Current node is not input');
+      input.performAction(MenuAction.KEYBOARD);
+
+      const keyboard =
+          await this.untilFocusIs({role: chrome.automation.RoleType.KEYBOARD});
+      keyboard.performAction('select');
+
+      // Grab the key.
+      const key = await this.untilFocusIs({instance: KeyboardNode});
+
+      // Simulate a page focusing the ok button.
+      const okButton = rootWebArea.find({attributes: {name: 'ok'}});
+      okButton.focus();
+
+      // Wait for the keyboard to become invisible and the ok button to be
+      // focused by automation.
+      await new Promise(
+          resolve => okButton.addEventListener(
+              chrome.automation.EventType.FOCUS, resolve));
+      await new Promise(resolve => {
+        keyboard.automationNode.addEventListener(
+            chrome.automation.EventType.STATE_CHANGED, event => {
+              if (event.target.role === chrome.automation.RoleType.KEYBOARD &&
+                  event.target.state.invisible) {
+                resolve();
+              }
+            });
+      });
+
+      // We should end up back on the focused button in SA.
+      const button =
+          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+      assertEquals('ok', button.automationNode.name);
+    });
+
+// TODO(crbug.com/1260231): Test is flaky.
+AX_TEST_F(
+    'SwitchAccessItemScanManagerTest', 'DISABLED_ChildrenChangedDoesNotRefresh',
+    async function() {
+      const website = `
+    <div id="slider" role="slider">
+      <div role="group"><div></div></div>
+    </div>
+    <button>done</button>
+  `;
+      const rootWebArea = await this.runWithLoadedTree(website);
+      // SA initially focuses this node in Ash Chrome; wait for it first.
+      await new Promise(resolve => {
+        chrome.commandLinePrivate.hasSwitch(
+            'lacros-chrome-path', async hasLacrosChromePath => {
+              if (!hasLacrosChromePath) {
+                await this.untilFocusIs(
+                    {className: 'BrowserNonClientFrameViewChromeOS'});
+              }
+              resolve();
+            });
+      });
+
+      // Move to the slider.
+      Navigator.byItem.moveTo_(this.findNodeById('slider'));
+      const slider = Navigator.byItem.node_;
+      assertEquals(
+          'slider', slider.automationNode.htmlId, 'Current node is not slider');
+
+      // Trigger a children changed on the group.
+      const automationGroup =
+          rootWebArea.find({role: chrome.automation.RoleType.GROUP});
+      assertTrue(Boolean(automationGroup));
+      const group = Navigator.byItem.group_;
+      assertTrue(Boolean(group));
+      const handler = group.childrenChangedHandler_;
+      assertTrue(Boolean(handler));
+
+      // Fake a children changed event.
+      handler.eventStack_ = [{
+        type: chrome.automation.EventType.CHILDREN_CHANGED,
+        target: automationGroup,
+      }];
+      handler.handleEvent_();
+
+      // This subtree is not interesting, so it should not have triggered a
+      // complete refresh of the SA tree.
+      assertEquals(slider, Navigator.byItem.node_);
+    });
+
+AX_TEST_F('SwitchAccessItemScanManagerTest', 'InitialFocus', async function() {
+  const website = `<input></input><button autofocus></button>`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  // The button should have initial focus. This ensures we move past the
+  // focus event below.
+  const button =
+      await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+
+  // Build a new ItemScanManager to see what it sets as the initial node.
+  const desktop = rootWebArea.parent.root;
+  assertEquals(
+      chrome.automation.RoleType.DESKTOP, desktop.role,
+      `Unexpected desktop ${desktop.toString()}`);
+  const manager = new ItemScanManager(desktop);
+  manager.start();
+  assertEquals(
+      button.automationNode, manager.node_.automationNode,
+      `Unexpected focus ${manager.node_.debugString()}`);
+});
+
+
+AX_TEST_F(
+    'SwitchAccessItemScanManagerTest', 'SyncFocusToNewWindow',
+    async function() {
+      const website1 = `<button autofocus>one</button>`;
+      const website2 = `<button autofocus>two</button>`;
+      await this.runWithLoadedTree(website1);
+      // Wait for the first button to get SA focused.
+      const button1 = await this.untilFocusIs(
+          {role: chrome.automation.RoleType.BUTTON, name: 'one'});
+
+      // Launch a new browser window and load up the second site.
+      EventGenerator.sendKeyPress(KeyCode.N, {ctrl: true});
+      await this.runWithLoadedTree(website2);
+      // Wait for the second button to get SA focused.
+      const button2 = await this.untilFocusIs(
+          {role: chrome.automation.RoleType.BUTTON, name: 'two'});
+
+      // Do a search for the title bar nodes for each of the browser windows. We
+      // do this by walking up to the widget for each window from the buttons.
+      let widget1 = button1.automationNode;
+      while (widget1.role !== chrome.automation.RoleType.WINDOW ||
+             widget1.className !== 'Widget') {
+        widget1 = widget1.parent;
+      }
+      assertTrue(Boolean(widget1));
+
+      let widget2 = button2.automationNode;
+      while (widget2.role !== chrome.automation.RoleType.WINDOW ||
+             widget2.className !== 'Widget') {
+        widget2 = widget2.parent;
+      }
+      assertTrue(Boolean(widget2));
+
+      const titleBar1 =
+          widget1.find({role: chrome.automation.RoleType.TITLE_BAR});
+      assertTrue(Boolean(titleBar1));
+      const titleBar2 =
+          widget2.find({role: chrome.automation.RoleType.TITLE_BAR});
+      assertTrue(Boolean(titleBar2));
+
+      // The focus is currently on widget2 (since button2 has focus). Start with
+      // focusing widget1 which should occur as a result of moving SA to title
+      // bar 1.
+      Navigator.byItem.moveTo_(titleBar1);
+
+      // Simulate entering this group to trigger the focus.
+      Navigator.byItem.enterGroup();
+
+      // Note that this and the second instance below is system OS focus, but
+      // doesn't impact SA focus to preserve current behavior and prevent
+      // flickering.
+      let currentFocus = await new Promise(r => {
+        widget1.addEventListener(
+            chrome.automation.EventType.FOCUS, e => r(e.target));
+      });
+      assertEquals(currentFocus, button1.automationNode);
+
+      // Now, switch to widget2 by moving to title bar 2.
+      Navigator.byItem.moveTo_(titleBar2);
+
+      // Simulate entering this group to trigger the focus.
+      Navigator.byItem.enterGroup();
+
+      currentFocus = await new Promise(r => {
+        widget2.addEventListener(
+            chrome.automation.EventType.FOCUS, e => r(e.target));
+      });
+      assertEquals(currentFocus, button2.automationNode);
+    });
+
+// TODO(crbug.com/1219067): Unflake.
+AX_TEST_F(
+    'SwitchAccessItemScanManagerTest', 'DISABLED_LockScreenBlocksUserSession',
+    async function() {
+      const website = `<button autofocus>kitties!</button>`;
+      await this.runWithLoadedTree(website);
+      let button =
+          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+      assertEquals('kitties!', button.automationNode.name);
+
+      // Lock the screen.
+      EventGenerator.sendKeyPress(KeyCode.L, {search: true});
+
+      // Wait for focus to move to the password field.
+      await this.untilFocusIs({
+        role: chrome.automation.RoleType.TEXT_FIELD,
+        name: 'Password for stub-user@example.com',
+      });
+
+      // The button is no longer in the tree because the screen is locked.
+      const predicate = node => node.name === 'kitties!' &&
+          node.role === chrome.automation.RoleType.BUTTON;
+      assertNotNullNorUndefined(
+          this.desktop_, 'this.desktop_ is null or undefined.');
+      const treeWalker = new AutomationTreeWalker(
+          this.desktop_, constants.Dir.FORWARD, {visit: predicate});
+      const node = treeWalker.next().node;
+      assertEquals(null, node);
+
+      // Log in again and confirm that the button is back and gets focus
+      // again.
+      EventGenerator.sendKeyPress(KeyCode.T);
+      EventGenerator.sendKeyPress(KeyCode.E);
+      EventGenerator.sendKeyPress(KeyCode.S);
+      EventGenerator.sendKeyPress(KeyCode.T);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.RETURN);
+
+      button =
+          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+      assertEquals('kitties!', button.automationNode.name);
+    });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/menu_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/menu_manager.ts
new file mode 100644
index 0000000..052f0f7
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/menu_manager.ts
@@ -0,0 +1,191 @@
+// 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.
+
+import {ArrayUtil} from '/common/array_util.js';
+import {EventHandler} from '/common/event_handler.js';
+
+import {ActionManager} from './action_manager.js';
+import {Navigator} from './navigator.js';
+import {SwitchAccess} from './switch_access.js';
+
+import AutomationEvent = chrome.automation.AutomationEvent;
+import AutomationNode = chrome.automation.AutomationNode;
+import EventType = chrome.automation.EventType;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+import RoleType = chrome.automation.RoleType;
+import ScreenRect = chrome.accessibilityPrivate.ScreenRect;
+import StateType = chrome.automation.StateType;
+import SwitchAccessBubble = chrome.accessibilityPrivate.SwitchAccessBubble;
+
+interface EventHandlerOptions {
+  capture: boolean|undefined;
+  exactMatch: boolean|undefined;
+  listenOnce: boolean|undefined;
+  predicate: ((arg: any) => boolean)|undefined;
+}
+
+/**
+ * Class to handle interactions with the Switch Access action menu, including
+ * opening and closing the menu and setting its location / the actions to be
+ * displayed.
+ */
+export class MenuManager {
+  private displayedActions_: MenuAction[]|null = null;
+  private displayedLocation_?: ScreenRect;
+  private isMenuOpen_ = false;
+  private menuAutomationNode_?: AutomationNode|null;
+  private clickHandler_: EventHandler;
+
+  static instance?: MenuManager;
+
+  private constructor() {
+    this.clickHandler_ = new EventHandler(
+        [], EventType.CLICKED,
+        (event: AutomationEvent) => this.onButtonClicked_(event));
+  }
+
+  static create(): MenuManager {
+    if (MenuManager.instance) {
+      throw new Error('Cannot instantiate more than one MenuManager');
+    }
+    MenuManager.instance = new MenuManager();
+    return MenuManager.instance;
+  }
+
+  // ================= Static Methods ==================
+
+  static isMenuOpen(): boolean {
+    // TODO(b/314203187): Not nulls asserted, check that this is correct.
+    return Boolean(MenuManager.instance) && MenuManager.instance!.isMenuOpen_;
+  }
+
+  static get menuAutomationNode(): AutomationNode|null|undefined {
+    if (MenuManager.instance) {
+      return MenuManager.instance.menuAutomationNode_;
+    }
+    return null;
+  }
+
+  // ================ Instance Methods =================
+
+  /**
+   * If multiple actions are available for the currently highlighted node,
+   * opens the menu. Otherwise performs the node's default action.
+   */
+  open(actions: MenuAction[], location?: ScreenRect): void {
+    if (!this.isMenuOpen_) {
+      if (!location) {
+        return;
+      }
+      this.displayedLocation_ = location;
+    }
+
+    if (ArrayUtil.contentsAreEqual(
+            actions, this.displayedActions_ ?? undefined)) {
+      return;
+    }
+    this.displayMenuWithActions_(actions);
+  }
+
+  /** Exits the menu. */
+  close(): void {
+    this.isMenuOpen_ = false;
+    this.displayedActions_ = null;
+    // To match the accessibilityPrivate function signature, displayedLocation_
+    // has to be undefined rather than null.
+    this.displayedLocation_ = undefined;
+    Navigator.byItem.exitIfInGroup(this.menuAutomationNode_ ?? null);
+    this.menuAutomationNode_ = null;
+
+    chrome.accessibilityPrivate.updateSwitchAccessBubble(
+        SwitchAccessBubble.MENU, false /* show */);
+  }
+
+  // ================= Private Methods ==================
+
+  private asAction_(actionString: string|undefined): MenuAction|null {
+    if (Object.values(MenuAction).includes(actionString as MenuAction)) {
+      return actionString as MenuAction;
+    }
+    return null;
+  }
+
+  /**
+   * Opens or reloads the menu for the current action node with the specified
+   * actions.
+   */
+  private displayMenuWithActions_(actions: MenuAction[]): void {
+    chrome.accessibilityPrivate.updateSwitchAccessBubble(
+        SwitchAccessBubble.MENU, true /* show */, this.displayedLocation_,
+        actions);
+
+    this.isMenuOpen_ = true;
+    this.findAndJumpToMenu_();
+    this.displayedActions_ = actions;
+  }
+
+  /**
+   * Searches the automation tree to find the node for the Switch Access menu.
+   * If we've already found a node, and it's still valid, then jump to that
+   * node.
+   */
+  private findAndJumpToMenu_(): void {
+    if (this.hasMenuNode_() && this.menuAutomationNode_) {
+      this.jumpToMenu_(this.menuAutomationNode_);
+      return;
+    }
+    SwitchAccess.findNodeMatching(
+        {
+          role: RoleType.MENU,
+          attributes: {className: 'SwitchAccessMenuView'},
+        },
+        node => this.jumpToMenu_(node));
+  }
+
+  private hasMenuNode_(): boolean {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    return Boolean(
+        this.menuAutomationNode_ && this.menuAutomationNode_.role &&
+        !this.menuAutomationNode_.state![StateType.OFFSCREEN]);
+  }
+
+  /**
+   * Saves the automation node representing the menu, adds all listeners, and
+   * jumps to the node.
+   */
+  private jumpToMenu_(node: AutomationNode): void {
+    if (!this.isMenuOpen_) {
+      return;
+    }
+
+    // If the menu hasn't fully loaded, wait for that before jumping.
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (node.children.length < 1 ||
+        node.firstChild!.state![StateType.OFFSCREEN]) {
+      new EventHandler(
+          node, [EventType.CHILDREN_CHANGED, EventType.LOCATION_CHANGED],
+          () => this.jumpToMenu_(node),
+          {listenOnce: true} as EventHandlerOptions)
+          .start();
+      return;
+    }
+
+    this.menuAutomationNode_ = node;
+    this.clickHandler_.setNodes(this.menuAutomationNode_);
+    this.clickHandler_.start();
+    Navigator.byItem.jumpTo(this.menuAutomationNode_);
+  }
+
+  /**
+   * Listener for when buttons are clicked. Identifies the action to perform
+   * and forwards the request to the action manager.
+   */
+  private onButtonClicked_(event: AutomationEvent): void {
+    const selectedAction = this.asAction_(event.target.value);
+    if (!this.isMenuOpen_ || !selectedAction) {
+      return;
+    }
+    ActionManager.performAction(selectedAction as MenuAction);
+  }
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/metrics.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/metrics.ts
new file mode 100644
index 0000000..832187d
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/metrics.ts
@@ -0,0 +1,16 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {StringUtil} from '/common/string_util.js';
+
+/**
+ * Class to record metrics for Switch Access.
+ */
+export namespace SwitchAccessMetrics {
+  export function recordMenuAction(menuAction: string): void {
+    const metricName = 'Accessibility.CrosSwitchAccess.MenuAction.' +
+        StringUtil.toUpperCamelCase(menuAction);
+    chrome.metricsPrivate.recordUserAction(metricName);
+  }
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/navigator.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/navigator.ts
new file mode 100644
index 0000000..75fe646
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/navigator.ts
@@ -0,0 +1,43 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {ItemScanManager} from './item_scan_manager.js';
+import {ItemNavigatorInterface, PointNavigatorInterface} from './navigator_interfaces.js';
+import {PointScanManager} from './point_scan_manager.js';
+import {SwitchAccess} from './switch_access.js';
+import {ErrorType} from './switch_access_constants.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+
+export class Navigator {
+  private static itemManager_?: ItemScanManager;
+  private static pointManager_?: PointScanManager;
+
+  static initializeSingletonInstances(desktop: AutomationNode): void {
+    Navigator.itemManager_ = new ItemScanManager(desktop);
+    Navigator.pointManager_ = new PointScanManager();
+  }
+
+  static get byItem(): ItemNavigatorInterface {
+    if (!Navigator.itemManager_) {
+      throw SwitchAccess.error(
+          ErrorType.UNINITIALIZED,
+          'Cannot access itemManager before Navigator.init()');
+    }
+    return Navigator.itemManager_;
+  }
+
+  static get byPoint(): PointNavigatorInterface {
+    if (!Navigator.pointManager_) {
+      throw SwitchAccess.error(
+          ErrorType.UNINITIALIZED,
+          'Cannot access pointManager before Navigator.init()');
+    }
+    return Navigator.pointManager_;
+  }
+}
+
+TestImportManager.exportForTesting(Navigator);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/navigator_interfaces.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/navigator_interfaces.ts
new file mode 100644
index 0000000..1784037
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/navigator_interfaces.ts
@@ -0,0 +1,116 @@
+// 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 {constants} from '/common/constants.js';
+
+import {SAChildNode, SANode, SARootNode} from './nodes/switch_access_node.js';
+
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+type AutomationNode = chrome.automation.AutomationNode;
+type Point = constants.Point;
+
+export abstract class ItemNavigatorInterface {
+  abstract currentGroupHasChild(node: SAChildNode): boolean;
+
+  /** Enters |this.node_|. */
+  abstract enterGroup(): void;
+
+  /**
+   * Puts focus on the virtual keyboard, if the current node is a text input.
+   */
+  abstract enterKeyboard(): void;
+
+  /** Unconditionally exits the current group. */
+  abstract exitGroupUnconditionally(): void;
+
+  /** Exits the specified node, if it is the currently focused group. */
+  abstract exitIfInGroup(node: (AutomationNode|SANode|null)): void;
+
+  abstract exitKeyboard(): Promise<void>;
+
+  /**
+   * Forces the current node to be |node|.
+   * Should only be called by subclasses of SARootNode and
+   *    only when they are focused.
+   */
+  abstract forceFocusedNode(node: SAChildNode): void;
+
+  /**
+   * Returns the current Switch Access tree, for debugging purposes.
+   * @param wholeTree Whether to print the whole tree, or just the
+   * current focus.
+   */
+  abstract getTreeForDebugging(wholeTree: boolean): SARootNode;
+
+  /**
+   * Jumps to a specific automation node. Maintains the history when
+   * navigating.
+   */
+  abstract jumpTo(automationNode: AutomationNode): void;
+
+  /** Move to the previous interesting node. */
+  abstract moveBackward(): void;
+
+  /** Move to the next interesting node. */
+  abstract moveForward(): void;
+
+  /**
+   * Tries to move to another node, |node|, but if |node| is a window that's not
+   * in the foreground it will use |getNext| to find the next node to try.
+   * Checks against |startingNode| to ensure we don't get stuck in an infinite
+   * loop.
+   * @param node The node to try to move into.
+   * @param getNext gets the next node to
+   *     try if we cannot move to |next|. Takes |next| as a parameter.
+   * @param startingNode The first node in the sequence. If we
+   *     loop back to this node, stop trying to move, as there are no other
+   *     nodes we can move to.
+   */
+  abstract tryMoving(
+      _node: SAChildNode, _getNext: (node: SAChildNode) => SAChildNode,
+      _startingNode: SAChildNode): Promise<void>;
+
+  /**
+   * Moves to the Switch Access focus up the group stack closest to the ancestor
+   * that hasn't been invalidated.
+   */
+  abstract moveToValidNode(): void;
+
+  /** Restarts item scanning from the last point chosen by point scanning. */
+  abstract restart(): void;
+
+  /** Restores the suspended group and focus, if there is one. */
+  abstract restoreSuspendedGroup(): void;
+
+  /** Saves the current focus and group, and then exits the group. */
+  abstract suspendCurrentGroup(): void;
+
+  /**
+   * Called when everything has been initialized to add the listeners and find
+   * the initial focus.
+   */
+  abstract start(): void;
+
+  // =============== Getter Methods ==============
+
+  /** Returns the currently focused node. */
+  abstract get currentNode(): SAChildNode;
+
+  /** Returns the desktop automation node object. */
+  abstract get desktopNode(): AutomationNode;
+}
+
+export abstract class PointNavigatorInterface {
+  /** Returns the current point scan point. */
+  abstract get currentPoint(): Point;
+
+  /** Starts point scanning. */
+  abstract start(): void;
+
+  /** Stops point scanning. */
+  abstract stop(): void;
+
+  /** Performs a mouse action at the currentPoint(). */
+  abstract performMouseAction(action: MenuAction): void;
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/back_button_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/back_button_node.ts
new file mode 100644
index 0000000..e6af383
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/back_button_node.ts
@@ -0,0 +1,179 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {EventHandler} from '/common/event_handler.js';
+import {RepeatedEventHandler} from '/common/repeated_event_handler.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {ActionManager} from '../action_manager.js';
+import {FocusRingManager} from '../focus_ring_manager.js';
+import {MenuManager} from '../menu_manager.js';
+import {Navigator} from '../navigator.js';
+import {SwitchAccess} from '../switch_access.js';
+import {ActionResponse} from '../switch_access_constants.js';
+
+import {SAChildNode, SARootNode} from './switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+const EventType = chrome.automation.EventType;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+type Rect = chrome.automation.Rect;
+import RoleType = chrome.automation.RoleType;
+
+/**
+ * This class handles the behavior of the back button.
+ */
+export class BackButtonNode extends SAChildNode {
+  /** The group that the back button is shown for. */
+  private group_: SARootNode;
+  private locationChangedHandler_?: RepeatedEventHandler;
+
+  private static automationNode_: AutomationNode;
+  private static clickHandler_: EventHandler;
+  static locationForTesting?: Rect;
+
+  constructor(group: SARootNode) {
+    super();
+    this.group_ = group;
+  }
+
+  // ================= Getters and setters =================
+
+  override get actions(): MenuAction[] {
+    return [MenuAction.SELECT];
+  }
+
+  override get automationNode(): AutomationNode {
+    return BackButtonNode.automationNode_;
+  }
+
+  override get group(): SARootNode {
+    return this.group_;
+  }
+
+  override get location(): Rect|undefined {
+    if (BackButtonNode.locationForTesting) {
+      return BackButtonNode.locationForTesting;
+    }
+    if (this.automationNode) {
+      return this.automationNode.location;
+    }
+    return undefined;
+  }
+
+  override get role(): RoleType {
+    return RoleType.BUTTON;
+  }
+
+  // ================= General methods =================
+
+  override asRootNode(): SARootNode|undefined {
+    return undefined;
+  }
+
+  override equals(other: SAChildNode): boolean {
+    return other instanceof BackButtonNode;
+  }
+
+  override isEquivalentTo(node: SAChildNode|SARootNode|AutomationNode|null|
+                          undefined): boolean {
+    return node instanceof BackButtonNode || this.automationNode === node;
+  }
+
+  override isGroup(): boolean {
+    return false;
+  }
+
+  override isValidAndVisible(): boolean {
+    return this.group_.isValidGroup();
+  }
+
+  override onFocus(): void {
+    super.onFocus();
+    chrome.accessibilityPrivate.updateSwitchAccessBubble(
+        chrome.accessibilityPrivate.SwitchAccessBubble.BACK_BUTTON,
+        true /* show */, this.group_.location);
+    BackButtonNode.findAutomationNode_();
+
+    this.locationChangedHandler_ = new RepeatedEventHandler(
+        this.group_.automationNode,
+        chrome.automation.EventType.LOCATION_CHANGED,
+        () => FocusRingManager.setFocusedNode(this),
+        {exactMatch: true, allAncestors: true});
+  }
+
+  override onUnfocus(): void {
+    super.onUnfocus();
+    chrome.accessibilityPrivate.updateSwitchAccessBubble(
+        chrome.accessibilityPrivate.SwitchAccessBubble.BACK_BUTTON,
+        false /* show */);
+
+    if (this.locationChangedHandler_) {
+      this.locationChangedHandler_.stop();
+    }
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    if (action === MenuAction.SELECT && this.automationNode) {
+      BackButtonNode.onClick_();
+      return ActionResponse.CLOSE_MENU;
+    }
+    return ActionResponse.NO_ACTION_TAKEN;
+  }
+
+  override ignoreWhenComputingUnionOfBoundingBoxes(): boolean {
+    return true;
+  }
+
+  // ================= Debug methods =================
+
+  override debugString(wholeTree: boolean, prefix = '', currentNode = null):
+      string {
+    if (!this.automationNode) {
+      return 'BackButtonNode';
+    }
+    return super.debugString(wholeTree, prefix, currentNode);
+  }
+
+  // ================= Static methods =================
+
+  /** Looks for the back button automation node. */
+  private static findAutomationNode_(): void {
+    if (BackButtonNode.automationNode_ && BackButtonNode.automationNode_.role) {
+      return;
+    }
+    SwitchAccess.findNodeMatching(
+        {
+          role: RoleType.BUTTON,
+          attributes: {className: 'SwitchAccessBackButtonView'},
+        },
+        BackButtonNode.saveAutomationNode_);
+  }
+
+  /**
+   * This function defines the behavior that should be taken when the back
+   * button is pressed.
+   */
+  private static onClick_(): void {
+    if (MenuManager.isMenuOpen()) {
+      ActionManager.exitCurrentMenu();
+    } else {
+      Navigator.byItem.exitGroupUnconditionally();
+    }
+  }
+
+  /** Saves the back button automation node. */
+  private static saveAutomationNode_(automationNode: AutomationNode): void {
+    BackButtonNode.automationNode_ = automationNode;
+
+    if (BackButtonNode.clickHandler_) {
+      BackButtonNode.clickHandler_.setNodes(automationNode);
+    } else {
+      BackButtonNode.clickHandler_ = new EventHandler(
+          automationNode, EventType.CLICKED, BackButtonNode.onClick_);
+    }
+  }
+}
+
+TestImportManager.exportForTesting(BackButtonNode);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/basic_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/basic_node.ts
new file mode 100644
index 0000000..c3d6d4e
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/basic_node.ts
@@ -0,0 +1,425 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {AutomationPredicate} from '/common/automation_predicate.js';
+import {constants} from '/common/constants.js';
+import {RepeatedEventHandler} from '/common/repeated_event_handler.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+import {AutomationTreeWalker} from '/common/tree_walker.js';
+
+import {SACache} from '../cache.js';
+import {FocusRingManager} from '../focus_ring_manager.js';
+import {Navigator} from '../navigator.js';
+import {SwitchAccess} from '../switch_access.js';
+import {ActionResponse, ErrorType} from '../switch_access_constants.js';
+import {SwitchAccessPredicate} from '../switch_access_predicate.js';
+
+import {BackButtonNode} from './back_button_node.js';
+import {SAChildNode, SARootNode} from './switch_access_node.js';
+
+import AutomationActionType = chrome.automation.ActionType;
+type AutomationNode = chrome.automation.AutomationNode;
+import EventType = chrome.automation.EventType;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+type Rect = chrome.automation.Rect;
+type RoleType = chrome.automation.RoleType;
+
+interface Creator {
+  predicate: AutomationPredicate.Unary;
+  creator: (node: AutomationNode, parent: SARootNode|null) => BasicNode;
+}
+
+interface RootBuilder {
+  predicate: AutomationPredicate.Unary;
+  builder: (node: AutomationNode) => BasicRootNode;
+}
+
+/**
+ * This class handles interactions with an onscreen element based on a single
+ * AutomationNode.
+ */
+export class BasicNode extends SAChildNode {
+  private baseNode_: AutomationNode;
+  private parent_: SARootNode|null;
+  private locationChangedHandler_?: RepeatedEventHandler;
+  private isActionable_: boolean;
+  private static creators_: Creator[] = [];
+
+  protected constructor(baseNode: AutomationNode, parent: SARootNode|null) {
+    super();
+    this.baseNode_ = baseNode;
+    this.parent_ = parent;
+    this.isActionable_ = !this.isGroup() ||
+        SwitchAccessPredicate.isActionable(baseNode, new SACache());
+  }
+
+  // ================= Getters and setters =================
+
+  override get actions(): MenuAction[] {
+    const actions: MenuAction[] = [];
+    if (this.isActionable_) {
+      actions.push(MenuAction.SELECT);
+    }
+    if (this.isGroup()) {
+      actions.push(MenuAction.DRILL_DOWN);
+    }
+
+    const ancestor = this.getScrollableAncestor_();
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (ancestor.scrollable) {
+      if (ancestor.scrollX! > ancestor.scrollXMin!) {
+        actions.push(MenuAction.SCROLL_LEFT);
+      }
+      if (ancestor.scrollX! < ancestor.scrollXMax!) {
+        actions.push(MenuAction.SCROLL_RIGHT);
+      }
+      if (ancestor.scrollY! > ancestor.scrollYMin!) {
+        actions.push(MenuAction.SCROLL_UP);
+      }
+      if (ancestor.scrollY! < ancestor.scrollYMax!) {
+        actions.push(MenuAction.SCROLL_DOWN);
+      }
+    }
+    // Coerce enums to string arrays for comparison.
+    const menuActions: string[] = Object.values(MenuAction);
+    const standardActions: string[] = this.baseNode_.standardActions!.filter(
+        (action: string) => menuActions.includes(action));
+    return actions.concat(standardActions as MenuAction[]);
+  }
+
+  override get automationNode(): AutomationNode {
+    return this.baseNode_;
+  }
+
+  override get location(): Rect|undefined {
+    return this.baseNode_.location;
+  }
+
+  override get role(): RoleType {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    return this.baseNode_.role!;
+  }
+
+  // ================= General methods =================
+
+  override asRootNode(): SARootNode|undefined {
+    if (!this.isGroup()) {
+      return undefined;
+    }
+    return BasicRootNode.buildTree(this.baseNode_);
+  }
+
+  override equals(rhs: SAChildNode|null|undefined): boolean {
+    if (!rhs || !(rhs instanceof BasicNode)) {
+      return false;
+    }
+
+    const other = rhs as BasicNode;
+    return other.baseNode_ === this.baseNode_;
+  }
+
+  override isEquivalentTo(node: AutomationNode|SAChildNode|SARootNode|
+                          null): boolean {
+    if (node instanceof BasicNode) {
+      return this.baseNode_ === node.baseNode_;
+    }
+    if (node instanceof BasicRootNode) {
+      return this.baseNode_ === node.automationNode;
+    }
+
+    if (node instanceof SAChildNode) {
+      return node.isEquivalentTo(this);
+    }
+    return this.baseNode_ === node;
+  }
+
+  override isGroup(): boolean {
+    const cache = new SACache();
+    return SwitchAccessPredicate.isGroup(this.baseNode_, this.parent_, cache);
+  }
+
+  override isValidAndVisible(): boolean {
+    // Nodes may have been deleted or orphaned.
+    if (!this.baseNode_ || !this.baseNode_.role) {
+      return false;
+    }
+    return SwitchAccessPredicate.isVisible(this.baseNode_) &&
+        super.isValidAndVisible();
+  }
+
+  override onFocus(): void {
+    super.onFocus();
+    this.locationChangedHandler_ = new RepeatedEventHandler(
+        this.baseNode_, EventType.LOCATION_CHANGED, () => {
+          if (this.isValidAndVisible()) {
+            FocusRingManager.setFocusedNode(this);
+          } else {
+            Navigator.byItem.moveToValidNode();
+          }
+        }, {exactMatch: true, allAncestors: true});
+  }
+
+  override onUnfocus(): void {
+    super.onUnfocus();
+    if (this.locationChangedHandler_) {
+      this.locationChangedHandler_.stop();
+    }
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    let ancestor;
+    switch (action) {
+      case MenuAction.DRILL_DOWN:
+        if (this.isGroup()) {
+          Navigator.byItem.enterGroup();
+          return ActionResponse.CLOSE_MENU;
+        }
+        // Should not happen.
+        console.error('Action DRILL_DOWN received on non-group node.');
+        return ActionResponse.NO_ACTION_TAKEN;
+      case MenuAction.SELECT:
+        this.baseNode_.doDefault();
+        return ActionResponse.CLOSE_MENU;
+      case MenuAction.SCROLL_DOWN:
+        ancestor = this.getScrollableAncestor_();
+        if (ancestor.scrollable) {
+          ancestor.scrollDown(() => {});
+        }
+        return ActionResponse.RELOAD_MENU;
+      case MenuAction.SCROLL_UP:
+        ancestor = this.getScrollableAncestor_();
+        if (ancestor.scrollable) {
+          ancestor.scrollUp(() => {});
+        }
+        return ActionResponse.RELOAD_MENU;
+      case MenuAction.SCROLL_RIGHT:
+        ancestor = this.getScrollableAncestor_();
+        if (ancestor.scrollable) {
+          ancestor.scrollRight(() => {});
+        }
+        return ActionResponse.RELOAD_MENU;
+      case MenuAction.SCROLL_LEFT:
+        ancestor = this.getScrollableAncestor_();
+        if (ancestor.scrollable) {
+          ancestor.scrollLeft(() => {});
+        }
+        return ActionResponse.RELOAD_MENU;
+      default:
+        const actions = Object.values(AutomationActionType);
+        const automationAction = actions.find((a: string) => a === action);
+        if (automationAction) {
+          this.baseNode_.performStandardAction(automationAction);
+        }
+        return ActionResponse.CLOSE_MENU;
+    }
+  }
+
+  // ================= Private methods =================
+
+  protected getScrollableAncestor_(): AutomationNode {
+    let ancestor = this.baseNode_;
+    while (!ancestor.scrollable && ancestor.parent) {
+      ancestor = ancestor.parent;
+    }
+    return ancestor;
+  }
+
+  // ================= Static methods =================
+
+  static create(baseNode: AutomationNode, parent: SARootNode|null): BasicNode {
+    const item = BasicNode.creators.find(
+        (creator: Creator) => creator.predicate(baseNode));
+    if (item) {
+      return item.creator(baseNode, parent);
+    }
+    return new BasicNode(baseNode, parent);
+  }
+
+  static get creators(): Creator[] {
+    return BasicNode.creators_;
+  }
+}
+
+/**
+ * This class handles constructing and traversing a group of onscreen elements
+ * based on all the interesting descendants of a single AutomationNode.
+ */
+export class BasicRootNode extends SARootNode {
+  private static builders_: RootBuilder[] = [];
+
+  private childrenChangedHandler_?: RepeatedEventHandler;
+  private invalidated_ = false;
+
+  /**
+   * WARNING: If you call this constructor, you must *explicitly* set children.
+   *     Use the static function BasicRootNode.buildTree for most use cases.
+   */
+  constructor(baseNode: AutomationNode) {
+    super(baseNode);
+  }
+
+  // ================= Getters and setters =================
+
+  override get location(): Rect {
+    return this.automationNode.location || super.location;
+  }
+
+  // ================= General methods =================
+
+  override equals(other: SARootNode|null|undefined): boolean {
+    if (!(other instanceof BasicRootNode)) {
+      return false;
+    }
+    return super.equals(other) && this.automationNode === other.automationNode;
+  }
+
+  override isEquivalentTo(node: AutomationNode|SAChildNode|SARootNode|
+                          null): boolean {
+    if (node instanceof BasicRootNode || node instanceof BasicNode) {
+      return this.automationNode === node.automationNode;
+    }
+
+    if (node instanceof SAChildNode) {
+      return node.isEquivalentTo(this);
+    }
+    return this.automationNode === node;
+  }
+
+  override isValidGroup(): boolean {
+    if (!this.automationNode.role) {
+      // If the underlying automation node has been invalidated, return false.
+      return false;
+    }
+    return !this.invalidated_ &&
+        SwitchAccessPredicate.isVisible(this.automationNode) &&
+        super.isValidGroup();
+  }
+
+  override onFocus(): void {
+    super.onFocus();
+    this.childrenChangedHandler_ = new RepeatedEventHandler(
+        this.automationNode, EventType.CHILDREN_CHANGED, event => {
+          const cache = new SACache();
+          if (SwitchAccessPredicate.isInterestingSubtree(event.target, cache)) {
+            this.refresh();
+          }
+        });
+  }
+
+  override onUnfocus(): void {
+    super.onUnfocus();
+    if (this.childrenChangedHandler_) {
+      this.childrenChangedHandler_.stop();
+    }
+  }
+
+  override refreshChildren(): void {
+    const childConstructor = (node: AutomationNode): BasicNode =>
+        BasicNode.create(node, this);
+    try {
+      BasicRootNode.findAndSetChildren(this, childConstructor);
+    } catch (e) {
+      this.invalidated_ = true;
+    }
+  }
+
+  override refresh(): void {
+    // Find the currently focused child.
+    let focusedChild: SAChildNode|null = null;
+    for (const child of this.children) {
+      if (child.isFocused()) {
+        focusedChild = child;
+        break;
+      }
+    }
+
+    // Update this BasicRootNode's children.
+    this.refreshChildren();
+    if (this.invalidated_) {
+      this.onUnfocus();
+      Navigator.byItem.moveToValidNode();
+      return;
+    }
+
+    // Set the new instance of that child to be the focused node.
+    if (focusedChild) {
+      for (const child of this.children) {
+        if (child.isEquivalentTo(focusedChild)) {
+          Navigator.byItem.forceFocusedNode(child);
+          return;
+        }
+      }
+    }
+
+    // If we didn't find a match, fall back and reset.
+    Navigator.byItem.moveToValidNode();
+  }
+
+  // ================= Static methods =================
+
+  static buildTree(rootNode: AutomationNode): BasicRootNode {
+    const item = BasicRootNode.builders.find(
+        (builder: RootBuilder) => builder.predicate(rootNode));
+    if (item) {
+      return item.builder(rootNode);
+    }
+
+    const root = new BasicRootNode(rootNode);
+    const childConstructor = (node: AutomationNode): BasicNode =>
+        BasicNode.create(node, root);
+
+    BasicRootNode.findAndSetChildren(root, childConstructor);
+    return root;
+  }
+
+  /**
+   * Helper function to connect tree elements, given the root node and a
+   * constructor for the child type.
+   * @param childConstructor Constructs a child node from an automation node.
+   */
+  static findAndSetChildren(
+      root: BasicRootNode,
+      childConstructor: (node: AutomationNode) => SAChildNode): void {
+    const interestingChildren = BasicRootNode.getInterestingChildren(root);
+    const children = interestingChildren.map(childConstructor)
+                         .filter(child => child.isValidAndVisible());
+
+    if (children.length < 1) {
+      throw SwitchAccess.error(
+          ErrorType.NO_CHILDREN,
+          'Root node must have at least 1 interesting child.',
+          true /* shouldRecover */);
+    }
+    children.push(new BackButtonNode(root));
+    root.children = children;
+  }
+
+  static getInterestingChildren(root: BasicRootNode|
+                                AutomationNode): AutomationNode[] {
+    if (root instanceof BasicRootNode) {
+      root = root.automationNode;
+    }
+
+    if (root.children.length === 0) {
+      return [];
+    }
+    const interestingChildren: AutomationNode[] = [];
+    const treeWalker = new AutomationTreeWalker(
+        root, constants.Dir.FORWARD, SwitchAccessPredicate.restrictions(root));
+    let node = treeWalker.next().node;
+
+    while (node) {
+      interestingChildren.push(node);
+      node = treeWalker.next().node;
+    }
+
+    return interestingChildren;
+  }
+
+  static get builders(): RootBuilder[] {
+    return BasicRootNode.builders_;
+  }
+}
+
+TestImportManager.exportForTesting(BasicNode, BasicRootNode);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/basic_node_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/basic_node_test.js
new file mode 100644
index 0000000..92783d7
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/basic_node_test.js
@@ -0,0 +1,194 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+GEN_INCLUDE(['../switch_access_e2e_test_base.js']);
+
+/** Test fixture for the node wrapper type. */
+SwitchAccessBasicNodeTest = class extends SwitchAccessE2ETest {
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    globalThis.MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+  }
+};
+
+AX_TEST_F('SwitchAccessBasicNodeTest', 'AsRootNode', async function() {
+  const website = `<div aria-label="outer">
+                     <div aria-label="inner">
+                       <input type="range">
+                       <button></button>
+                     </div>
+                     <button></button>
+                   </div>`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const slider = rootWebArea.find({role: chrome.automation.RoleType.SLIDER});
+  const inner = slider.parent;
+  assertNotEquals(undefined, inner, 'Could not find inner group');
+  const outer = inner.parent;
+  assertNotEquals(undefined, outer, 'Could not find outer group');
+
+  const outerRootNode = BasicRootNode.buildTree(outer, null);
+  const innerNode = outerRootNode.firstChild;
+  assertTrue(innerNode.isGroup(), 'Inner group node is not a group');
+
+  const innerRootNode = innerNode.asRootNode();
+  assertEquals(3, innerRootNode.children.length, 'Expected 3 children');
+  const sliderNode = innerRootNode.firstChild;
+  assertEquals(
+      chrome.automation.RoleType.SLIDER, sliderNode.role,
+      'First child should be a slider');
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, sliderNode.next.role,
+      'Second child should be a button');
+  assertTrue(
+      innerRootNode.lastChild instanceof BackButtonNode,
+      'Final child should be the back button');
+});
+
+TEST_F('SwitchAccessBasicNodeTest', 'Equals', function() {
+  this.runWithLoadedDesktop(desktop => {
+    const desktopNode = DesktopNode.build(desktop);
+
+    let childGroup = desktopNode.firstChild;
+    let i = 0;
+    while (!childGroup.isGroup() && i < desktopNode.children.length) {
+      childGroup = childGroup.next;
+      i++;
+    }
+    childGroup = childGroup.asRootNode();
+
+    assertFalse(desktopNode.equals(), 'Root node equals nothing');
+    assertFalse(
+        desktopNode.equals(new SARootNode()),
+        'Different type root nodes are equal');
+    assertFalse(
+        new SARootNode().equals(desktopNode),
+        'Equals is not symmetric? Different types of root are equal');
+    assertFalse(
+        desktopNode.equals(childGroup),
+        'Groups with different children are equal');
+    assertFalse(
+        childGroup.equals(desktopNode),
+        'Equals is not symmetric? Groups with different children are equal');
+
+    assertTrue(
+        desktopNode.equals(desktopNode),
+        'Equals is not reflexive? (root node)');
+    const desktopCopy = DesktopNode.build(desktop);
+    assertTrue(
+        desktopNode.equals(desktopCopy), 'Two desktop roots are not equal');
+    assertTrue(
+        desktopCopy.equals(desktopNode),
+        'Equals is not symmetric? Two desktop roots aren\'t equal');
+
+    const wrappedNode = desktopNode.firstChild;
+    assertTrue(
+        wrappedNode instanceof BasicNode,
+        'Child node is not of type BasicNode');
+    assertGT(desktopNode.children.length, 1, 'Desktop root has only 1 child');
+
+    assertFalse(wrappedNode.equals(), 'Child BasicNode equals nothing');
+    assertFalse(
+        wrappedNode.equals(new BackButtonNode()),
+        'Child BasicNode equals a BackButtonNode');
+    assertFalse(
+        new BackButtonNode().equals(wrappedNode),
+        'Equals is not symmetric? BasicNode equals a BackButtonNode');
+    assertFalse(
+        wrappedNode.equals(desktopNode.lastChild),
+        'Children with different base nodes are equal');
+    assertFalse(
+        desktopNode.lastChild.equals(wrappedNode),
+        'Equals is not symmetric? Nodes with different base nodes are equal');
+
+    const equivalentWrappedNode =
+        BasicNode.create(wrappedNode.baseNode_, desktopNode);
+    assertTrue(
+        wrappedNode.equals(wrappedNode),
+        'Equals is not reflexive? (child node)');
+    assertTrue(
+        wrappedNode.equals(equivalentWrappedNode),
+        'Two nodes with the same base node are not equal');
+    assertTrue(
+        equivalentWrappedNode.equals(wrappedNode),
+        'Equals is not symmetric? Nodes with the same base node aren\'t equal');
+  });
+});
+
+AX_TEST_F('SwitchAccessBasicNodeTest', 'Actions', async function() {
+  const website = `<input type="text">
+                   <div role="button" aria-label="group">
+                     <button>A</button>
+                     <button>B</button>
+                   </div>
+                   <input type="range" min=1 max=5 value=3>`;
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const textField = BasicNode.create(
+      rootWebArea.find({role: chrome.automation.RoleType.TEXT_FIELD}),
+      new SARootNode());
+
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, textField.role,
+      'Text field node is not a text field');
+  assertTrue(
+      textField.hasAction(MenuAction.KEYBOARD),
+      'Text field does not have action KEYBOARD');
+  assertTrue(
+      textField.hasAction(MenuAction.DICTATION),
+      'Text field does not have action DICTATION');
+  assertFalse(
+      textField.hasAction(MenuAction.SELECT), 'Text field has action SELECT');
+
+  const buttonGroup = BasicNode.create(
+      rootWebArea.find({
+        role: chrome.automation.RoleType.BUTTON,
+        attributes: {name: 'group'},
+      }),
+      new SARootNode());
+  assertNotNullNorUndefined(buttonGroup);
+
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, buttonGroup.role,
+      'Button group node is not a button');
+  assertTrue(
+      buttonGroup.hasAction(MenuAction.SELECT),
+      'Button group does not have action SELECT');
+  assertFalse(
+      buttonGroup.hasAction(MenuAction.KEYBOARD), 'Button has action KEYBOARD');
+  assertFalse(
+      buttonGroup.hasAction(MenuAction.DICTATION),
+      'Button has action DICTATION');
+  assertTrue(buttonGroup.isGroup(), 'Button group is not a group');
+  assertTrue(
+      buttonGroup.hasAction(MenuAction.DRILL_DOWN),
+      'Button group does not have action DRILL_DOWN');
+  assertTrue(
+      buttonGroup.asRootNode().children.length === 3,
+      'Button group does not have three children (A, B, and the back button)');
+
+  const buttonA = buttonGroup.asRootNode().firstChild;
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, buttonA.role,
+      'Button node A is not a button');
+  assertTrue(
+      buttonA.hasAction(MenuAction.SELECT),
+      'Button A does not have action SELECT');
+  assertFalse(
+      buttonA.hasAction(MenuAction.DRILL_DOWN),
+      'Button A should not have action DRILL_DOWN');
+  assertFalse(buttonA.isGroup(), 'Button A should not be a group');
+
+  const slider = BasicNode.create(
+      rootWebArea.find({role: chrome.automation.RoleType.SLIDER}),
+      new SARootNode());
+
+  assertEquals(
+      chrome.automation.RoleType.SLIDER, slider.role,
+      'Slider node is not a slider');
+  assertTrue(
+      slider.hasAction(MenuAction.INCREMENT),
+      'Slider does not have action INCREMENT');
+  assertTrue(
+      slider.hasAction(MenuAction.DECREMENT),
+      'Slider does not have action DECREMENT');
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/combo_box_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/combo_box_node.ts
new file mode 100644
index 0000000..55463d0
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/combo_box_node.ts
@@ -0,0 +1,86 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {AutomationPredicate} from '/common/automation_predicate.js';
+import {EventGenerator} from '/common/event_generator.js';
+import {KeyCode} from '/common/key_code.js';
+import {RepeatedEventHandler} from '/common/repeated_event_handler.js';
+
+import {Navigator} from '../navigator.js';
+import {ActionResponse} from '../switch_access_constants.js';
+
+import {BasicNode} from './basic_node.js';
+import {SARootNode} from './switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+const EventType = chrome.automation.EventType;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+
+/**
+ * This class handles interactions with combo boxes.
+ * TODO(anastasi): Add a test for this class.
+ */
+class ComboBoxNode extends BasicNode {
+  private expandedChangedHandler_?: RepeatedEventHandler|null;
+
+  constructor(baseNode: AutomationNode, parent: SARootNode|null) {
+    super(baseNode, parent);
+  }
+
+  override get actions(): MenuAction[] {
+    const actions = super.actions;
+    if (!actions.includes(MenuAction.INCREMENT) &&
+        !actions.includes(MenuAction.DECREMENT)) {
+      actions.push(MenuAction.INCREMENT, MenuAction.DECREMENT);
+    }
+    return actions;
+  }
+
+  override onFocus(): void {
+    super.onFocus();
+
+    this.expandedChangedHandler_ = new RepeatedEventHandler(
+        this.automationNode, EventType.EXPANDED, () => this.onExpandedChanged(),
+        {exactMatch: true});
+  }
+
+  override onUnfocus(): void {
+    super.onUnfocus();
+
+    if (this.expandedChangedHandler_) {
+      this.expandedChangedHandler_.stop();
+      this.expandedChangedHandler_ = null;
+    }
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    // The box of options that typically pops up with combo boxes is not
+    // currently given a location in the automation tree, so we work around that
+    // by selecting a value without opening the pop-up, using the up and down
+    // arrows.
+    switch (action) {
+      case MenuAction.DECREMENT:
+        EventGenerator.sendKeyPress(KeyCode.UP);
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.INCREMENT:
+        EventGenerator.sendKeyPress(KeyCode.DOWN);
+        return ActionResponse.REMAIN_OPEN;
+    }
+    return super.performAction(action);
+  }
+
+  onExpandedChanged(): void {
+    // TODO: figure out why a short timeout is needed here.
+    setTimeout(() => {
+      if (this.isGroup()) {
+        Navigator.byItem.enterGroup();
+      }
+    }, 250);
+  }
+}
+
+BasicNode.creators.push({
+  predicate: AutomationPredicate.comboBox,
+  creator: (node, parent) => new ComboBoxNode(node, parent),
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/desktop_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/desktop_node.ts
new file mode 100644
index 0000000..1e1f0dc
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/desktop_node.ts
@@ -0,0 +1,93 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {Navigator} from '../navigator.js';
+import {SwitchAccess} from '../switch_access.js';
+import {ErrorType} from '../switch_access_constants.js';
+
+import {BasicNode, BasicRootNode} from './basic_node.js';
+import {SAChildNode, SARootNode} from './switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+
+/**
+ * This class handles interactions with the desktop automation node.
+ */
+export class DesktopNode extends BasicRootNode {
+  // ================= General methods =================
+
+  override equals(other: SARootNode): boolean {
+    // The underlying automation tree only has one desktop node, so all
+    // DesktopNode instances are equal.
+    return other instanceof DesktopNode;
+  }
+
+  override isValidGroup(): boolean {
+    return true;
+  }
+
+  override refresh(): void {
+    // Find the currently focused child.
+    let focusedChild: SAChildNode|null = null;
+    for (const child of this.children) {
+      if (child.isFocused()) {
+        focusedChild = child;
+        break;
+      }
+    }
+
+    // Update this DesktopNode's children.
+    const childConstructor = (node: AutomationNode): BasicNode =>
+        BasicNode.create(node, this);
+    DesktopNode.findAndSetChildren(this, childConstructor);
+
+    // Set the new instance of that child to be the focused node.
+    for (const child of this.children) {
+      if (child.isEquivalentTo(focusedChild)) {
+        Navigator.byItem.forceFocusedNode(child);
+        return;
+      }
+    }
+
+    // If the previously focused node no longer exists, focus the first node in
+    // the group.
+    Navigator.byItem.forceFocusedNode(this.children[0]);
+  }
+
+  // ================= Static methods =================
+
+  static build(desktop: AutomationNode): DesktopNode {
+    const root = new DesktopNode(desktop);
+    const childConstructor = (autoNode: AutomationNode): BasicNode =>
+        BasicNode.create(autoNode, root);
+
+    DesktopNode.findAndSetChildren(root, childConstructor);
+    return root;
+  }
+
+  static override findAndSetChildren(
+      root: DesktopNode,
+      childConstructor: (node: AutomationNode) => SAChildNode): void {
+    const interestingChildren = BasicRootNode.getInterestingChildren(root);
+
+    if (interestingChildren.length < 1) {
+      // If the desktop node does not behave as expected, we have no basis for
+      // recovering. Wait for the next user input.
+      throw SwitchAccess.error(
+          ErrorType.MALFORMED_DESKTOP,
+          'Desktop node must have at least 1 interesting child.',
+          false /* shouldRecover */);
+    }
+
+    // TODO(crbug.com/40706137): Add hittest intervals to new children which are
+    // SwitchAccessPredicate.isWindow to check whether those children are
+    // occluded or visible. Remove any intervals on the previous window
+    // children before reassigning root.children.
+    root.children = interestingChildren.map(childConstructor);
+  }
+}
+
+TestImportManager.exportForTesting(DesktopNode);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/desktop_node_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/desktop_node_test.js
new file mode 100644
index 0000000..4d25203
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/desktop_node_test.js
@@ -0,0 +1,31 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+GEN_INCLUDE(['../switch_access_e2e_test_base.js']);
+
+/** Test fixture for the desktop node. */
+SwitchAccessDesktopNodeTest = class extends SwitchAccessE2ETest {};
+
+TEST_F('SwitchAccessDesktopNodeTest', 'Build', function() {
+  this.runWithLoadedDesktop(desktop => {
+    const desktopNode = DesktopNode.build(desktop);
+
+    const children = desktopNode.children;
+    for (let i = 0; i < children.length; i++) {
+      const child = children[i];
+      // The desktop tree should not include a back button.
+      assertFalse(child instanceof BackButtonNode);
+
+      // Check that the children form a loop.
+      const next = children[(i + 1) % children.length];
+      assertEquals(
+          next, child.next, 'next not properly initialized on child ' + i);
+      // We add children.length to ensure the value is greater than zero.
+      const previous = children[(i - 1 + children.length) % children.length];
+      assertEquals(
+          previous, child.previous,
+          'previous not properly initialized on child ' + i);
+    }
+  });
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/editable_text_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/editable_text_node.ts
new file mode 100644
index 0000000..d4db73d1
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/editable_text_node.ts
@@ -0,0 +1,148 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {EventGenerator} from '/common/event_generator.js';
+import {EventHandler} from '/common/event_handler.js';
+import {KeyCode} from '/common/key_code.js';
+
+import {Navigator} from '../navigator.js';
+import {SwitchAccess} from '../switch_access.js';
+import {ActionResponse} from '../switch_access_constants.js';
+import {SwitchAccessPredicate} from '../switch_access_predicate.js';
+import {TextNavigationManager} from '../text_navigation_manager.js';
+
+import {BasicNode} from './basic_node.js';
+import {SARootNode} from './switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+import EventType = chrome.automation.EventType;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+import StateType = chrome.automation.StateType;
+
+/**
+ * This class handles interactions with editable text fields.
+ */
+export class EditableTextNode extends BasicNode {
+  constructor(baseNode: AutomationNode, parent: SARootNode|null) {
+    super(baseNode, parent);
+  }
+
+  // ================= Getters and setters =================
+
+  override get actions(): MenuAction[] {
+    const actions = super.actions;
+    // The SELECT action is used to press buttons, etc. For text inputs, the
+    // equivalent action is KEYBOARD, which focuses the input and opens the
+    // keyboard.
+    const selectIndex = actions.indexOf(MenuAction.SELECT);
+    if (selectIndex >= 0) {
+      actions.splice(selectIndex, 1);
+    }
+
+    actions.unshift(MenuAction.KEYBOARD, MenuAction.DICTATION);
+
+    if (SwitchAccess.improvedTextInputEnabled()) {
+      actions.push(
+          MenuAction.MOVE_CURSOR, MenuAction.JUMP_TO_BEGINNING_OF_TEXT,
+          MenuAction.JUMP_TO_END_OF_TEXT,
+          MenuAction.MOVE_BACKWARD_ONE_CHAR_OF_TEXT,
+          MenuAction.MOVE_FORWARD_ONE_CHAR_OF_TEXT,
+          MenuAction.MOVE_BACKWARD_ONE_WORD_OF_TEXT,
+          MenuAction.MOVE_FORWARD_ONE_WORD_OF_TEXT,
+          MenuAction.MOVE_DOWN_ONE_LINE_OF_TEXT,
+          MenuAction.MOVE_UP_ONE_LINE_OF_TEXT);
+
+      actions.push(MenuAction.START_TEXT_SELECTION);
+      if (TextNavigationManager.currentlySelecting()) {
+        actions.push(MenuAction.END_TEXT_SELECTION);
+      }
+
+      if (TextNavigationManager.selectionExists) {
+        actions.push(MenuAction.CUT, MenuAction.COPY);
+      }
+      if (TextNavigationManager.clipboardHasData) {
+        actions.push(MenuAction.PASTE);
+      }
+    }
+    return actions;
+  }
+
+  // ================= General methods =================
+
+  override doDefaultAction(): void {
+    this.performAction(MenuAction.KEYBOARD);
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    switch (action) {
+      case MenuAction.KEYBOARD:
+        Navigator.byItem.enterKeyboard();
+        return ActionResponse.CLOSE_MENU;
+      case MenuAction.DICTATION:
+        // TODO(crbug.com/314203187): Not null asserted, check that this is
+        // correct.
+        if (this.automationNode.state![StateType.FOCUSED]) {
+          chrome.accessibilityPrivate.toggleDictation();
+        } else {
+          new EventHandler(
+              this.automationNode, EventType.FOCUS,
+              () => chrome.accessibilityPrivate.toggleDictation(),
+              {exactMatch: true, listenOnce: true})
+              .start();
+          this.automationNode.focus();
+        }
+        return ActionResponse.CLOSE_MENU;
+      case MenuAction.MOVE_CURSOR:
+        return ActionResponse.OPEN_TEXT_NAVIGATION_MENU;
+
+      case MenuAction.CUT:
+        EventGenerator.sendKeyPress(KeyCode.X, {ctrl: true});
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.COPY:
+        EventGenerator.sendKeyPress(KeyCode.C, {ctrl: true});
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.PASTE:
+        EventGenerator.sendKeyPress(KeyCode.V, {ctrl: true});
+        return ActionResponse.REMAIN_OPEN;
+
+      case MenuAction.START_TEXT_SELECTION:
+        TextNavigationManager.saveSelectStart();
+        return ActionResponse.OPEN_TEXT_NAVIGATION_MENU;
+      case MenuAction.END_TEXT_SELECTION:
+        TextNavigationManager.saveSelectEnd();
+        return ActionResponse.EXIT_SUBMENU;
+
+      case MenuAction.JUMP_TO_BEGINNING_OF_TEXT:
+        TextNavigationManager.jumpToBeginning();
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.JUMP_TO_END_OF_TEXT:
+        TextNavigationManager.jumpToEnd();
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.MOVE_BACKWARD_ONE_CHAR_OF_TEXT:
+        TextNavigationManager.moveBackwardOneChar();
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.MOVE_BACKWARD_ONE_WORD_OF_TEXT:
+        TextNavigationManager.moveBackwardOneWord();
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.MOVE_DOWN_ONE_LINE_OF_TEXT:
+        TextNavigationManager.moveDownOneLine();
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.MOVE_FORWARD_ONE_CHAR_OF_TEXT:
+        TextNavigationManager.moveForwardOneChar();
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.MOVE_UP_ONE_LINE_OF_TEXT:
+        TextNavigationManager.moveUpOneLine();
+        return ActionResponse.REMAIN_OPEN;
+      case MenuAction.MOVE_FORWARD_ONE_WORD_OF_TEXT:
+        TextNavigationManager.moveForwardOneWord();
+        return ActionResponse.REMAIN_OPEN;
+    }
+    return super.performAction(action);
+  }
+}
+
+BasicNode.creators.push({
+  predicate: SwitchAccessPredicate.isTextInput,
+  creator: (node, parentNode) => new EditableTextNode(node, parentNode),
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/group_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/group_node.ts
new file mode 100644
index 0000000..ca9e9d9
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/group_node.ts
@@ -0,0 +1,150 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {RectUtil} from '/common/rect_util.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {Navigator} from '../navigator.js';
+import {ActionResponse} from '../switch_access_constants.js';
+
+import {BackButtonNode} from './back_button_node.js';
+import {SAChildNode, SARootNode} from './switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+type Rect = chrome.automation.Rect;
+import RoleType = chrome.automation.RoleType;
+
+/**
+ * This class handles the grouping of nodes that are not grouped in the
+ *     automation tree. They are defined by their parent and child nodes.
+ * Ex: Nodes in the virtual keyboard have no intermediate grouping, but should
+ *     be grouped by row.
+ */
+export class GroupNode extends SAChildNode {
+  /**
+   * @param children The nodes that this group contains.
+   *     Should not include the back button.
+   * @param containingNode The automation node most closely containing the
+   *     children.
+   */
+  private constructor(
+      private children_: SAChildNode[],
+      private containingNode_: AutomationNode) {
+    super();
+  }
+
+  // ================= Getters and setters =================
+
+  override get actions(): MenuAction[] {
+    return [MenuAction.DRILL_DOWN];
+  }
+
+  override get automationNode(): AutomationNode {
+    return this.containingNode_;
+  }
+
+  override get location(): Rect {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    const childLocations =
+        this.children_.filter(c => c.isValidAndVisible()).map(c => c.location!);
+    return RectUtil.unionAll(childLocations);
+  }
+
+  override get role(): RoleType {
+    return RoleType.GROUP;
+  }
+
+  // ================= General methods =================
+
+  override asRootNode(): SARootNode {
+    const root = new SARootNode(this.containingNode_);
+
+    // Make a copy of the children array.
+    const children = [...this.children_];
+
+    children.push(new BackButtonNode(root));
+    root.children = children;
+
+    return root;
+  }
+
+  override equals(other: SAChildNode): boolean {
+    if (!(other instanceof GroupNode)) {
+      return false;
+    }
+
+    if (other.children_.length !== this.children_.length) {
+      return false;
+    }
+    for (let i = 0; i < this.children_.length; i++) {
+      if (!other.children_[i].equals(this.children_[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  override isEquivalentTo(node: AutomationNode|SAChildNode|
+                          SARootNode): boolean {
+    if (node instanceof GroupNode) {
+      return this.equals(node);
+    }
+
+    for (const child of this.children_) {
+      if (child.isEquivalentTo(node)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  override isGroup(): boolean {
+    return true;
+  }
+
+  override isValidAndVisible(): boolean {
+    for (const child of this.children_) {
+      if (child.isValidAndVisible()) {
+        return super.isValidAndVisible();
+      }
+    }
+    return false;
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    if (action === MenuAction.DRILL_DOWN) {
+      Navigator.byItem.enterGroup();
+      return ActionResponse.CLOSE_MENU;
+    }
+    return ActionResponse.NO_ACTION_TAKEN;
+  }
+
+  // ================= Static methods =================
+
+  /** Assumes nodes are visually in rows. */
+  static separateByRow(nodes: SAChildNode[], containingNode: AutomationNode):
+      GroupNode[] {
+    const result: GroupNode[] = [];
+
+    for (let i = 0; i < nodes.length;) {
+      const children: SAChildNode[] = [];
+      children.push(nodes[i]);
+      i++;
+
+      while (i < nodes.length &&
+             RectUtil.sameRow(children[0].location, nodes[i].location)) {
+        children.push(nodes[i]);
+        i++;
+      }
+
+      result.push(new GroupNode(children, containingNode));
+    }
+
+    return result;
+  }
+}
+
+TestImportManager.exportForTesting(GroupNode);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/group_node_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/group_node_test.js
new file mode 100644
index 0000000..202f864
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/group_node_test.js
@@ -0,0 +1,33 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+GEN_INCLUDE(['../switch_access_e2e_test_base.js']);
+
+/** Test fixture for the node wrapper type. */
+SwitchAccessGroupNodeTest = class extends SwitchAccessE2ETest {};
+
+TEST_F('SwitchAccessGroupNodeTest', 'NodesRemoved', function() {
+  const website = `<button></button>`;
+  this.runWithLoadedTree(website, rootWebArea => {
+    const button = rootWebArea.find({role: chrome.automation.RoleType.BUTTON});
+    assertNotEquals(undefined, button);
+
+    const root = new BasicRootNode(rootWebArea);
+    assertEquals(0, root.children_.length);
+
+    // Add a group child which has two buttons (same underlying automation
+    // node).
+    const buttonNode = new BasicNode(button, root);
+    const otherButtonNode = new BasicNode(button, root);
+    const groupNode = new GroupNode([buttonNode, otherButtonNode]);
+    root.children_ = [groupNode];
+
+    // Try asking for the location of the group.
+    assertTrue(Boolean(groupNode.location));
+
+    // Try again after clearing one of the button's underlying node.
+    buttonNode.baseNode_ = undefined;
+    assertTrue(Boolean(groupNode.location));
+  });
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/keyboard_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/keyboard_node.ts
new file mode 100644
index 0000000..4d213ce
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/keyboard_node.ts
@@ -0,0 +1,247 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {EventGenerator} from '/common/event_generator.js';
+import {EventHandler} from '/common/event_handler.js';
+import {RectUtil} from '/common/rect_util.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {AutoScanManager} from '../auto_scan_manager.js';
+import {Navigator} from '../navigator.js';
+import {SwitchAccess} from '../switch_access.js';
+import {ActionResponse, ErrorType} from '../switch_access_constants.js';
+import {SwitchAccessPredicate} from '../switch_access_predicate.js';
+
+import {BackButtonNode} from './back_button_node.js';
+import {BasicNode, BasicRootNode} from './basic_node.js';
+import {GroupNode} from './group_node.js';
+import {SAChildNode, SARootNode} from './switch_access_node.js';
+
+type AutomationEvent = chrome.automation.AutomationEvent;
+type AutomationNode = chrome.automation.AutomationNode;
+const EventType = chrome.automation.EventType;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+const RoleType = chrome.automation.RoleType;
+
+/**
+ * This class handles the behavior of keyboard nodes directly associated with a
+ * single AutomationNode.
+ */
+export class KeyboardNode extends BasicNode {
+  static resetting = false;
+
+  constructor(node: AutomationNode, parent: SARootNode) {
+    super(node, parent);
+  }
+
+  // ================= Getters and setters =================
+
+  override get actions(): MenuAction[] {
+    return [MenuAction.SELECT];
+  }
+
+  // ================= General methods =================
+
+  override asRootNode(): SARootNode|undefined {
+    return undefined;
+  }
+
+  override isGroup(): boolean {
+    return false;
+  }
+
+  override isValidAndVisible(): boolean {
+    if (super.isValidAndVisible()) {
+      return true;
+    }
+    if (!KeyboardNode.resetting &&
+        Navigator.byItem.currentGroupHasChild(this)) {
+      // TODO(crbug/1130773): move this code to another location, if possible
+      KeyboardNode.resetting = true;
+      KeyboardRootNode.ignoreNextExit = true;
+      Navigator.byItem.exitKeyboard().then(
+          () => Navigator.byItem.enterKeyboard());
+    }
+
+    return false;
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    if (action !== MenuAction.SELECT) {
+      return ActionResponse.NO_ACTION_TAKEN;
+    }
+
+    const keyLocation = this.location;
+    if (!keyLocation) {
+      return ActionResponse.NO_ACTION_TAKEN;
+    }
+
+    // doDefault() does nothing on Virtual Keyboard buttons, so we must
+    // simulate a mouse click.
+    const center = RectUtil.center(keyLocation);
+    EventGenerator.sendMouseClick(
+        center.x, center.y, {delayMs: VK_KEY_PRESS_DURATION_MS});
+
+    return ActionResponse.CLOSE_MENU;
+  }
+}
+
+/**
+ * This class handles the top-level Keyboard node, as well as the construction
+ * of the Keyboard tree.
+ */
+export class KeyboardRootNode extends BasicRootNode {
+  static ignoreNextExit = false;
+  private static isVisible_ = false;
+  private static explicitStateChange_ = false;
+  private static object_?: AutomationNode;
+
+
+  private constructor(groupNode: AutomationNode) {
+    super(groupNode);
+    KeyboardNode.resetting = false;
+  }
+
+  // ================= General methods =================
+
+  override isValidGroup(): boolean {
+    // To ensure we can find the keyboard root node to appropriately respond to
+    // visibility changes, never mark it as invalid.
+    return true;
+  }
+
+  override onExit(): void {
+    if (KeyboardRootNode.ignoreNextExit) {
+      KeyboardRootNode.ignoreNextExit = false;
+      return;
+    }
+
+    // If the keyboard is currently visible, ignore the corresponding
+    // state change.
+    if (KeyboardRootNode.isVisible_) {
+      KeyboardRootNode.explicitStateChange_ = true;
+      chrome.accessibilityPrivate.setVirtualKeyboardVisible(false);
+    }
+
+    AutoScanManager.setInKeyboard(false);
+  }
+
+  override refreshChildren(): void {
+    KeyboardRootNode.findAndSetChildren_(this);
+  }
+
+  // ================= Static methods =================
+
+  /** Creates the tree structure for the keyboard. */
+  static override buildTree(): KeyboardRootNode {
+    KeyboardRootNode.loadKeyboard_();
+    AutoScanManager.setInKeyboard(true);
+
+    const keyboard = KeyboardRootNode.getKeyboardObject();
+    if (!keyboard) {
+      throw SwitchAccess.error(
+          ErrorType.MISSING_KEYBOARD,
+          'Could not find keyboard in the automation tree',
+          true /* shouldRecover */);
+    }
+    const root = new KeyboardRootNode(keyboard);
+    KeyboardRootNode.findAndSetChildren_(root);
+    return root;
+  }
+
+  /** Start listening for keyboard open/closed. */
+  static startWatchingVisibility(): void {
+    const keyboardObject = KeyboardRootNode.getKeyboardObject();
+    if (!keyboardObject) {
+      SwitchAccess.findNodeMatching(
+          {role: RoleType.KEYBOARD}, KeyboardRootNode.startWatchingVisibility);
+      return;
+    }
+
+    KeyboardRootNode.isVisible_ = KeyboardRootNode.isKeyboardVisible_();
+
+    new EventHandler(
+        keyboardObject, EventType.LOAD_COMPLETE,
+        KeyboardRootNode.checkVisibilityChanged_)
+        .start();
+    new EventHandler(
+        keyboardObject, EventType.STATE_CHANGED,
+        KeyboardRootNode.checkVisibilityChanged_, {exactMatch: true})
+        .start();
+  }
+
+  // ================= Private static methods =================
+
+  private static isKeyboardVisible_(): boolean {
+    const keyboardObject = KeyboardRootNode.getKeyboardObject();
+    return Boolean(
+        keyboardObject && SwitchAccessPredicate.isVisible(keyboardObject) &&
+        keyboardObject.find({role: RoleType.ROOT_WEB_AREA}));
+  }
+
+  private static checkVisibilityChanged_(_event: AutomationEvent): void {
+    const currentlyVisible = KeyboardRootNode.isKeyboardVisible_();
+    if (currentlyVisible === KeyboardRootNode.isVisible_) {
+      return;
+    }
+
+    KeyboardRootNode.isVisible_ = currentlyVisible;
+
+    if (KeyboardRootNode.explicitStateChange_) {
+      // When the user has explicitly shown / hidden the keyboard, do not
+      // enter / exit the keyboard again to avoid looping / double-calls.
+      KeyboardRootNode.explicitStateChange_ = false;
+      return;
+    }
+
+    if (KeyboardRootNode.isVisible_) {
+      Navigator.byItem.enterKeyboard();
+    } else {
+      Navigator.byItem.exitKeyboard();
+    }
+  }
+
+  /** Helper function to connect tree elements, given the root node. */
+  private static findAndSetChildren_(root: KeyboardRootNode): void {
+    const childConstructor = (node: AutomationNode): KeyboardNode =>
+        new KeyboardNode(node, root);
+    const interestingChildren =
+        root.automationNode.findAll({role: RoleType.BUTTON});
+    const children: SAChildNode[] = GroupNode.separateByRow(
+        interestingChildren.map(childConstructor), root.automationNode);
+
+    children.push(new BackButtonNode(root));
+    root.children = children;
+  }
+
+  private static getKeyboardObject(): AutomationNode {
+    if (!this.object_ || !this.object_.role) {
+      this.object_ =
+          Navigator.byItem.desktopNode.find({role: RoleType.KEYBOARD});
+    }
+    return this.object_;
+  }
+
+  /** Loads the keyboard. */
+  private static loadKeyboard_(): void {
+    if (KeyboardRootNode.isVisible_) {
+      return;
+    }
+
+    chrome.accessibilityPrivate.setVirtualKeyboardVisible(true);
+  }
+}
+
+BasicRootNode.builders.push({
+  predicate: rootNode => rootNode.role === RoleType.KEYBOARD,
+  builder: KeyboardRootNode.buildTree,
+});
+
+/**
+ * The delay between keydown and keyup events on the virtual keyboard,
+ * allowing the key press animation to display.
+ */
+const VK_KEY_PRESS_DURATION_MS = 100;
+
+TestImportManager.exportForTesting(KeyboardNode, KeyboardRootNode);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/modal_dialog_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/modal_dialog_node.ts
new file mode 100644
index 0000000..58fd89c0
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/modal_dialog_node.ts
@@ -0,0 +1,30 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {EventGenerator} from '/common/event_generator.js';
+import {KeyCode} from '/common/key_code.js';
+
+import {BasicNode, BasicRootNode} from './basic_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+
+/** This class represents the group rooted at a modal dialog. */
+export class ModalDialogRootNode extends BasicRootNode {
+  override onExit(): void {
+    // To close a modal dialog, we need to send an escape key event.
+    EventGenerator.sendKeyPress(KeyCode.ESCAPE);
+  }
+
+  /**
+   * Creates the tree structure for a modal dialog.
+   */
+  static override buildTree(dialogNode: AutomationNode): ModalDialogRootNode {
+    const root = new ModalDialogRootNode(dialogNode);
+    const childConstructor = (node: AutomationNode): BasicNode =>
+        BasicNode.create(node, root);
+
+    BasicRootNode.findAndSetChildren(root, childConstructor);
+    return root;
+  }
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/slider_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/slider_node.ts
new file mode 100644
index 0000000..223109af
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/slider_node.ts
@@ -0,0 +1,51 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {EventGenerator} from '/common/event_generator.js';
+import {KeyCode} from '/common/key_code.js';
+
+import {ActionResponse} from '../switch_access_constants.js';
+
+import {BasicNode} from './basic_node.js';
+import {SARootNode} from './switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+import RoleType = chrome.automation.RoleType;
+
+/** This class handles interactions with sliders. */
+export class SliderNode extends BasicNode {
+  private isCustomSlider_ = true;
+
+  constructor(baseNode: AutomationNode, parent: SARootNode|null) {
+    super(baseNode, parent);
+  }
+
+  override onFocus(): void {
+    super.onFocus();
+    this.automationNode.focus();
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    // Currently, custom sliders have no way to support increment/decrement via
+    // the automation API. We handle this case by simulating left/right arrow
+    // presses.
+    if (this.isCustomSlider_) {
+      if (action === MenuAction.INCREMENT) {
+        EventGenerator.sendKeyPress(KeyCode.RIGHT);
+        return ActionResponse.REMAIN_OPEN;
+      } else if (action === MenuAction.DECREMENT) {
+        EventGenerator.sendKeyPress(KeyCode.LEFT);
+        return ActionResponse.REMAIN_OPEN;
+      }
+    }
+
+    return super.performAction(action);
+  }
+}
+
+BasicNode.creators.push({
+  predicate: baseNode => baseNode.role === RoleType.SLIDER,
+  creator: (node, parent) => new SliderNode(node, parent),
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/switch_access_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/switch_access_node.ts
new file mode 100644
index 0000000..da86dd7
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/switch_access_node.ts
@@ -0,0 +1,387 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {RectUtil} from '/common/rect_util.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {FocusRingManager} from '../focus_ring_manager.js';
+import {SwitchAccess} from '../switch_access.js';
+import {ActionResponse, ErrorType} from '../switch_access_constants.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+type ScreenRect = chrome.accessibilityPrivate.ScreenRect;
+type RoleType = chrome.automation.RoleType;
+
+/**
+ * This interface represents some object or group of objects on screen
+ *     that Switch Access may be interested in interacting with.
+ *
+ * There is no guarantee of uniqueness; two distinct SAChildNodes may refer
+ *     to the same object. However, it is expected that any pair of
+ *     SAChildNodes referring to the same interesting object are equal
+ *     (calling .equals() returns true).
+ */
+export abstract class SAChildNode {
+  private isFocused_ = false;
+  private next_: SAChildNode|null = null;
+  private previous_: SAChildNode|null = null;
+  private valid_ = true;
+
+  // Abstract methods.
+
+  /** Returns a list of all the actions available for this node. */
+  abstract get actions(): MenuAction[];
+  /** If this node is a group, returns the analogous SARootNode. */
+  abstract asRootNode(): SARootNode|undefined;
+  /** The automation node that most closely contains this node. */
+  abstract get automationNode(): AutomationNode;
+  abstract equals(other: SAChildNode|null|undefined): boolean;
+  abstract isEquivalentTo(node: AutomationNode|SAChildNode|SARootNode|
+                          null): boolean;
+  /** Returns whether this node should be displayed as a group. */
+  abstract isGroup(): boolean;
+  abstract get location(): ScreenRect|undefined;
+  /** Performs the specified action on the node, if it is available. */
+  abstract performAction(action: MenuAction): ActionResponse;
+  abstract get role(): RoleType|undefined;
+
+
+  // ================= Getters and setters =================
+
+  get group(): SARootNode|null {
+    return null;
+  }
+
+  set next(newVal: SAChildNode) {
+    this.next_ = newVal;
+  }
+
+  /** Returns the next node in pre-order traversal. */
+  get next(): SAChildNode {
+    let next: SAChildNode|null = this;
+    while (true) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      next = next!.next_;
+      if (!next) {
+        this.onInvalidNavigation_(
+            ErrorType.NEXT_UNDEFINED,
+            'Next node must be set on all SAChildNodes before navigating');
+      }
+      if (this === next) {
+        this.onInvalidNavigation_(ErrorType.NEXT_INVALID, 'No valid next node');
+      }
+      if (next!.isValidAndVisible()) {
+        return next!;
+      }
+    }
+  }
+
+  set previous(newVal: SAChildNode) {
+    this.previous_ = newVal;
+  }
+
+  /** Returns the previous node in pre-order traversal. */
+  get previous(): SAChildNode {
+    let previous: SAChildNode|null = this;
+    while (true) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      previous = previous!.previous_;
+      if (!previous) {
+        this.onInvalidNavigation_(
+            ErrorType.PREVIOUS_UNDEFINED,
+            'Previous node must be set on all SAChildNodes before navigating');
+      }
+      if (this === previous) {
+        this.onInvalidNavigation_(
+            ErrorType.PREVIOUS_INVALID, 'No valid previous node');
+      }
+      if (previous!.isValidAndVisible()) {
+        return previous!;
+      }
+    }
+  }
+
+  // ================= General methods =================
+
+  /** Performs the node's default action. */
+  doDefaultAction(): void {
+    if (!this.isFocused_) {
+      return;
+    }
+    if (this.isGroup()) {
+      this.performAction(MenuAction.DRILL_DOWN);
+    } else {
+      this.performAction(MenuAction.SELECT);
+    }
+  }
+
+  /** Given a menu action, returns whether it can be performed on this node. */
+  hasAction(action: MenuAction): boolean {
+    return this.actions.includes(action);
+  }
+
+  /** Returns whether the node is currently focused by Switch Access. */
+  isFocused(): boolean {
+    return this.isFocused_;
+  }
+
+  /**
+   * Returns whether this node is still both valid and visible onscreen (e.g.
+   *    has a location, and, if representing an AutomationNode, not hidden,
+   *    not offscreen, not invisible).
+   */
+  isValidAndVisible(): boolean {
+    return this.valid_ && Boolean(this.location);
+  }
+
+  /** Called when this node becomes the primary highlighted node. */
+  onFocus(): void {
+    this.isFocused_ = true;
+    FocusRingManager.setFocusedNode(this);
+  }
+
+  /** Called when this node stops being the primary highlighted node. */
+  onUnfocus(): void {
+    this.isFocused_ = false;
+  }
+
+  // ================= Debug methods =================
+
+  /** String-ifies the node (for debugging purposes). */
+  debugString(
+      wholeTree: boolean, prefix: string = '',
+      currentNode: SAChildNode|null = null): string {
+    if (this.isGroup() && wholeTree) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      return this.asRootNode()!.debugString(
+          wholeTree, prefix + '  ', currentNode);
+    }
+
+    let str = this.constructor.name + ' role(' + this.role + ') ';
+
+    if (this.automationNode.name) {
+      str += 'name(' + this.automationNode.name + ') ';
+    }
+
+    const loc = this.location;
+    if (loc) {
+      str += 'loc(' + RectUtil.toString(loc) + ') ';
+    }
+
+    if (this.isGroup()) {
+      str += '[isGroup]';
+    }
+
+    return str;
+  }
+
+  // ================= Private methods =================
+
+  private onInvalidNavigation_(error: ErrorType, message: string): void {
+    this.valid_ = false;
+    throw SwitchAccess.error(error, message, true /* shouldRecover */);
+  }
+
+  /** @return Whether to ignore when computing the SARootNode's location. */
+  ignoreWhenComputingUnionOfBoundingBoxes(): boolean {
+    return false;
+  }
+}
+
+/**
+ * This class represents the root node of a Switch Access traversal group.
+ */
+export class SARootNode {
+  private children_: SAChildNode[] = [];
+  private automationNode_: AutomationNode;
+
+  /**
+   * @param autoNode The automation node that most closely contains all of
+   * this node's children.
+   */
+  constructor(autoNode: AutomationNode) {
+    this.automationNode_ = autoNode;
+  }
+
+  // ================= Getters and setters =================
+
+  /**
+   * @return The automation node that most closely contains all of this node's
+   * children.
+   */
+  get automationNode(): AutomationNode {
+    return this.automationNode_;
+  }
+
+  set children(newVal: SAChildNode[]) {
+    this.children_ = newVal;
+    this.connectChildren_();
+  }
+
+  get children(): SAChildNode[] {
+    return this.children_;
+  }
+
+  get firstChild(): SAChildNode {
+    if (this.children_.length > 0) {
+      return this.children_[0];
+    } else {
+      throw SwitchAccess.error(
+          ErrorType.NO_CHILDREN, 'Root nodes must contain children.',
+          true /* shouldRecover */);
+    }
+  }
+
+  get lastChild(): SAChildNode {
+    if (this.children_.length > 0) {
+      return this.children_[this.children_.length - 1];
+    } else {
+      throw SwitchAccess.error(
+          ErrorType.NO_CHILDREN, 'Root nodes must contain children.',
+          true /* shouldRecover */);
+    }
+  }
+
+  get location(): ScreenRect {
+    const children = this.children_.filter(
+        c => !c.ignoreWhenComputingUnionOfBoundingBoxes());
+    const childLocations = children.map(c => c.location).filter(l => l);
+    return RectUtil.unionAll(childLocations as ScreenRect[]);
+  }
+
+  // ================= General methods =================
+
+  equals(other: SARootNode): boolean {
+    if (!other) {
+      return false;
+    }
+    if (this.children_.length !== other.children_.length) {
+      return false;
+    }
+
+    let result = true;
+    for (let i = 0; i < this.children_.length; i++) {
+      if (!this.children_[i]) {
+        console.error(
+            SwitchAccess.error(ErrorType.NULL_CHILD, 'Child cannot be null.'));
+        return false;
+      }
+      result = result && this.children_[i].equals(other.children_[i]);
+    }
+
+    return result;
+  }
+
+  /**
+   * Looks for and returns the specified node within this node's children.
+   * If no equivalent node is found, returns null.
+   */
+  findChild(node: AutomationNode|SAChildNode|SARootNode): SAChildNode|null {
+    for (const child of this.children_) {
+      if (child.isEquivalentTo(node)) {
+        return child;
+      }
+    }
+    return null;
+  }
+
+  isEquivalentTo(node: AutomationNode|SARootNode|SAChildNode|null): boolean {
+    if (node instanceof SARootNode) {
+      return this.equals(node);
+    }
+    if (node instanceof SAChildNode) {
+      return node.isEquivalentTo(this);
+    }
+    return false;
+  }
+
+  isValidGroup(): boolean {
+    // Must have one interesting child whose location is important.
+    return this.children_
+               .filter(
+                   (child: SAChildNode) =>
+                       !(child.ignoreWhenComputingUnionOfBoundingBoxes()) &&
+                       child.isValidAndVisible())
+               .length >= 1;
+  }
+
+  firstValidChild(): SAChildNode|null {
+    const children = this.children_.filter(child => child.isValidAndVisible());
+    return children.length > 0 ? children[0] : null;
+  }
+
+  /** Called when a group is set as the current group. */
+  onFocus(): void {}
+
+  /** Called when a group is no longer the current group. */
+  onUnfocus(): void {}
+
+  /** Called when a group is explicitly exited. */
+  onExit(): void {}
+
+  /** Called when a group should recalculate its children. */
+  refreshChildren(): void {
+    this.children =
+        this.children.filter((child: SAChildNode) => child.isValidAndVisible());
+  }
+
+  /** Called when the group's children may have changed. */
+  refresh(): void {}
+
+  // ================= Debug methods =================
+
+  /**
+   * String-ifies the node (for debugging purposes).
+   * @param wholeTree Whether to recursively descend the tree
+   * @param currentNode the currently focused node, to mark.
+   */
+  debugString(
+      wholeTree: boolean = false, prefix: string = '',
+      currentNode: SAChildNode|null = null): string {
+    let str =
+        'Root: ' + this.constructor.name + ' ' + this.automationNode.role + ' ';
+    if (this.automationNode.name) {
+      str += 'name(' + this.automationNode.name + ') ';
+    }
+
+    const loc = this.location;
+    if (loc) {
+      str += 'loc(' + RectUtil.toString(loc) + ') ';
+    }
+
+    for (const child of this.children) {
+      str += '\n' + prefix + ((child.equals(currentNode)) ? ' * ' : ' - ');
+      str += child.debugString(wholeTree, prefix, currentNode);
+    }
+
+    return str;
+  }
+
+  // ================= Private methods =================
+
+  /** Helper function to connect children. */
+  private connectChildren_(): void {
+    if (this.children_.length < 1) {
+      console.error(SwitchAccess.error(
+          ErrorType.NO_CHILDREN,
+          'Root node must have at least 1 interesting child.'));
+      return;
+    }
+
+    let previous = this.children_[this.children_.length - 1];
+
+    for (let i = 0; i < this.children_.length; i++) {
+      const current = this.children_[i];
+      previous.next = current;
+      current.previous = previous;
+
+      previous = current;
+    }
+  }
+}
+
+export type SANode = SAChildNode|SARootNode;
+
+TestImportManager.exportForTesting(SARootNode);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/tab_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/tab_node.ts
new file mode 100644
index 0000000..5b779e6
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/tab_node.ts
@@ -0,0 +1,123 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {RectUtil} from '/common/rect_util.js';
+
+import {Navigator} from '../navigator.js';
+import {ActionResponse} from '../switch_access_constants.js';
+
+import {BackButtonNode} from './back_button_node.js';
+import {BasicNode, BasicRootNode} from './basic_node.js';
+import {SAChildNode, SARootNode} from './switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+type Rect = chrome.automation.Rect;
+import RoleType = chrome.automation.RoleType;
+
+/**
+ * This class handles the behavior of tab nodes at the top level (i.e. as
+ * groups).
+ */
+export class TabNode extends BasicNode {
+  /**
+   * @param node The node in the automation tree
+   * @param tabAsRoot A pre-calculated object for exploring the parts of tab
+   * (i.e. choosing whether to open the tab or close it).
+   */
+  constructor(
+      node: AutomationNode, parent: SARootNode|null,
+      private tabAsRoot_: SARootNode) {
+    super(node, parent);
+  }
+
+  // ================= Getters and setters =================
+
+  override get actions(): MenuAction[] {
+    return [MenuAction.DRILL_DOWN];
+  }
+
+  // ================= General methods =================
+
+  override asRootNode(): SARootNode {
+    return this.tabAsRoot_;
+  }
+
+  override isGroup(): boolean {
+    return true;
+  }
+
+  override performAction(action: MenuAction): ActionResponse {
+    if (action !== MenuAction.DRILL_DOWN) {
+      return ActionResponse.NO_ACTION_TAKEN;
+    }
+    Navigator.byItem.enterGroup();
+    return ActionResponse.CLOSE_MENU;
+  }
+
+  // ================= Static methods =================
+
+  static override create(tabNode: AutomationNode, parent: SARootNode|null):
+      BasicNode {
+    const tabAsRoot = new BasicRootNode(tabNode);
+
+    let closeButton;
+    for (const child of tabNode.children) {
+      if (child.role === RoleType.BUTTON) {
+        closeButton = new BasicNode(child, tabAsRoot);
+        break;
+      }
+    }
+    if (!closeButton) {
+      // Pinned tabs have no close button, and so can be treated as just
+      // actionable.
+      return new ActionableTabNode(tabNode, parent, null);
+    }
+
+    const tabToSelect = new ActionableTabNode(tabNode, tabAsRoot, closeButton);
+    const backButton = new BackButtonNode(tabAsRoot);
+    tabAsRoot.children = [tabToSelect, closeButton, backButton];
+
+    return new TabNode(tabNode, parent, tabAsRoot);
+  }
+}
+
+/** This class handles the behavior of tabs as actionable elements */
+class ActionableTabNode extends BasicNode {
+  constructor(
+      node: AutomationNode, parent: SARootNode|null,
+      private closeButton_: SAChildNode|null) {
+    super(node, parent);
+  }
+
+  // ================= Getters and setters =================
+
+  override get actions(): MenuAction[] {
+    return [MenuAction.SELECT];
+  }
+
+  override get location(): Rect|undefined {
+    if (!this.closeButton_) {
+      return super.location;
+    }
+    return RectUtil.difference(super.location, this.closeButton_.location);
+  }
+
+  // ================= General methods =================
+
+  override asRootNode(): SARootNode|undefined {
+    return undefined;
+  }
+
+  override isGroup(): boolean {
+    return false;
+  }
+}
+
+// TODO(crbug.com/314203187): Not null asserted, check that this is correct.
+BasicNode.creators.push({
+  predicate: baseNode => baseNode.role === RoleType.TAB &&
+      baseNode.root!.role === RoleType.DESKTOP,
+  creator: TabNode.create,
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/tab_node_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/tab_node_test.js
new file mode 100644
index 0000000..132dbd7
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/tab_node_test.js
@@ -0,0 +1,97 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+GEN_INCLUDE(['../switch_access_e2e_test_base.js']);
+
+/** Test fixture for the tab node type. */
+SwitchAccessTabNodeTest = class extends SwitchAccessE2ETest {
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    globalThis.MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+  }
+};
+
+AX_TEST_F('SwitchAccessTabNodeTest', 'FindCloseButton', async function() {
+  await this.runWithLoadedTree('');
+  const tab = this.desktop_.find({role: chrome.automation.RoleType.TAB});
+
+  // To find the close button, Switch Access relies on it being the only
+  // button within a tab.
+  let buttonCount = 0;
+  for (const child of tab.children) {
+    if (child.role === chrome.automation.RoleType.BUTTON) {
+      buttonCount++;
+    }
+  }
+  assertEquals(buttonCount, 1);
+});
+
+AX_TEST_F('SwitchAccessTabNodeTest', 'Construction', async function() {
+  await this.runWithLoadedTree('');
+  const tabAutomationNode =
+      this.desktop_.find({role: chrome.automation.RoleType.TAB});
+  assertNotNullNorUndefined(tabAutomationNode);
+  Navigator.byItem.moveTo_(tabAutomationNode);
+
+  const tab = Navigator.byItem.node_;
+  assertEquals(
+      chrome.automation.RoleType.TAB, tab.role, 'Tab node is not a tab');
+  assertTrue(tab.isGroup(), 'Tab node should be a group');
+  assertEquals(
+      1, tab.actions.length,
+      'Tab as a group should have 1 action (drill down)');
+  assertEquals(
+      MenuAction.DRILL_DOWN, tab.actions[0],
+      'Tab as a group should have the action DRILL_DOWN');
+
+  Navigator.byItem.node_.doDefaultAction();
+
+  const tabAsRoot = Navigator.byItem.group_;
+  assertTrue(
+      RectUtil.equal(tab.location, tabAsRoot.location),
+      'Tab location should not change when treated as root');
+  assertEquals(
+      3, tabAsRoot.children.length, 'Tab as root should have 3 children');
+
+  const tabToSelect = Navigator.byItem.node_;
+  assertEquals(
+      chrome.automation.RoleType.TAB, tabToSelect.role,
+      'Tab node to select is not a tab');
+  assertFalse(
+      tabToSelect.isGroup(), 'Tab node to select should not be a group');
+  assertTrue(
+      tabToSelect.hasAction(MenuAction.SELECT),
+      'Tab as a group should have a SELECT action');
+  assertFalse(
+      RectUtil.equal(tabAsRoot.location, tabToSelect.location),
+      'Tab node to select should not have the same location as tab as root');
+  assertEquals(
+      undefined, tabToSelect.asRootNode(),
+      'Tab node to select should not be a root node');
+
+  Navigator.byItem.moveForward();
+
+  const close = Navigator.byItem.node_;
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, close.role,
+      'Close button is not a button');
+  assertFalse(close.isGroup(), 'Close button should not be a group');
+  assertTrue(
+      close.hasAction(MenuAction.SELECT),
+      'Close button should have a SELECT action');
+  assertFalse(
+      RectUtil.equal(tabAsRoot.location, close.location),
+      'Close button should not have the same location as tab as root');
+  const overlap = RectUtil.intersection(tabToSelect.location, close.location);
+  assertTrue(
+      RectUtil.equal(RectUtil.ZERO_RECT, overlap),
+      'Close button and tab node to select should not overlap');
+
+  BackButtonNode
+      .locationForTesting = {top: 10, left: 10, width: 10, height: 10};
+  Navigator.byItem.moveForward();
+  assertTrue(
+      Navigator.byItem.node_ instanceof BackButtonNode,
+      'Third node should be a BackButtonNode');
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/window_node.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/window_node.ts
new file mode 100644
index 0000000..f2ed65a
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/nodes/window_node.ts
@@ -0,0 +1,41 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {SwitchAccessPredicate} from '../switch_access_predicate.js';
+
+import {BasicNode, BasicRootNode} from './basic_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+const RoleType = chrome.automation.RoleType;
+
+/** This class represents a window. */
+export class WindowRootNode extends BasicRootNode {
+  override onFocus(): void {
+    super.onFocus();
+
+    let focusNode = this.automationNode;
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    while (focusNode.className !== 'BrowserFrame' &&
+           focusNode.parent!.role === RoleType.WINDOW) {
+      focusNode = focusNode.parent!;
+    }
+    focusNode.focus();
+  }
+
+  /** Creates the tree structure for a window node. */
+  static override buildTree(windowNode: AutomationNode): WindowRootNode {
+    const root = new WindowRootNode(windowNode);
+    const childConstructor = (node: AutomationNode): BasicNode =>
+        BasicNode.create(node, root);
+
+    BasicRootNode.findAndSetChildren(root, childConstructor);
+    return root;
+  }
+}
+
+BasicRootNode.builders.push({
+  predicate: (rootNode: AutomationNode) =>
+      SwitchAccessPredicate.isWindow(rootNode),
+  builder: WindowRootNode.buildTree,
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/point_scan_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/point_scan_manager.ts
new file mode 100644
index 0000000..0151f29
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/point_scan_manager.ts
@@ -0,0 +1,73 @@
+// 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 {constants} from '/common/constants.js';
+import {EventGenerator, MouseClickParams} from '/common/event_generator.js';
+
+import {ActionManager} from './action_manager.js';
+import {FocusRingManager} from './focus_ring_manager.js';
+import {PointNavigatorInterface} from './navigator_interfaces.js';
+import {SwitchAccess} from './switch_access.js';
+import {MenuType, Mode} from './switch_access_constants.js';
+
+import MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+import Point = constants.Point;
+import PointScanState = chrome.accessibilityPrivate.PointScanState;
+
+export class PointScanManager implements PointNavigatorInterface {
+  private point_: Point = {x: 0, y: 0};
+  private pointListener_: (point: Point) => void;
+
+  constructor() {
+    this.pointListener_ = point => this.handleOnPointScanSet_(point);
+  }
+
+  // ====== PointNavigatorInterface implementation =====
+
+  get currentPoint(): Point {
+    return this.point_;
+  }
+
+  start(): void {
+    FocusRingManager.clearAll();
+    SwitchAccess.mode = Mode.POINT_SCAN;
+    chrome.accessibilityPrivate.onPointScanSet.addListener(this.pointListener_);
+    chrome.accessibilityPrivate.setPointScanState(PointScanState.START);
+  }
+
+  stop(): void {
+    chrome.accessibilityPrivate.setPointScanState(PointScanState.STOP);
+  }
+
+  performMouseAction(action: MenuAction): void {
+    if (SwitchAccess.mode !== Mode.POINT_SCAN) {
+      return;
+    }
+    if (action !== MenuAction.LEFT_CLICK && action !== MenuAction.RIGHT_CLICK) {
+      return;
+    }
+
+    const params: MouseClickParams = {};
+    if (action === MenuAction.RIGHT_CLICK) {
+      params.mouseButton =
+          chrome.accessibilityPrivate.SyntheticMouseEventButton.RIGHT;
+    }
+
+    EventGenerator.sendMouseClick(this.point_.x, this.point_.y, params);
+    this.start();
+  }
+
+  // ============= Private Methods =============
+
+  /**
+   * Shows the point scan menu and sets the point scan position
+   * coordinates.
+   */
+  private handleOnPointScanSet_(point: Point): void {
+    this.point_ = point;
+    ActionManager.openMenu(MenuType.POINT_SCAN_MENU);
+    chrome.accessibilityPrivate.onPointScanSet.removeListener(
+        this.pointListener_);
+  }
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/point_scan_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/point_scan_manager_test.js
new file mode 100644
index 0000000..e44ef22
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/point_scan_manager_test.js
@@ -0,0 +1,101 @@
+// 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.
+
+GEN_INCLUDE(['switch_access_e2e_test_base.js']);
+
+/** Test fixture for the point scan manager. */
+SwitchAccessPointScanManagerTest = class extends SwitchAccessE2ETest {
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    globalThis.MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+  }
+};
+
+AX_TEST_F(
+    'SwitchAccessPointScanManagerTest', 'PointScanLeftClick', async function() {
+      const website =
+          '<input type=checkbox style="width: 800px; height: 800px;">';
+      const rootWebArea = await this.runWithLoadedTree(website);
+      const checkbox = rootWebArea.find({role: 'checkBox'});
+      checkbox.doDefault();
+
+      const verifyChecked = checked => resolve => {
+        const checkedHandler = event => {
+          assertEquals(event.target.checked, String(checked));
+          event.target.removeEventListener(
+              chrome.automation.EventType.CHECKED_STATE_CHANGED,
+              checkedHandler);
+          resolve();
+        };
+        checkbox.addEventListener(
+            chrome.automation.EventType.CHECKED_STATE_CHANGED, checkedHandler);
+      };
+      await new Promise(verifyChecked(true));
+
+      SwitchAccess.mode = Mode.POINT_SCAN;
+      Navigator.byPoint.point_ = {x: 600, y: 600};
+      Navigator.byPoint.performMouseAction(MenuAction.LEFT_CLICK);
+      await new Promise(verifyChecked(false));
+    });
+
+AX_TEST_F(
+    'SwitchAccessPointScanManagerTest', 'PointScanRightClick',
+    async function() {
+      const website = '<p>Kittens r cute</p>';
+      const rootWebArea = await this.runWithLoadedTree(website);
+      const findParams = {role: 'menuItem', attributes: {name: /Back.*/}};
+      // Context menu with back button shouldn't exist yet.
+      const initialMenuItem = rootWebArea.find(findParams);
+      assertEquals(initialMenuItem, null);
+
+      const menuItemLoaded = () => resolve => {
+        const observer = treeChange => {
+          // Wait for the context menu with the back button to show up.
+          const menuItem = treeChange.target.find(findParams);
+          if (menuItem !== null) {
+            chrome.automation.removeTreeChangeObserver(observer);
+            resolve();
+          }
+        };
+        chrome.automation.addTreeChangeObserver('allTreeChanges', observer);
+      };
+
+      SwitchAccess.mode = Mode.POINT_SCAN;
+      Navigator.byPoint.point_ = {x: 400, y: 400};
+      Navigator.byPoint.performMouseAction(MenuAction.RIGHT_CLICK);
+      await new Promise(menuItemLoaded());
+    });
+
+// Verifies that chrome.accessibilityPrivate.setFocusRings() is not called when
+// point scanning is running.
+AX_TEST_F(
+    'SwitchAccessPointScanManagerTest', 'PointScanNoFocusRings',
+    async function() {
+      const sleep = () => {
+        return new Promise(resolve => setTimeout(resolve, 2 * 1000));
+      };
+
+      const site = '<button>Test</button>';
+      const rootWebArea = await this.runWithLoadedTree(site);
+      let setFocusRingsCallCount = 0;
+      // Mock this API to track how many times it's called.
+      chrome.accessibilityPrivate.setFocusRings = focusRings => {
+        setFocusRingsCallCount += 1;
+      };
+      assertEquals(0, setFocusRingsCallCount);
+      Navigator.byPoint.start();
+      // When point scanning starts, setFocusRings() gets called once to clear
+      // the focus rings.
+      assertEquals(1, setFocusRingsCallCount);
+      // Simulate the page focusing the button.
+      const button =
+          rootWebArea.find({role: chrome.automation.RoleType.BUTTON});
+      assertNotNullNorUndefined(button);
+      button.focus();
+      // Allow point scanning to run for 2 seconds and ensure no extra calls to
+      // setFocusRings().
+      await sleep();
+      assertEquals(1, setFocusRingsCallCount);
+    });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/settings_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/settings_manager.ts
new file mode 100644
index 0000000..67b7c6d
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/settings_manager.ts
@@ -0,0 +1,75 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {Settings} from '/common/settings.js';
+
+import {AutoScanManager} from './auto_scan_manager.js';
+
+/**
+ * Class to manage user preferences.
+ */
+export class SettingsManager {
+  static async init(): Promise<void> {
+    await Settings.init(Object.values(Preference));
+    Settings.addListener(
+        Preference.AUTO_SCAN_ENABLED,
+        (value: boolean|number) =>
+            AutoScanManager.setEnabled(value as boolean));
+    Settings.addListener(
+        Preference.AUTO_SCAN_TIME,
+        (value: boolean|number) =>
+            AutoScanManager.setPrimaryScanTime(value as number));
+    Settings.addListener(
+        Preference.AUTO_SCAN_KEYBOARD_TIME,
+        (value: boolean|number) =>
+            AutoScanManager.setKeyboardScanTime(value as number));
+
+    if (!SettingsManager.settingsAreConfigured_()) {
+      chrome.accessibilityPrivate.openSettingsSubpage(
+          'manageAccessibility/switchAccess');
+    }
+  }
+
+  // =============== Private Methods ==============
+
+  /**
+   * Whether the current settings configuration is reasonably usable;
+   * specifically, whether there is a way to select and a way to navigate.
+   */
+  private static settingsAreConfigured_(): boolean {
+    const selectPref = Settings.get(Preference.SELECT_DEVICE_KEY_CODES);
+    const selectSet = selectPref ? Object.keys(selectPref).length : false;
+
+    const nextPref = Settings.get(Preference.NEXT_DEVICE_KEY_CODES);
+    const nextSet = nextPref ? Object.keys(nextPref).length : false;
+
+    const previousPref = Settings.get(Preference.PREVIOUS_DEVICE_KEY_CODES);
+    const previousSet = previousPref ? Object.keys(previousPref).length : false;
+
+    const autoScanEnabled = Settings.get(Preference.AUTO_SCAN_ENABLED);
+
+    if (!selectSet) {
+      return false;
+    }
+
+    if (nextSet || previousSet) {
+      return true;
+    }
+
+    return Boolean(autoScanEnabled);
+  }
+}
+
+/** Preferences that are configurable in Switch Access. */
+enum Preference {
+  AUTO_SCAN_ENABLED = 'settings.a11y.switch_access.auto_scan.enabled',
+  AUTO_SCAN_TIME = 'settings.a11y.switch_access.auto_scan.speed_ms',
+  AUTO_SCAN_KEYBOARD_TIME =
+      'settings.a11y.switch_access.auto_scan.keyboard.speed_ms',
+  NEXT_DEVICE_KEY_CODES = 'settings.a11y.switch_access.next.device_key_codes',
+  PREVIOUS_DEVICE_KEY_CODES =
+      'settings.a11y.switch_access.previous.device_key_codes',
+  SELECT_DEVICE_KEY_CODES =
+      'settings.a11y.switch_access.select.device_key_codes',
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access.ts
new file mode 100644
index 0000000..233cf6c
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access.ts
@@ -0,0 +1,149 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {AsyncUtil} from '/common/async_util.js';
+import {EventHandler} from '/common/event_handler.js';
+import {FlagName, Flags} from '/common/flags.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {Navigator} from './navigator.js';
+import {KeyboardRootNode} from './nodes/keyboard_node.js';
+import {ErrorType, Mode} from './switch_access_constants.js';
+
+type AutomationEvent = chrome.automation.AutomationEvent;
+type AutomationNode = chrome.automation.AutomationNode;
+const EventType = chrome.automation.EventType;
+type FindParams = chrome.automation.FindParams;
+const RoleType = chrome.automation.RoleType;
+
+let readyCallback: VoidFunction;
+const readyPromise: Promise<void> =
+    new Promise(resolve => readyCallback = resolve);
+
+/**
+ * The top-level class for the Switch Access accessibility feature. Handles
+ * initialization and small matters that don't fit anywhere else in the
+ * codebase.
+ */
+export class SwitchAccess {
+  static instance?: SwitchAccess;
+  static mode = Mode.ITEM_SCAN;
+  private constructor() {}
+
+  static async init(desktop: AutomationNode): Promise<void> {
+    if (SwitchAccess.instance) {
+      throw new Error('Cannot create two SwitchAccess.instances');
+    }
+    SwitchAccess.instance = new SwitchAccess();
+
+    const currentFocus = await AsyncUtil.getFocus();
+    await SwitchAccess.instance.waitForFocus_(desktop, currentFocus);
+  }
+
+  /** Starts Switch Access behavior. */
+  static start(): void {
+    KeyboardRootNode.startWatchingVisibility();
+    Navigator.byItem.start();
+    readyCallback();
+  }
+
+  static async ready(): Promise<void> {
+    return readyPromise;
+  }
+
+  /**
+   * Returns whether or not the feature flag
+   * for improved text input is enabled.
+   */
+  static improvedTextInputEnabled(): boolean {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    return Flags.isEnabled(FlagName.SWITCH_ACCESS_TEXT)!;
+  }
+
+  /**
+   * Helper function to robustly find a node fitting a given FindParams, even if
+   * that node has not yet been created.
+   * Used to find the menu and back button.
+   */
+  static findNodeMatching(
+      findParams: FindParams,
+      foundCallback: (node: AutomationNode) => void): void {
+    const desktop = Navigator.byItem.desktopNode;
+    // First, check if the node is currently in the tree.
+    let node = desktop.find(findParams);
+    if (node) {
+      foundCallback(node);
+      return;
+    }
+    // If it's not currently in the tree, listen for changes to the desktop
+    // tree.
+    const eventHandler = new EventHandler(
+        desktop, EventType.CHILDREN_CHANGED, (_evt: AutomationEvent) => {});
+
+    const onEvent = (event: AutomationEvent): void => {
+      if (event.target.matches(findParams)) {
+        // If the event target is the node we're looking for, we've found it.
+        eventHandler.stop();
+        foundCallback(event.target);
+      } else if (event.target.children.length > 0) {
+        // Otherwise, see if one of its children is the node we're looking for.
+        node = event.target.find(findParams);
+        if (node) {
+          eventHandler.stop();
+          foundCallback(node);
+        }
+      }
+    };
+
+    eventHandler.setCallback(onEvent);
+    eventHandler.start();
+  }
+
+  /** Creates and records the specified error. */
+  static error(
+      errorType: ErrorType, errorString: string, shouldRecover = false): Error {
+    if (shouldRecover) {
+      setTimeout(Navigator.byItem.moveToValidNode.bind(Navigator.byItem), 0);
+    }
+    const errorTypeCountForUMA = Object.keys(ErrorType).length;
+    chrome.metricsPrivate.recordEnumerationValue(
+        'Accessibility.CrosSwitchAccess.Error', errorType,
+        errorTypeCountForUMA);
+    return new Error(errorString);
+  }
+
+  private async waitForFocus_(
+      desktop: AutomationNode,
+      currentFocus: AutomationNode|undefined): Promise<void> {
+    return new Promise(resolve => {
+      // Focus is available. Finish init without waiting for further events.
+      // Disallow web view nodes, which indicate a root web area is still
+      // loading and pending focus.
+      if (currentFocus && currentFocus.role !== RoleType.WEB_VIEW) {
+        resolve();
+        return;
+      }
+
+      // Wait for the focus to be sent. If |currentFocus| was undefined, this is
+      // guaranteed. Otherwise, also set a timed callback to ensure we do
+      // eventually init.
+      let callbackId = 0;
+      const listener = (maybeEvent: AutomationEvent|undefined): void => {
+        if (maybeEvent && maybeEvent.target.role === RoleType.WEB_VIEW) {
+          return;
+        }
+
+        desktop.removeEventListener(EventType.FOCUS, listener, false);
+        clearTimeout(callbackId);
+
+        resolve();
+      };
+
+      desktop.addEventListener(EventType.FOCUS, listener, false);
+      callbackId = setTimeout(listener, 5000);
+    });
+  }
+}
+
+TestImportManager.exportForTesting(SwitchAccess);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_constants.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_constants.ts
new file mode 100644
index 0000000..c034aba
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_constants.ts
@@ -0,0 +1,63 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Constants used in Switch Access.
+ */
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+/** When an action is performed, how the menu should respond. */
+export enum ActionResponse {
+  NO_ACTION_TAKEN = -1,
+  REMAIN_OPEN,
+  CLOSE_MENU,
+  EXIT_SUBMENU,
+  RELOAD_MENU,
+  OPEN_TEXT_NAVIGATION_MENU,
+}
+
+/**
+ * The types of error or unexpected state that can be encountered by Switch
+ * Access.
+ * These values are persisted to logs and should not be renumbered or re-used.
+ * See tools/metrics/histograms/enums.xml.
+ */
+export enum ErrorType {
+  UNKNOWN = 0,
+  PREFERENCE_TYPE = 1,
+  UNTRANSLATED_STRING = 2,
+  INVALID_COLOR = 3,
+  NEXT_UNDEFINED = 4,
+  PREVIOUS_UNDEFINED = 5,
+  NULL_CHILD = 6,
+  NO_CHILDREN = 7,
+  MALFORMED_DESKTOP = 8,
+  MISSING_LOCATION = 9,
+  MISSING_KEYBOARD = 10,
+  ROW_TOO_SHORT = 11,
+  MISSING_BASE_NODE = 12,
+  NEXT_INVALID = 13,
+  PREVIOUS_INVALID = 14,
+  INVALID_SELECTION_BOUNDS = 15,
+  UNINITIALIZED = 16,
+  DUPLICATE_INITIALIZATION = 17,
+}
+
+/** The different types of menus and sub-menus that can be shown. */
+export enum MenuType {
+  MAIN_MENU,
+  TEXT_NAVIGATION,
+  POINT_SCAN_MENU,
+}
+
+/**
+ * The modes of interaction the user can select for how to interact with the
+ * device.
+ */
+export enum Mode {
+  ITEM_SCAN,
+  POINT_SCAN,
+}
+
+TestImportManager.exportForTesting(['Mode', Mode]);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_e2e_test_base.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_e2e_test_base.js
new file mode 100644
index 0000000..786b1f1
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_e2e_test_base.js
@@ -0,0 +1,129 @@
+// 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.
+
+GEN_INCLUDE([
+  '../../common/testing/assert_additions.js',
+  '../../common/testing/e2e_test_base.js',
+]);
+
+/** Base class for browser tests for Switch Access. */
+SwitchAccessE2ETest = class extends E2ETestBase {
+  /** @override */
+  testGenCppIncludes() {
+    super.testGenCppIncludes();
+    GEN(`
+#include "ash/keyboard/ui/keyboard_util.h"
+#include "ash/accessibility/accessibility_controller.h"
+#include "ash/constants/ash_pref_names.h"
+#include "chrome/browser/ash/accessibility/accessibility_manager.h"
+    `);
+  }
+
+  /** @override */
+  testGenPreamble() {
+    super.testGenPreamble();
+    GEN(`
+    auto* controller = ash::AccessibilityController::Get();
+    controller->DisableSwitchAccessDisableConfirmationDialogTesting();
+    // Don't show the dialog saying Switch Access was enabled.
+    controller->DisableSwitchAccessEnableNotificationTesting();
+    // Set some Switch Access prefs so that the os://settings page is not
+    // opened (this is done if settings are not configured on first use):
+    auto* manager = ash::AccessibilityManager::Get();
+    manager->SetSwitchAccessKeysForTest(
+        {'1', 'A'}, ash::prefs::kAccessibilitySwitchAccessNextDeviceKeyCodes);
+    manager->SetSwitchAccessKeysForTest(
+        {'2', 'B'},
+        ash::prefs::kAccessibilitySwitchAccessSelectDeviceKeyCodes);
+  base::OnceClosure load_cb =
+      base::BindOnce(&ash::AccessibilityManager::SetSwitchAccessEnabled,
+          base::Unretained(ash::AccessibilityManager::Get()),
+          true);
+    `);
+    super.testGenPreambleCommon('kSwitchAccessExtensionId');
+  }
+
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    await SwitchAccess.ready();
+  }
+
+  /**
+   * @param {string} id The HTML id of an element.
+   * @return {!AutomationNode}
+   */
+  findNodeById(id) {
+    const predicate = node => node.htmlId === id;
+    const nodeString = 'node with id "' + id + '"';
+    return this.findNodeMatchingPredicate(predicate, nodeString);
+  }
+
+  /**
+   * @param {string} name The name of the node within the automation tree.
+   * @param {string} role The node's role.
+   * @return {!AutomationNode}
+   */
+  findNodeByNameAndRole(name, role) {
+    const predicate = node => node.name === name && node.role === role;
+    const nodeString = 'node with name "' + name + '" and role ' + role;
+    return this.findNodeMatchingPredicate(predicate, nodeString);
+  }
+
+  /**
+   * @param {function(): boolean} predicate The condition under which the
+   *     callback should be fired.
+   * @param {function()} callback
+   */
+  waitForPredicate(predicate, callback) {
+    this.listenUntil(predicate, this.desktop_, 'childrenChanged', callback);
+  }
+
+  /**
+   * @param {!Object} expected
+   * @return {!Promise}
+   */
+  untilFocusIs(expected) {
+    const doesMatch = expected => {
+      const newNode = Navigator.byItem.node_;
+      const automationNode = newNode.automationNode || {};
+      return (!expected.instance || newNode instanceof expected.instance) &&
+          (!expected.role || expected.role === automationNode.role) &&
+          (!expected.name || expected.name === automationNode.name) &&
+          (!expected.className ||
+           expected.className === automationNode.className);
+    };
+
+    let didResolve = false;
+    let lastFocusChangeTime = new Date();
+    const id = setInterval(() => {
+      if (didResolve) {
+        clearInterval(id);
+        return;
+      }
+
+      if ((new Date() - lastFocusChangeTime) < 3000) {
+        return;
+      }
+
+      console.log(
+          `\nStill waiting for expectation: ${JSON.stringify(expected)}\n` +
+          `Focus is: ${Navigator.byItem.node_.debugString()}`);
+    }, 1000);
+    return new Promise(resolve => {
+      if (doesMatch(expected)) {
+        didResolve = true;
+        resolve(Navigator.byItem.node_);
+        return;
+      }
+      this.addCallbackPostMethod(Navigator.byItem, 'setNode_', node => {
+        lastFocusChangeTime = new Date();
+        if (doesMatch(expected)) {
+          didResolve = true;
+          resolve(Navigator.byItem.node_);
+        }
+      }, () => doesMatch(expected));
+    });
+  }
+};
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_loader.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_loader.ts
new file mode 100644
index 0000000..9524f33
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_loader.ts
@@ -0,0 +1,38 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '/common/testing/test_import_manager.js';
+
+import {AsyncUtil} from '/common/async_util.js';
+import {Flags} from '/common/flags.js';
+import {InstanceChecker} from '/common/instance_checker.js';
+
+import {ActionManager} from './action_manager.js';
+import {AutoScanManager} from './auto_scan_manager.js';
+import {SACommands} from './commands.js';
+import {FocusRingManager} from './focus_ring_manager.js';
+import {Navigator} from './navigator.js';
+import {SettingsManager} from './settings_manager.js';
+import {SwitchAccess} from './switch_access.js';
+
+InstanceChecker.closeExtraInstances();
+
+async function initAll(): Promise<void> {
+  await Flags.init();
+  const desktop = await AsyncUtil.getDesktop();
+  await SwitchAccess.init(desktop);
+
+  // Navigator must be initialized before other classes.
+  Navigator.initializeSingletonInstances(desktop);
+
+  ActionManager.init();
+  AutoScanManager.init();
+  FocusRingManager.init();
+  SACommands.init();
+  SettingsManager.init();
+
+  SwitchAccess.start();
+}
+
+initAll();
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_predicate.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_predicate.ts
new file mode 100644
index 0000000..0e121dd
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_predicate.ts
@@ -0,0 +1,273 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {AutomationPredicate} from '/common/automation_predicate.js';
+import {RectUtil} from '/common/rect_util.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+import {AutomationTreeWalkerRestriction} from '/common/tree_walker.js';
+
+import {SACache} from './cache.js';
+import {SARootNode} from './nodes/switch_access_node.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+const StateType = chrome.automation.StateType;
+const Restriction = chrome.automation.Restriction;
+const RoleType = chrome.automation.RoleType;
+const DefaultActionVerb = chrome.automation.DefaultActionVerb;
+
+/**
+ * Contains predicates for the chrome automation API. The following basic
+ * predicates are available:
+ *    - isActionable
+ *    - isGroup
+ *    - isInteresting
+ *    - isInterestingSubtree
+ *    - isVisible
+ *    - isTextInput
+ *
+ * In addition to these basic predicates, there are also methods to get the
+ * restrictions required by TreeWalker for specific traversal situations.
+ */
+export namespace SwitchAccessPredicate {
+  export const GROUP_INTERESTING_CHILD_THRESHOLD = 2;
+
+  /**
+   * Returns true if |node| is actionable, meaning that a user can interact with
+   * it in some way.
+   */
+  export function isActionable(node: AutomationNode|undefined, cache: SACache):
+      boolean {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (cache.isActionable.has(node!)) {
+      return cache.isActionable.get(node!)!;
+    }
+
+    const defaultActionVerb = node!.defaultActionVerb;
+
+    // Skip things that are offscreen or invisible.
+    if (!SwitchAccessPredicate.isVisible(node!)) {
+      cache.isActionable.set(node!, false);
+      return false;
+    }
+
+    // Skip things that are disabled.
+    if (node!.restriction === Restriction.DISABLED) {
+      cache.isActionable.set(node!, false);
+      return false;
+    }
+
+    // These web containers are not directly actionable.
+    if (AutomationPredicate.structuralContainer(node!)) {
+      cache.isActionable.set(node!, false);
+      return false;
+    }
+
+    // Check various indicators that the node is actionable.
+    const actionableRole = AutomationPredicate.roles(
+        [RoleType.BUTTON, RoleType.SLIDER, RoleType.TAB]);
+    if (actionableRole(node!)) {
+      cache.isActionable.set(node!, true);
+      return true;
+    }
+
+    if (AutomationPredicate.comboBox(node!) ||
+        SwitchAccessPredicate.isTextInput(node!)) {
+      cache.isActionable.set(node!, true);
+      return true;
+    }
+
+    if (defaultActionVerb &&
+        (defaultActionVerb === DefaultActionVerb.ACTIVATE ||
+         defaultActionVerb === DefaultActionVerb.CHECK ||
+         defaultActionVerb === DefaultActionVerb.OPEN ||
+         defaultActionVerb === DefaultActionVerb.PRESS ||
+         defaultActionVerb === DefaultActionVerb.SELECT ||
+         defaultActionVerb === DefaultActionVerb.UNCHECK)) {
+      cache.isActionable.set(node!, true);
+      return true;
+    }
+
+    if (node!.role === RoleType.LIST_ITEM &&
+        defaultActionVerb === DefaultActionVerb.CLICK) {
+      cache.isActionable.set(node!, true);
+      return true;
+    }
+
+    // Focusable items should be surfaced as either groups or actionable. So
+    // should menu items.
+    // Current heuristic is to show as actionble any focusable item where no
+    // child is an interesting subtree.
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (node!.state![StateType.FOCUSABLE] ||
+        node!.role === RoleType.MENU_ITEM) {
+      const result = !node!.children.some(
+          (child: AutomationNode) =>
+              SwitchAccessPredicate.isInterestingSubtree(child, cache));
+      cache.isActionable.set(node!, result);
+      return result;
+    }
+    return false;
+  }
+
+  /**
+   * Returns true if |node| is a group, meaning that the node has more than one
+   * interesting descendant, and that its interesting descendants exist in more
+   * than one subtree of its immediate children.
+   *
+   * Additionally, for |node| to be a group, it cannot have the same bounding
+   * box as its scope.
+   */
+  export function isGroup(
+      node: AutomationNode|undefined, scope: AutomationNode|SARootNode|null,
+      cache: SACache): boolean {
+    // If node is invalid (undefined or an undefined role), return false.
+    if (!node || !node.role) {
+      return false;
+    }
+    if (cache.isGroup.has(node)) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      return cache.isGroup.get(node)!;
+    }
+
+    const scopeEqualsNode = scope &&
+        (scope instanceof SARootNode ? scope.isEquivalentTo(node) :
+                                       scope === node);
+    if (scope && !scopeEqualsNode &&
+        RectUtil.equal(node.location, scope.location)) {
+      cache.isGroup.set(node, false);
+      return false;
+    }
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (node.state![StateType.INVISIBLE]) {
+      cache.isGroup.set(node, false);
+      return false;
+    }
+
+    if (node.role === RoleType.KEYBOARD) {
+      cache.isGroup.set(node, true);
+      return true;
+    }
+
+    let interestingBranchesCount =
+        SwitchAccessPredicate.isActionable(node, cache) ? 1 : 0;
+    let child = node.firstChild;
+    while (child) {
+      if (SwitchAccessPredicate.isInterestingSubtree(child, cache)) {
+        interestingBranchesCount += 1;
+      }
+      if (interestingBranchesCount >=
+          SwitchAccessPredicate.GROUP_INTERESTING_CHILD_THRESHOLD) {
+        cache.isGroup.set(node, true);
+        return true;
+      }
+      child = child.nextSibling;
+    }
+    cache.isGroup.set(node, false);
+    return false;
+  }
+
+  /**
+   * Returns true if |node| is interesting for the user, meaning that |node|
+   * is either actionable or a group.
+   */
+  export function isInteresting(
+      node: AutomationNode|undefined, scope: AutomationNode|SARootNode,
+      cache?: SACache): boolean {
+    cache = cache || new SACache();
+    return SwitchAccessPredicate.isActionable(node, cache) ||
+        SwitchAccessPredicate.isGroup(node, scope, cache);
+  }
+
+  /** Returns true if the element is visible to the user for any reason. */
+  export function isVisible(node: AutomationNode): boolean {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    return Boolean(
+        !node.state![StateType.OFFSCREEN] && node.location &&
+        node.location.top >= 0 && node.location.left >= 0 &&
+        !node.state![StateType.INVISIBLE]);
+  }
+
+  /**
+   * Returns true if there is an interesting node in the subtree containing
+   * |node| as its root (including |node| itself).
+   *
+   * This function does not call isInteresting directly, because that would
+   * cause a loop (isInteresting calls isGroup, and isGroup calls
+   * isInterestingSubtree).
+   */
+  export function isInterestingSubtree(node: AutomationNode, cache?: SACache):
+      boolean {
+    cache = cache || new SACache();
+    if (cache.isInterestingSubtree.has(node)) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      return cache.isInterestingSubtree.get(node)!;
+    }
+    const result = SwitchAccessPredicate.isActionable(node, cache) ||
+        node.children.some(
+            child => SwitchAccessPredicate.isInterestingSubtree(child, cache));
+    cache.isInterestingSubtree.set(node, result);
+    return result;
+  }
+
+  /** Returns true if |node| is an element that contains editable text. */
+  export function isTextInput(node?: AutomationNode): boolean {
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    return Boolean(node && node.state![StateType.EDITABLE]);
+  }
+
+  /** Returns true if |node| should be considered a window. */
+  export function isWindow(node?: AutomationNode): boolean {
+    return Boolean(
+        node &&
+        (node.role === RoleType.WINDOW ||
+         (node.role === RoleType.CLIENT && node.parent &&
+          node.parent.role === RoleType.WINDOW)));
+  }
+
+  /**
+   * Returns a Restrictions object ready to be passed to AutomationTreeWalker.
+   */
+  export function restrictions(scope: AutomationNode):
+      AutomationTreeWalkerRestriction {
+    const cache = new SACache();
+    return {
+      leaf: SwitchAccessPredicate.leaf(scope, cache),
+      root: SwitchAccessPredicate.root(scope),
+      visit: SwitchAccessPredicate.visit(scope, cache),
+    };
+  }
+
+  /**
+   * Creates a function that confirms if |node| is a terminal leaf node of a
+   * SwitchAccess scope tree when |scope| is the root.
+   */
+  export function leaf(scope: AutomationNode, cache: SACache):
+      AutomationPredicate.Unary {
+    return (node: AutomationNode) =>
+               !SwitchAccessPredicate.isInterestingSubtree(node, cache) ||
+        (scope !== node &&
+         SwitchAccessPredicate.isInteresting(node, scope, cache));
+  }
+
+  /**
+   * Creates a function that confirms if |node| is the root of a SwitchAccess
+   * scope tree when |scope| is the root.
+   */
+  export function root(scope: AutomationNode): AutomationPredicate.Unary {
+    return (node: AutomationNode) => scope === node;
+  }
+
+  /**
+   * Creates a function that determines whether |node| is to be visited in the
+   * SwitchAccess scope tree with |scope| as the root.
+   */
+  export function visit(scope: AutomationNode, cache: SACache):
+      AutomationPredicate.Unary {
+    return (node: AutomationNode) => node.role !== RoleType.DESKTOP &&
+        SwitchAccessPredicate.isInteresting(node, scope, cache);
+  }
+}
+
+TestImportManager.exportForTesting(
+    ['SwitchAccessPredicate', SwitchAccessPredicate]);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_predicate_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_predicate_test.js
new file mode 100644
index 0000000..ea43c43
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_predicate_test.js
@@ -0,0 +1,470 @@
+// 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.
+
+GEN_INCLUDE(['switch_access_e2e_test_base.js']);
+
+/** Test fixture for the Switch Access predicates. */
+SwitchAccessPredicateTest = class extends SwitchAccessE2ETest {};
+
+function fakeLoc(x) {
+  return {left: x, top: x, width: x, height: x};
+}
+
+// This page has a 1:1 correlation between DOM nodes and accessibility nodes.
+function testWebsite() {
+  return `<div aria-description="upper1">
+            <div aria-description="lower1">
+              <button>leaf1</button>
+              <p aria-description="leaf2">leaf2</p>
+              <button>leaf3</button>
+            </div>
+            <div aria-description="lower2">
+              <p aria-description="leaf4">leaf4</p>
+              <button>leaf5</button>
+            </div>
+          </div>
+          <div aria-description="upper2" role="button">
+            <div aria-description="lower3" >
+              <p aria-description="leaf6">leaf6</p>
+              <p aria-description="leaf7">leaf7</p>`;
+}
+
+function getTree(loadedPage) {
+  const root = loadedPage;
+  assertTrue(
+      root.role === chrome.automation.RoleType.ROOT_WEB_AREA,
+      'We should receive the root web area');
+  assertTrue(
+      root.firstChild && root.firstChild.description === 'upper1',
+      'First child should be upper1');
+
+  const upper1 = root.firstChild;
+  assertTrue(upper1 && upper1.description === 'upper1', 'Upper1 not found');
+  const upper2 = upper1.nextSibling;
+  assertTrue(upper2 && upper2.description === 'upper2', 'Upper2 not found');
+  const lower1 = upper1.firstChild;
+  assertTrue(lower1 && lower1.description === 'lower1', 'Lower1 not found');
+  const lower2 = lower1.nextSibling;
+  assertTrue(lower2 && lower2.description === 'lower2', 'Lower2 not found');
+  const lower3 = upper2.firstChild;
+  assertTrue(lower3 && lower3.description === 'lower3', 'Lower3 not found');
+  const leaf1 = lower1.firstChild;
+  assertTrue(leaf1 && leaf1.name === 'leaf1', 'Leaf1 not found');
+  const leaf2 = leaf1.nextSibling;
+  assertTrue(leaf2 && leaf2.description === 'leaf2', 'Leaf2 not found');
+  const leaf3 = leaf2.nextSibling;
+  assertTrue(leaf3 && leaf3.name === 'leaf3', 'Leaf3 not found');
+  const leaf4 = lower2.firstChild;
+  assertTrue(leaf4 && leaf4.description === 'leaf4', 'Leaf4 not found');
+  const leaf5 = leaf4.nextSibling;
+  assertTrue(leaf5 && leaf5.name === 'leaf5', 'Leaf5 not found');
+  const leaf6 = lower3.firstChild;
+  assertTrue(leaf6 && leaf6.description === 'leaf6', 'Leaf6 not found');
+  const leaf7 = leaf6.nextSibling;
+  assertTrue(leaf7 && leaf7.description === 'leaf7', 'Leaf7 not found');
+
+  return {
+    root,
+    upper1,
+    upper2,
+    lower1,
+    lower2,
+    lower3,
+    leaf1,
+    leaf2,
+    leaf3,
+    leaf4,
+    leaf5,
+    leaf6,
+    leaf7,
+  };
+}
+
+AX_TEST_F('SwitchAccessPredicateTest', 'IsInteresting', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
+
+  // The scope is only used to verify the locations are not the same, and
+  // since the buildTree function depends on isInteresting, pass in null
+  // for the scope.
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.root, null, cache),
+      'Root should be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.upper1, null, cache),
+      'Upper1 should be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.upper2, null, cache),
+      'Upper2 should be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.lower1, null, cache),
+      'Lower1 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.lower2, null, cache),
+      'Lower2 should not be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.lower3, null, cache),
+      'Lower3 should not be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.leaf1, null, cache),
+      'Leaf1 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf2, null, cache),
+      'Leaf2 should not be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.leaf3, null, cache),
+      'Leaf3 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf4, null, cache),
+      'Leaf4 should not be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.leaf5, null, cache),
+      'Leaf5 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf6, null, cache),
+      'Leaf6 should not be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf7, null, cache),
+      'Leaf7 should not be interesting');
+});
+
+AX_TEST_F('SwitchAccessPredicateTest', 'IsGroup', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
+
+  // The scope is only used to verify the locations are not the same, and
+  // since the buildTree function depends on isGroup, pass in null for
+  // the scope.
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.root, null, cache),
+      'Root should be a group');
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.upper1, null, cache),
+      'Upper1 should be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.upper2, null, cache),
+      'Upper2 should not be a group');
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.lower1, null, cache),
+      'Lower1 should be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.lower2, null, cache),
+      'Lower2 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.lower3, null, cache),
+      'Lower3 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf1, null, cache),
+      'Leaf1 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf2, null, cache),
+      'Leaf2 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf3, null, cache),
+      'Leaf3 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf4, null, cache),
+      'Leaf4 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf5, null, cache),
+      'Leaf5 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf6, null, cache),
+      'Leaf6 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf7, null, cache),
+      'Leaf7 should not be a group');
+});
+
+AX_TEST_F(
+    'SwitchAccessPredicateTest', 'IsInterestingSubtree', async function() {
+      const loadedPage = await this.runWithLoadedTree(testWebsite());
+      const t = getTree(loadedPage);
+      const cache = new SACache();
+
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.root, cache),
+          'Root should be an interesting subtree');
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.upper1, cache),
+          'Upper1 should be an interesting subtree');
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.upper2, cache),
+          'Upper2 should be an interesting subtree');
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.lower1, cache),
+          'Lower1 should be an interesting subtree');
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.lower2, cache),
+          'Lower2 should be an interesting subtree');
+      assertFalse(
+          SwitchAccessPredicate.isInterestingSubtree(t.lower3, cache),
+          'Lower3 should not be an interesting subtree');
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.leaf1, cache),
+          'Leaf1 should be an interesting subtree');
+      assertFalse(
+          SwitchAccessPredicate.isInterestingSubtree(t.leaf2, cache),
+          'Leaf2 should not be an interesting subtree');
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.leaf3, cache),
+          'Leaf3 should be an interesting subtree');
+      assertFalse(
+          SwitchAccessPredicate.isInterestingSubtree(t.leaf4, cache),
+          'Leaf4 should not be an interesting subtree');
+      assertTrue(
+          SwitchAccessPredicate.isInterestingSubtree(t.leaf5, cache),
+          'Leaf5 should be an interesting subtree');
+      assertFalse(
+          SwitchAccessPredicate.isInterestingSubtree(t.leaf6, cache),
+          'Leaf6 should not be an interesting subtree');
+      assertFalse(
+          SwitchAccessPredicate.isInterestingSubtree(t.leaf7, cache),
+          'Leaf7 should not be an interesting subtree');
+    });
+
+AX_TEST_F('SwitchAccessPredicateTest', 'IsActionable', async function() {
+  const treeString =
+      `<button style="position:absolute; top:-100px;">offscreen</button>
+       <button disabled>disabled</button>
+       <a href="https://www.google.com/" aria-label="link1">link1</a>
+       <input type="text" aria-label="input1">input1</input>
+       <button>button3</button>
+       <input type="range" aria-label="slider" value=5 min=0 max=10>
+       <ol><div id="clickable" role="listitem" onclick="2+2"></div></ol>
+       <div id="div1"><p>p1</p></div>`;
+  const loadedPage = await this.runWithLoadedTree(treeString);
+  const cache = new SACache();
+
+  const offscreenButton = this.findNodeByNameAndRole('offscreen', 'button');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(offscreenButton, cache),
+      'Offscreen objects should not be actionable');
+
+  const disabledButton = this.findNodeByNameAndRole('disabled', 'button');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(disabledButton, cache),
+      'Disabled objects should not be actionable');
+
+  assertFalse(
+      SwitchAccessPredicate.isActionable(loadedPage, cache),
+      'Root web area should not be directly actionable');
+
+  const link1 = this.findNodeByNameAndRole('link1', 'link');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(link1, cache),
+      'Links should be actionable');
+
+  const input1 = this.findNodeByNameAndRole('input1', 'textField');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(input1, cache),
+      'Inputs should be actionable');
+
+  const button3 = this.findNodeByNameAndRole('button3', 'button');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(button3, cache),
+      'Buttons should be actionable');
+
+  const slider = this.findNodeByNameAndRole('slider', 'slider');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(slider, cache),
+      'Sliders should be actionable');
+
+  const clickable = this.findNodeById('clickable');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(clickable, cache),
+      'Clickable list items should be actionable');
+
+  const div1 = this.findNodeById('div1');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(div1, cache),
+      'Divs should not generally be actionable');
+
+  const p1 = this.findNodeByNameAndRole('p1', 'staticText');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(p1, cache),
+      'Static text should not generally be actionable');
+});
+
+AX_TEST_F(
+    'SwitchAccessPredicateTest', 'IsActionableFocusableElements',
+    async function() {
+      const treeString = `<div id="noChildren" tabindex=0></div>
+       <div id="oneInterestingChild" tabindex=0>
+         <div>
+           <div>
+             <div>
+               <button>button1</button>
+             </div>
+           </div>
+         </div>
+       </div>
+       <div id="oneUninterestingChild" tabindex=0>
+         <p>p1</p>
+       </div>
+       <div id="interestingChildren" tabindex=0>
+         <button>button2</button>
+         <button>button3</button>
+       </div>
+       <div id="uninterestingChildren" tabindex=0>
+         <p>p2</p>
+         <p>p3</p>
+       </div>`;
+      await this.runWithLoadedTree(treeString);
+      const cache = new SACache();
+
+      const noChildren = this.findNodeById('noChildren');
+      assertTrue(
+          SwitchAccessPredicate.isActionable(noChildren, cache),
+          'Focusable element with no children should be actionable');
+
+      const oneInterestingChild = this.findNodeById('oneInterestingChild');
+      assertFalse(
+          SwitchAccessPredicate.isActionable(oneInterestingChild, cache),
+          'Focusable element with an interesting child should not be actionable');
+
+      const interestingChildren = this.findNodeById('interestingChildren');
+      assertFalse(
+          SwitchAccessPredicate.isActionable(interestingChildren, cache),
+          'Focusable element with interesting children should not be ' +
+              'actionable');
+
+      const oneUninterestingChild = this.findNodeById('oneUninterestingChild');
+      assertTrue(
+          SwitchAccessPredicate.isActionable(oneUninterestingChild, cache),
+          'Focusable element with one uninteresting child should be ' +
+              'actionable');
+
+      const uninterestingChildren = this.findNodeById('uninterestingChildren');
+      assertTrue(
+          SwitchAccessPredicate.isActionable(uninterestingChildren, cache),
+          'Focusable element with uninteresting children should be actionable');
+    });
+
+AX_TEST_F('SwitchAccessPredicateTest', 'LeafPredicate', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
+
+  // Start with root as scope
+  let leaf = SwitchAccessPredicate.leaf(t.root, cache);
+  assertFalse(leaf(t.root), 'Root should not be a leaf node');
+  assertTrue(leaf(t.upper1), 'Upper1 should be a leaf node for root tree');
+  assertTrue(leaf(t.upper2), 'Upper2 should be a leaf node for root tree');
+
+  // Set upper1 as scope
+  leaf = SwitchAccessPredicate.leaf(t.upper1, cache);
+  assertFalse(leaf(t.upper1), 'Upper1 should not be a leaf for upper1 tree');
+  assertTrue(leaf(t.lower1), 'Lower1 should be a leaf for upper1 tree');
+  assertTrue(leaf(t.leaf4), 'leaf4 should be a leaf for upper1 tree');
+  assertTrue(leaf(t.leaf5), 'leaf5 should be a leaf for upper1 tree');
+
+  // Set lower1 as scope
+  leaf = SwitchAccessPredicate.leaf(t.lower1, cache);
+  assertFalse(leaf(t.lower1), 'Lower1 should not be a leaf for lower1 tree');
+  assertTrue(leaf(t.leaf1), 'Leaf1 should be a leaf for lower1 tree');
+  assertTrue(leaf(t.leaf2), 'Leaf2 should be a leaf for lower1 tree');
+  assertTrue(leaf(t.leaf3), 'Leaf3 should be a leaf for lower1 tree');
+});
+
+AX_TEST_F('SwitchAccessPredicateTest', 'RootPredicate', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+
+  // Start with root as scope
+  let root = SwitchAccessPredicate.root(t.root);
+  assertTrue(root(t.root), 'Root should be a root of the root tree');
+  assertFalse(root(t.upper1), 'Upper1 should not be a root of the root tree');
+  assertFalse(root(t.upper2), 'Upper2 should not be a root of the root tree');
+
+  // Set upper1 as scope
+  root = SwitchAccessPredicate.root(t.upper1);
+  assertTrue(root(t.upper1), 'Upper1 should be a root of the upper1 tree');
+  assertFalse(root(t.lower1), 'Lower1 should not be a root of the upper1 tree');
+  assertFalse(root(t.lower2), 'Lower2 should not be a root of the upper1 tree');
+
+  // Set lower1 as scope
+  root = SwitchAccessPredicate.root(t.lower1);
+  assertTrue(root(t.lower1), 'Lower1 should be a root of the lower1 tree');
+  assertFalse(root(t.leaf1), 'Leaf1 should not be a root of the lower1 tree');
+  assertFalse(root(t.leaf2), 'Leaf2 should not be a root of the lower1 tree');
+  assertFalse(root(t.leaf3), 'Leaf3 should not be a root of the lower1 tree');
+});
+
+AX_TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
+
+  // Start with root as scope
+  let visit = SwitchAccessPredicate.visit(t.root, cache);
+  assertTrue(visit(t.root), 'Root should be visited in root tree');
+  assertTrue(visit(t.upper1), 'Upper1 should be visited in root tree');
+  assertTrue(visit(t.upper2), 'Upper2 should be visited in root tree');
+
+  // Set upper1 as scope
+  visit = SwitchAccessPredicate.visit(t.upper1, cache);
+  assertTrue(visit(t.upper1), 'Upper1 should be visited in upper1 tree');
+  assertTrue(visit(t.lower1), 'Lower1 should be visited in upper1 tree');
+  assertFalse(visit(t.lower2), 'Lower2 should not be visited in upper1 tree');
+  assertFalse(visit(t.leaf4), 'Leaf4 should not be visited in upper1 tree');
+  assertTrue(visit(t.leaf5), 'Leaf5 should be visited in upper1 tree');
+
+  // Set lower1 as scope
+  visit = SwitchAccessPredicate.visit(t.lower1, cache);
+  assertTrue(visit(t.lower1), 'Lower1 should be visited in lower1 tree');
+  assertTrue(visit(t.leaf1), 'Leaf1 should be visited in lower1 tree');
+  assertFalse(visit(t.leaf2), 'Leaf2 should not be visited in lower1 tree');
+  assertTrue(visit(t.leaf3), 'Leaf3 should be visited in lower1 tree');
+
+  // An uninteresting subtree should return false, regardless of scope
+  assertFalse(visit(t.lower3), 'Lower3 should not be visited in lower1 tree');
+  assertFalse(visit(t.leaf6), 'Leaf6 should not be visited in lower1 tree');
+  assertFalse(visit(t.leaf7), 'Leaf7 should not be visited in lower1 tree');
+});
+
+AX_TEST_F('SwitchAccessPredicateTest', 'Cache', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
+
+  let locationAccessCount = 0;
+  class TestRoot extends SARootNode {
+    /** @override */
+    get location() {
+      locationAccessCount++;
+      return null;
+    }
+  }
+  const group = new TestRoot(t.root);
+
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.root, group, cache),
+      'Root should be a group');
+  assertEquals(
+      locationAccessCount, 1,
+      'Location should have been accessed to calculate isGroup');
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.root, group, cache),
+      'isGroup value should not change');
+  assertEquals(
+      locationAccessCount, 1,
+      'Cache should have been used, avoiding second location access');
+
+  locationAccessCount = 0;
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
+      'Leaf should not be a group');
+  assertEquals(
+      locationAccessCount, 1,
+      'Location should have been accessed to calculate isGroup');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
+      'isGroup value should not change');
+  assertEquals(
+      locationAccessCount, 1,
+      'Cache should have been used, avoiding second location access');
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_test.js
new file mode 100644
index 0000000..3f1425b
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/switch_access_test.js
@@ -0,0 +1,58 @@
+// 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.
+
+GEN_INCLUDE(['switch_access_e2e_test_base.js']);
+
+/** Test fixture for the SwitchAccess class. */
+SwitchAccessSwitchAccessTest = class extends SwitchAccessE2ETest {
+  async waitForCallback() {
+    return new Promise(resolve => this.promiseCallback = resolve);
+  }
+};
+
+function resetState() {
+  delete Flags.instance;
+  delete SwitchAccess.instance;
+}
+
+AX_TEST_F(
+    'SwitchAccessSwitchAccessTest', 'NoFocusDefersInit', async function() {
+      await this.runWithLoadedTree('');
+      // Build a new SwitchAccess instance with hooks.
+      let initCount = 0;
+      const oldInit = SwitchAccess.init;
+      SwitchAccess.init = async (...args) => {
+        await oldInit(...args);
+        initCount++;
+        assertNotNullNorUndefined(this.promiseCallback);
+        this.promiseCallback();
+        delete this.promiseCallback;
+      };
+
+      // Stub this out so that focus is undefined.
+      chrome.automation.getFocus = callback => {
+        callback();
+        assertNotNullNorUndefined(this.promiseCallback);
+        this.promiseCallback();
+        delete this.promiseCallback;
+      };
+
+      // Reset state so there are no re-initialization errors.
+      resetState();
+
+      // Initialize; we should not have incremented initCount since there's no
+      // focus.
+      SwitchAccess.init(this.desktop);
+      await this.waitForCallback();
+      assertEquals(0, initCount);
+
+      // Reset state so there are no re-initialization errors.
+      resetState();
+
+      // Restub this to pass a "focused" node.
+      chrome.automation.getFocus = callback => callback({});
+      SwitchAccess.init(this.desktop);
+      await this.waitForCallback();
+      assertEquals(1, initCount);
+    });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/test_support.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/test_support.js
new file mode 100644
index 0000000..1ea6773
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/test_support.js
@@ -0,0 +1,119 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Test support for tests driven by C++.
+ */
+
+(async function() {
+  let module = await import('./nodes/back_button_node.js');
+  globalThis.BackButtonNode = module.BackButtonNode;
+
+  module = await import('./focus_ring_manager.js');
+  globalThis.FocusRingManager = module.FocusRingManager;
+
+  module = await import('./navigator.js');
+  globalThis.Navigator = module.Navigator;
+
+  module = await import('./switch_access_constants.js');
+  globalThis.Mode = module.Mode;
+
+  module = await import('./switch_access.js');
+  globalThis.SwitchAccess = module.SwitchAccess;
+  await SwitchAccess.ready();
+
+  const focusRingState = {
+    'primary': {'role': '', 'name': ''},
+    'preview': {'role': '', 'name': ''},
+  };
+  let expectedType = '';
+  let expectedRole = '';
+  let expectedName = '';
+  let successCallback = null;
+  const transcript = [];
+
+  function checkFocusRingState() {
+    if (expectedType !== '' &&
+        focusRingState[expectedType].role === expectedRole &&
+        focusRingState[expectedType].name === expectedName) {
+      if (successCallback) {
+        transcript.push(
+            `Success type=${expectedType} ` +
+            `role=${expectedRole} name=${expectedName}`);
+        successCallback();
+        successCallback = null;
+      }
+    }
+  }
+
+  function findAutomationNodeByName(name) {
+    return Navigator.byItem.desktopNode.find({attributes: {name}});
+  }
+
+  globalThis.doDefault = function(name, callback) {
+    transcript.push(`Performing default action on node with name=${name}`);
+    const node = findAutomationNodeByName(name);
+    node.doDefault();
+    callback();
+  };
+
+  globalThis.pointScanClick = function(x, y, callback) {
+    transcript.push(`Clicking with point scanning at location x=${x} y=${y}`);
+    SwitchAccess.mode = Mode.POINT_SCAN;
+    Navigator.byPoint.point_ = {x, y};
+    Navigator.byPoint.performMouseAction(
+        chrome.accessibilityPrivate.SwitchAccessMenuAction.LEFT_CLICK);
+    callback();
+  };
+
+  globalThis.waitForFocusRing = function(type, role, name, callback) {
+    transcript.push(`Waiting for type=${type} role=${role} name=${name}`);
+    expectedType = type;
+    expectedRole = role;
+    expectedName = name;
+    successCallback = callback;
+    checkFocusRingState();
+  };
+
+  globalThis.waitForEventOnAutomationNode = function(
+      eventType, name, callback) {
+    transcript.push(`Waiting for eventType=${eventType} name=${name}`);
+    const node = findAutomationNodeByName(name);
+    const listener = () => {
+      node.removeEventListener(eventType, listener);
+      callback();
+    };
+    node.addEventListener(eventType, listener);
+  };
+
+  FocusRingManager.setObserver((primary, preview) => {
+    if (primary && primary instanceof BackButtonNode) {
+      focusRingState['primary']['role'] = 'back';
+      focusRingState['primary']['name'] = '';
+    } else if (primary && primary.automationNode) {
+      const node = primary.automationNode;
+      focusRingState['primary']['role'] = node.role;
+      focusRingState['primary']['name'] = node.name;
+    } else {
+      focusRingState['primary']['role'] = '';
+      focusRingState['primary']['name'] = '';
+    }
+    if (preview && preview.automationNode) {
+      const node = preview.automationNode;
+      focusRingState['preview']['role'] = node.role;
+      focusRingState['preview']['name'] = node.name;
+    } else {
+      focusRingState['preview']['role'] = '';
+      focusRingState['preview']['name'] = '';
+    }
+    transcript.push(`Focus ring state: ${JSON.stringify(focusRingState)}`);
+    checkFocusRingState();
+  });
+  globalThis.domAutomationController.send('ready');
+
+  setInterval(() => {
+    console.error(
+        'Test still running. Transcript so far:\n' + transcript.join('\n'));
+  }, 5000);
+})();
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/test_utility.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/test_utility.js
new file mode 100644
index 0000000..945f5ed
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/test_utility.js
@@ -0,0 +1,105 @@
+// 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.
+
+/**
+ * Helper functions for tests driven from JS, especially SAATLite tests.
+ * Must call `await TestUtility.setup()` before using these functions.
+ *
+ * Available functions:
+ *     expectFocusOn(descriptor) - waits for Switch Access focus to land on a
+ *         node with properties exactly matching those in the descriptor.
+ *     pressNextSwitch()
+ *     pressPreviousSwitch()
+ *     pressSelectSwitch()
+ *     startFocusInside(rootWebArea) - moves focus to the first interesting
+ *         descendant of rootWebArea.
+ */
+const TestUtility = {
+  setup() {
+    FocusRingManager.instance.observer_ = TestUtility.whenFocusChanges_;
+  },
+
+  currentFocus: {primary: null, preview: null},
+
+  async expectFocusOn(descriptor) {
+    const current = TestUtility.currentFocus.primary;
+    if (current && TestUtility.matches_(current, descriptor)) {
+      return;
+    }
+
+    TestUtility.waitingForFocusDescriptor_ = descriptor;
+    await new Promise(
+        resolve => TestUtility.waitingForFocusResolver_ = resolve);
+  },
+
+  pressNextSwitch() {
+    SACommands.instance.runCommand_(
+        chrome.accessibilityPrivate.SwitchAccessCommand.NEXT);
+  },
+
+  pressPreviousSwitch() {
+    SACommands.instance.runCommand_(
+        chrome.accessibilityPrivate.SwitchAccessCommand.PREVIOUS);
+  },
+
+  pressSelectSwitch() {
+    SACommands.instance.runCommand_(
+        chrome.accessibilityPrivate.SwitchAccessCommand.SELECT);
+  },
+
+  /** Only call after runWithLoadedTree() */
+  startFocusInside(rootWebArea) {
+    if (!rootWebArea) {
+      throw new Error('Web root node is undefined');
+    }
+    if (!SwitchAccessPredicate.isInterestingSubtree(rootWebArea)) {
+      Navigator.byItem.moveTo_(rootWebArea);
+      return;
+    }
+
+    let node = rootWebArea;
+    while (!SwitchAccessPredicate.isInteresting(node)) {
+      for (const child of node.children) {
+        if (SwitchAccessPredicate.isInterestingSubtree(child)) {
+          node = child;
+          break;
+        }
+      }
+    }
+
+    Navigator.byItem.moveTo_(node);
+  },
+
+  // =============== Private Functions ==============
+
+  /** @private */
+  matches_(node, descriptor) {
+    for (const key of Object.keys(descriptor)) {
+      if (node[key] !== descriptor[key]) {
+        return false;
+      }
+    }
+    return true;
+  },
+
+  /** @private */
+  whenFocusChanges_(primary, preview) {
+    TestUtility.currentFocus.primary = primary;
+    TestUtility.currentFocus.preview = preview;
+
+    if (TestUtility.waitingForFocusResolver_ &&
+        TestUtility.matches_(primary, TestUtility.waitingForFocusDescriptor_)) {
+      TestUtility.waitingForFocusResolver_();
+
+      TestUtility.waitingForFocusDescriptor_ = null;
+      TestUtility.waitingForFocusResolver_ = null;
+    }
+  },
+
+  /** @private */
+  waitingForFocusDescriptor_: null,
+
+  /** @private */
+  waitingForFocusResolver_: null,
+};
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/text_navigation_manager.ts b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/text_navigation_manager.ts
new file mode 100644
index 0000000..204bc87
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/text_navigation_manager.ts
@@ -0,0 +1,368 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {EventGenerator} from '/common/event_generator.js';
+import {EventHandler} from '/common/event_handler.js';
+import {KeyCode} from '/common/key_code.js';
+import {TestImportManager} from '/common/testing/test_import_manager.js';
+
+import {ActionManager} from './action_manager.js';
+import {Navigator} from './navigator.js';
+import {SwitchAccess} from './switch_access.js';
+import {ErrorType} from './switch_access_constants.js';
+
+type AutomationNode = chrome.automation.AutomationNode;
+const EventType = chrome.automation.EventType;
+const MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
+
+/**
+ * Class to handle navigating text. Currently, only
+ * navigation and selection in editable text fields is supported.
+ */
+export class TextNavigationManager {
+  private static instance_?: TextNavigationManager;
+
+  private currentlySelecting_ = false;
+  /** Keeps track of when there's a selection in the current node. */
+  private selectionExists_ = false;
+  /** Keeps track of when the clipboard is empty. */
+  private clipboardHasData_ = false;
+
+  private selectionStartIndex_ = TextNavigationManager.NO_SELECT_INDEX;
+  private selectionStartObject_?: AutomationNode;
+  private selectionEndIndex_ = TextNavigationManager.NO_SELECT_INDEX;
+  private selectionEndObject_?: AutomationNode;
+  private selectionListener_: EventHandler;
+
+  private constructor() {
+    this.selectionListener_ = new EventHandler(
+        [], EventType.TEXT_SELECTION_CHANGED, () => this.onNavChange_());
+
+    if (SwitchAccess.improvedTextInputEnabled()) {
+      chrome.clipboard.onClipboardDataChanged.addListener(
+          () => this.updateClipboardHasData_());
+    }
+  }
+
+  static get instance(): TextNavigationManager {
+    if (!TextNavigationManager.instance_) {
+      TextNavigationManager.instance_ = new TextNavigationManager();
+    }
+    return TextNavigationManager.instance_;
+  }
+
+  // =============== Static Methods ==============
+
+  /**
+   * Returns if the selection start index is set in the current node.
+   */
+  static currentlySelecting(): boolean {
+    const manager = TextNavigationManager.instance;
+    return (
+        manager.selectionStartIndex_ !==
+            TextNavigationManager.NO_SELECT_INDEX &&
+        manager.currentlySelecting_);
+  }
+
+  /**
+   * Jumps to the beginning of the text field (does nothing
+   * if already at the beginning).
+   */
+  static jumpToBeginning(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(false /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.HOME, {ctrl: true});
+  }
+
+  /**
+   * Jumps to the end of the text field (does nothing if
+   * already at the end).
+   */
+  static jumpToEnd(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(false /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.END, {ctrl: true});
+  }
+
+  /**
+   * Moves the text caret one character back (does nothing
+   * if there are no more characters preceding the current
+   * location of the caret).
+   */
+  static moveBackwardOneChar(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(true /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.LEFT);
+  }
+
+  /**
+   * Moves the text caret one word backwards (does nothing
+   * if already at the beginning of the field). If the
+   * text caret is in the middle of a word, moves the caret
+   * to the beginning of that word.
+   */
+  static moveBackwardOneWord(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(false /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.LEFT, {ctrl: true});
+  }
+
+  /**
+   * Moves the text caret one line down (does nothing
+   * if there are no lines below the current location of
+   * the caret).
+   */
+  static moveDownOneLine(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(true /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.DOWN);
+  }
+
+  /**
+   * Moves the text caret one character forward (does nothing
+   * if there are no more characters following the current
+   * location of the caret).
+   */
+  static moveForwardOneChar(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(true /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.RIGHT);
+  }
+
+  /**
+   * Moves the text caret one word forward (does nothing if
+   * already at the end of the field). If the text caret is
+   * in the middle of a word, moves the caret to the end of
+   * that word.
+   */
+  static moveForwardOneWord(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(false /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.RIGHT, {ctrl: true});
+  }
+
+  /**
+   * Moves the text caret one line up (does nothing
+   * if there are no lines above the current location of
+   * the caret).
+   */
+  static moveUpOneLine(): void {
+    const manager = TextNavigationManager.instance;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(true /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.UP);
+  }
+
+  /**
+   * Reset the currentlySelecting variable to false, reset the selection
+   * indices, and remove the listener on navigation.
+   */
+  static resetCurrentlySelecting(): void {
+    const manager = TextNavigationManager.instance;
+    manager.currentlySelecting_ = false;
+    manager.manageNavigationListener_(false /** Removing listener */);
+    manager.selectionStartIndex_ = TextNavigationManager.NO_SELECT_INDEX;
+    manager.selectionEndIndex_ = TextNavigationManager.NO_SELECT_INDEX;
+    if (manager.currentlySelecting_) {
+      manager.setupDynamicSelection_(true /* resetCursor */);
+    }
+    EventGenerator.sendKeyPress(KeyCode.DOWN);
+  }
+
+  static get clipboardHasData(): boolean {
+    return TextNavigationManager.instance.clipboardHasData_;
+  }
+
+  static get selectionExists(): boolean {
+    return TextNavigationManager.instance.selectionExists_;
+  }
+
+  static set selectionExists(newVal: boolean) {
+    TextNavigationManager.instance.selectionExists_ = newVal;
+  }
+
+  getSelEndIndex(): number {
+    return this.selectionEndIndex_;
+  }
+
+  resetSelStartIndex(): void {
+    this.selectionStartIndex_ = TextNavigationManager.NO_SELECT_INDEX;
+  }
+
+  getSelStartIndex(): number {
+    return this.selectionStartIndex_;
+  }
+
+  setSelStartIndexAndNode(startIndex: number, textNode: AutomationNode): void {
+    this.selectionStartIndex_ = startIndex;
+    this.selectionStartObject_ = textNode;
+  }
+
+  /**
+   * Sets the selectionStart variable based on the selection of the current
+   * node. Also sets the currently selecting boolean to true.
+   */
+  static saveSelectStart(): void {
+    const manager = TextNavigationManager.instance;
+    chrome.automation.getFocus((focusedNode: AutomationNode|undefined) => {
+      manager.selectionStartObject_ = focusedNode;
+      manager.selectionStartIndex_ = manager.getSelectionIndexFromNode_(
+          // TODO(b/314203187): Not null asserted, check that this is correct.
+          manager.selectionStartObject_!,
+          true /* We are getting the start index.*/);
+      manager.currentlySelecting_ = true;
+    });
+  }
+
+  // =============== Instance Methods ==============
+
+  /**
+   * Returns either the selection start index or the selection end index of the
+   * node based on the getStart param.
+   * @return selection start if getStart is true otherwise selection
+   * end
+   */
+  private getSelectionIndexFromNode_(node: AutomationNode, getStart: boolean):
+      number {
+    let indexFromNode = TextNavigationManager.NO_SELECT_INDEX;
+    // TODO(b/314203187): Not null asserted, check that this is correct.
+    if (getStart) {
+      indexFromNode = node.textSelStart!;
+    } else {
+      indexFromNode = node.textSelEnd!;
+    }
+    if (indexFromNode === undefined) {
+      return TextNavigationManager.NO_SELECT_INDEX;
+    }
+    return indexFromNode;
+  }
+
+  /** Adds or removes the selection listener. */
+  private manageNavigationListener_(addListener: boolean): void {
+    if (!this.selectionStartObject_) {
+      return;
+    }
+
+    if (addListener) {
+      this.selectionListener_.setNodes(this.selectionStartObject_);
+      this.selectionListener_.start();
+    } else {
+      this.selectionListener_.stop();
+    }
+  }
+
+  /**
+   * Function to handle changes in the cursor position during selection.
+   * This function will remove the selection listener and set the end of the
+   * selection based on the new position.
+   */
+  private onNavChange_(): void {
+    this.manageNavigationListener_(false);
+    if (this.currentlySelecting_) {
+      TextNavigationManager.saveSelectEnd();
+    }
+  }
+
+  /**
+   * Sets the selectionEnd variable based on the selection of the current node.
+   */
+  static saveSelectEnd(): void {
+    const manager = TextNavigationManager.instance;
+    chrome.automation.getFocus(focusedNode => {
+      manager.selectionEndObject_ = focusedNode;
+      manager.selectionEndIndex_ = manager.getSelectionIndexFromNode_(
+          manager.selectionEndObject_,
+          false /*We are not getting the start index.*/);
+      manager.saveSelection_();
+    });
+  }
+
+  /** Sets the selection after verifying that the bounds are set. */
+  private saveSelection_(): void {
+    if (this.selectionStartIndex_ === TextNavigationManager.NO_SELECT_INDEX ||
+        this.selectionEndIndex_ === TextNavigationManager.NO_SELECT_INDEX) {
+      console.error(SwitchAccess.error(
+          ErrorType.INVALID_SELECTION_BOUNDS,
+          'Selection bounds are not set properly: ' +
+              this.selectionStartIndex_ + ' ' + this.selectionEndIndex_));
+    } else {
+      this.setSelection_();
+    }
+  }
+
+  /**
+   * Sets up the cursor position and selection listener for dynamic selection.
+   * If the needToResetCursor boolean is true, the function will move the cursor
+   * to the end point of the selection before adding the event listener. If not,
+   * it will simply add the listener.
+   */
+  private setupDynamicSelection_(needToResetCursor: boolean): void {
+    /**
+     * TODO(crbug.com/999400): Work on text selection dynamic highlight and
+     * text selection implementation.
+     */
+    if (needToResetCursor) {
+      if (TextNavigationManager.currentlySelecting() &&
+          this.selectionEndIndex_ !== TextNavigationManager.NO_SELECT_INDEX) {
+        // Move the cursor to the end of the existing selection.
+        this.setSelection_();
+      }
+    }
+    this.manageNavigationListener_(true /** Add the listener */);
+  }
+
+  /**
+   * Sets the selection. If start and end object are equal, uses
+   * AutomationNode.setSelection. Otherwise calls
+   * chrome.automation.setDocumentSelection.
+   */
+  private setSelection_(): void {
+    if (this.selectionStartObject_ === this.selectionEndObject_) {
+      // TODO(b/314203187): Not null asserted, check that this is correct.
+      this.selectionStartObject_!.setSelection(
+          this.selectionStartIndex_, this.selectionEndIndex_);
+    } else {
+      chrome.automation.setDocumentSelection({
+        anchorObject: this.selectionStartObject_!,
+        anchorOffset: this.selectionStartIndex_,
+        focusObject: this.selectionEndObject_!,
+        focusOffset: this.selectionEndIndex_,
+      });
+    }
+  }
+
+  /*
+   * TODO(rosalindag): Add functionality to catch when clipboardHasData_ needs
+   * to be set to false.
+   * Set the clipboardHasData variable to true and reload the menu.
+   */
+  private updateClipboardHasData_(): void {
+    this.clipboardHasData_ = true;
+    const node = Navigator.byItem.currentNode;
+    if (node.hasAction(MenuAction.PASTE)) {
+      ActionManager.refreshMenuForNode(node);
+    }
+  }
+}
+
+export namespace TextNavigationManager {
+  export const NO_SELECT_INDEX = -1;
+}
+
+TestImportManager.exportForTesting(TextNavigationManager);
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/text_navigation_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/text_navigation_manager_test.js
new file mode 100644
index 0000000..78e824e
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/mv3/text_navigation_manager_test.js
@@ -0,0 +1,431 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+GEN_INCLUDE(['switch_access_e2e_test_base.js']);
+
+/** Text fixture for the text navigation manager. */
+SwitchAccessTextNavigationManagerTest = class extends SwitchAccessE2ETest {
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    this.textNavigationManager = TextNavigationManager.instance;
+    this.navigationManager = Navigator.byItem;
+  }
+};
+
+
+/**
+ * Generates a website with a text area, finds the node for the text
+ * area, sets up the node to listen for a text navigation action, and then
+ * executes the specified text navigation action. Upon detecting the
+ * text navigation action, the node will verify that the action correctly
+ * changed the index of the text caret.
+ * @param {!SwitchAccessE2ETest} testFixture
+ * @param {{content: string,
+ *          initialIndex: number,
+ *          targetIndex: number,
+ *          navigationAction: function(),
+ *          id: (string || undefined),
+ *          cols: (number || undefined),
+ *          wrap: (string || undefined)}} textParams
+ */
+async function runTextNavigationTest(testFixture, textParams) {
+  // Required parameters.
+  const textContent = textParams.content;
+  const initialTextIndex = textParams.initialIndex;
+  const targetTextIndex = textParams.targetIndex;
+  const textNavigationAction = textParams.navigationAction;
+
+  // Default parameters.
+  const textId = textParams.id || 'test';
+  const textCols = textParams.cols || 20;
+  const textWrap = textParams.wrap || 'soft';
+
+  const website = generateWebsiteWithTextArea(
+      textId, textContent, initialTextIndex, textCols, textWrap);
+
+  await testFixture.runWithLoadedTree(website);
+  const inputNode = this.findNodeById(textId);
+  assertNotEquals(inputNode, null);
+
+  setUpCursorChangeListener(
+      testFixture, inputNode, initialTextIndex, targetTextIndex,
+      targetTextIndex);
+
+  textNavigationAction();
+}
+
+/**
+ * This function:
+ * - Generates a website with a text area
+ * - Executes setSelectStart finds the node for the text
+ * area
+ * - Sets up the node to listen for a text navigation action
+ * - executes the specified text navigation action. Upon detecting the
+ * - Verifies that the action correctly changed the index of the text caret
+ * - Sets up a second listener for a text selection action
+ * - Calls saveSelectEnd function from the event listener
+ * - Verifies that the selection was set correctly
+ * textParams should specify parameters
+ * for the test as follows:
+ *  -content: content of the text area.
+ *  -initialIndex: index of the text caret before the navigation action.
+ *  -targetStartIndex: start index of the selection after the selection action.
+ *  -targetEndIndex: end index of the selection after the navigation action.
+ *  -navigationAction: function executing a text navigation action or selection
+ * action. -id: id of the text area element (optional). -cols: number of columns
+ * in the text area (optional). -wrap: the wrap attribute ("hard" or "soft") of
+ * the text area (optional).
+ *
+ * @param {!SwitchAccessE2ETest} testFixture
+ * @param {selectionTextParams} textParams,
+ */
+async function runTextSelectionTest(testFixture, textParams) {
+  // Required parameters.
+  const textContent = textParams.content;
+  const initialTextIndex = textParams.initialIndex;
+  const targetTextStartIndex = textParams.targetStartIndex;
+  const targetTextEndIndex = textParams.targetEndIndex;
+  const textNavigationAction = textParams.navigationAction;
+
+  // Default parameters.
+  const selectionIsBackward = textParams.backward || false;
+  const textId = textParams.id || 'test';
+  const textCols = textParams.cols || 20;
+  const textWrap = textParams.wrap || 'soft';
+
+  const website = generateWebsiteWithTextArea(
+      textId, textContent, initialTextIndex, textCols, textWrap);
+
+  let navigationTargetIndex = targetTextEndIndex;
+  if (selectionIsBackward) {
+    navigationTargetIndex = targetTextStartIndex;
+  }
+
+  await testFixture.runWithLoadedTree(website);
+  const inputNode = this.findNodeById(textId);
+  assertNotEquals(inputNode, null);
+  checkNodeIsFocused(inputNode);
+  const callback = testFixture.newCallback(function() {
+    setUpCursorChangeListener(
+        testFixture, inputNode, targetTextEndIndex, targetTextStartIndex,
+        targetTextEndIndex);
+    testFixture.textNavigationManager.saveSelectEnd();
+  });
+
+  testFixture.textNavigationManager.saveSelectStart();
+
+  setUpCursorChangeListener(
+      testFixture, inputNode, initialTextIndex, navigationTargetIndex,
+      navigationTargetIndex, callback);
+
+  textNavigationAction();
+}
+
+/**
+ * Returns a website string with a text area with the given properties.
+ * @param {number} id The ID of the text area element.
+ * @param {string} contents The contents of the text area.
+ * @param {number} textIndex The index of the text caret within the text area.
+ * @param {number} cols The number of columns in the text area.
+ * @param {string} wrap The wrap attribute of the text area ('hard' or 'soft').
+ * @return {string}
+ */
+function generateWebsiteWithTextArea(id, contents, textIndex, cols, wrap) {
+  const website = `data:text/html;charset=utf-8,
+    <textarea id=${id} cols=${cols} wrap=${wrap}
+    autofocus="true">${contents}</textarea>
+    <script>
+      const input = document.getElementById("${id}");
+      input.selectionStart = ${textIndex};
+      input.selectionEnd = ${textIndex};
+      input.focus();
+    </script>`;
+  return website;
+}
+
+/**
+ * Check that the node in the JS file matches the node in the test.
+ * The nodes can be assumed to be the same if their roles match as there is only
+ * one text input node on the generated webpage.
+ * @param {!AutomationNode} inputNode
+ */
+function checkNodeIsFocused(inputNode) {
+  chrome.automation.getFocus(focusedNode => {
+    assertEquals(focusedNode.role, inputNode.role);
+  });
+}
+
+/**
+ * Sets up the input node (text field) to listen for text
+ * navigation and selection actions. When the index of the text selection
+ * changes from its initial position, checks that the text
+ * indices now matches the target text start and end index. Assumes that the
+ * initial and target text start/end indices are distinct (to detect a
+ * change from the text navigation action). Also assumes that
+ * the text navigation and selection actions directly changes the text caret
+ * to the correct index (with no intermediate movements).
+ * @param {!SwitchAccessE2ETest} testFixture
+ * @param {!AutomationNode} inputNode
+ * @param {number} initialTextIndex
+ * @param {number} targetTextStartIndex
+ * @param {number} targetTextEndIndex
+ * @param {function() || undefined} callback
+ */
+function setUpCursorChangeListener(
+    testFixture, inputNode, initialTextIndex, targetTextStartIndex,
+    targetTextEndIndex, callback) {
+  // Ensures that the text index has changed before checking the new index.
+  const checkActionFinished = function(tab) {
+    if (inputNode.textSelStart !== initialTextIndex ||
+        inputNode.textSelEnd !== initialTextIndex) {
+      checkTextIndex();
+      if (callback) {
+        callback();
+      }
+    }
+  };
+
+  // Test will not exit until this check is called.
+  const checkTextIndex = testFixture.newCallback(function() {
+    assertEquals(inputNode.textSelStart, targetTextStartIndex);
+    assertEquals(inputNode.textSelEnd, targetTextEndIndex);
+    // If there's a callback then this is the navigation listener for a
+    // selection test, thus remove it when fired to make way for the selection
+    // listener.
+    if (callback) {
+      inputNode.removeEventListener(
+          chrome.automation.EventType.TEXT_SELECTION_CHANGED,
+          checkActionFinished);
+    }
+  });
+
+  inputNode.addEventListener(
+      chrome.automation.EventType.TEXT_SELECTION_CHANGED, checkActionFinished);
+}
+
+// TODO(crbug.com/1268230): Re-enable test.
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_JumpToBeginning',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'hi there',
+        initialIndex: 6,
+        targetIndex: 0,
+        navigationAction: () => {
+          TextNavigationManager.jumpToBeginning();
+        },
+      });
+    });
+
+// TODO(crbug.com/1268230): Re-enable test.
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_JumpToEnd',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'hi there',
+        initialIndex: 3,
+        targetIndex: 8,
+        navigationAction: () => {
+          TextNavigationManager.jumpToEnd();
+        },
+      });
+    });
+
+// TODO(crbug.com/1177096) Renable test
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveBackwardOneChar',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'parrots!',
+        initialIndex: 7,
+        targetIndex: 6,
+        navigationAction: () => {
+          TextNavigationManager.moveBackwardOneChar();
+        },
+      });
+    });
+
+// TODO(crbug.com/1268230): Re-enable test.
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveBackwardOneWord',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'more parrots!',
+        initialIndex: 5,
+        targetIndex: 0,
+        navigationAction: () => {
+          TextNavigationManager.moveBackwardOneWord();
+        },
+      });
+    });
+
+// TODO(crbug.com/1268230): Re-enable test.
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveForwardOneChar',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'hello',
+        initialIndex: 0,
+        targetIndex: 1,
+        navigationAction: () => {
+          TextNavigationManager.moveForwardOneChar();
+        },
+      });
+    });
+
+// TODO(crbug.com/1268230): Re-enable test.
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveForwardOneWord',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'more parrots!',
+        initialIndex: 4,
+        targetIndex: 12,
+        navigationAction: () => {
+          TextNavigationManager.moveForwardOneWord();
+        },
+      });
+    });
+
+// TODO(crbug.com/1268230): Re-enable test.
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveUpOneLine',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'more parrots!',
+        initialIndex: 7,
+        targetIndex: 2,
+        cols: 8,
+        wrap: 'hard',
+        navigationAction: () => {
+          TextNavigationManager.moveUpOneLine();
+        },
+      });
+    });
+
+// TODO(crbug.com/1268230): Re-enable test.
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveDownOneLine',
+    async function() {
+      await runTextNavigationTest(this, {
+        content: 'more parrots!',
+        initialIndex: 3,
+        targetIndex: 8,
+        cols: 8,
+        wrap: 'hard',
+        navigationAction: () => {
+          TextNavigationManager.moveDownOneLine();
+        },
+      });
+    });
+
+
+/**
+ * Test the setSelectStart function by checking correct index is stored as the
+ * selection start index.
+ */
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectStart',
+    async function() {
+      const website =
+          generateWebsiteWithTextArea('test', 'test123', 3, 20, 'hard');
+
+      await this.runWithLoadedTree(website);
+      const inputNode = this.findNodeById('test');
+      assertNotEquals(inputNode, null);
+      checkNodeIsFocused(inputNode);
+
+      this.textNavigationManager.saveSelectStart();
+      const startIndex = this.textNavigationManager.selectionStartIndex_;
+      assertEquals(startIndex, 3);
+    });
+
+/**
+ * Test the setSelectEnd function by manually setting the selection start index
+ * and node then calling setSelectEnd and checking for the correct selection
+ * bounds
+ */
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectEnd',
+    async function() {
+      const website =
+          generateWebsiteWithTextArea('test', 'test 123', 6, 20, 'hard');
+
+      await this.runWithLoadedTree(website);
+      const inputNode = this.findNodeById('test');
+      assertNotEquals(inputNode, null);
+      checkNodeIsFocused(inputNode);
+
+
+      const startIndex = 3;
+      this.textNavigationManager.selectionStartIndex_ = startIndex;
+      this.textNavigationManager.selectionStartObject_ = inputNode;
+      this.textNavigationManager.saveSelectEnd();
+      const endIndex = inputNode.textSelEnd;
+      assertEquals(6, endIndex);
+    });
+
+/**
+ * Test use of setSelectStart and setSelectEnd with the moveForwardOneChar
+ * function.
+ */
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectCharacter',
+    async function() {
+      await runTextSelectionTest(this, {
+        content: 'hello world!',
+        initialIndex: 0,
+        targetStartIndex: 0,
+        targetEndIndex: 1,
+        cols: 8,
+        wrap: 'hard',
+        navigationAction: () => {
+          TextNavigationManager.moveForwardOneChar();
+        },
+      });
+    });
+
+/**
+ * Test use of setSelectStart and setSelectEnd with a backward selection using
+ * the moveBackwardOneWord function.
+ */
+AX_TEST_F(
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectWordBackward',
+    async function() {
+      await runTextSelectionTest(this, {
+        content: 'hello world!',
+        initialIndex: 5,
+        targetStartIndex: 0,
+        targetEndIndex: 5,
+        cols: 8,
+        wrap: 'hard',
+        navigationAction: () => {
+          TextNavigationManager.moveBackwardOneWord();
+        },
+        backward: true,
+      });
+    });
+
+/**
+ * selectionTextParams should specify parameters
+ * for the test as follows:
+ *  -content: content of the text area.
+ *  -initialIndex: index of the text caret before the navigation action.
+ *  -targetIndex: index of the text caret after the navigation action.
+ *  -navigationAction: function executing a text navigation action.
+ *  -id: id of the text area element (optional).
+ *  -cols: number of columns in the text area (optional).
+ *  -wrap: the wrap attribute ("hard" or "soft") of the text area (optional).
+ *@typedef {{content: string,
+ *          initialIndex: number,
+ *          targetStartIndex: number,
+ *          targetEndIndex: number,
+ *          textAction: function(),
+ *          backward: (boolean || undefined)
+ *          id: (string || undefined),
+ *          cols: (number || undefined),
+ *          wrap: (string || undefined),}}
+ */
+let selectionTextParams;
diff --git a/chrome/browser/resources/commerce/product_specifications/BUILD.gn b/chrome/browser/resources/commerce/product_specifications/BUILD.gn
index dd12ff6..a9990256 100644
--- a/chrome/browser/resources/commerce/product_specifications/BUILD.gn
+++ b/chrome/browser/resources/commerce/product_specifications/BUILD.gn
@@ -19,7 +19,6 @@
     "app.ts",
     "new_column_selector.ts",
     "product_selection_menu.ts",
-    "product_selector.ts",
     "table.ts",
   ]
 
@@ -34,6 +33,7 @@
     "header_menu.css",
     "horizontal_carousel.css",
     "loading_state.css",
+    "product_selector.css",
     "shared_vars.css",
   ]
 
@@ -67,6 +67,8 @@
     "horizontal_carousel.ts",
     "loading_state.html.ts",
     "loading_state.ts",
+    "product_selector.html.ts",
+    "product_selector.ts",
     "router.ts",
     "utils.ts",
     "window_proxy.ts",
diff --git a/chrome/browser/resources/commerce/product_specifications/app.html b/chrome/browser/resources/commerce/product_specifications/app.html
index be3d476..13d8173b 100644
--- a/chrome/browser/resources/commerce/product_specifications/app.html
+++ b/chrome/browser/resources/commerce/product_specifications/app.html
@@ -154,7 +154,7 @@
 <div id="appContainer">
   <product-specifications-header id="header" subtitle="[[setName_]]"
       menu-button-disabled$="[[loadingState_.loading]]"
-      on-delete-click="deleteSet_"
+      on-delete-click="onHeaderMenuDeleteClick_"
       on-name-change="updateSetName_"
       on-see-all-click="seeAllSets_">
   </product-specifications-header>
diff --git a/chrome/browser/resources/commerce/product_specifications/app.ts b/chrome/browser/resources/commerce/product_specifications/app.ts
index 02e2971..3edfbd7 100644
--- a/chrome/browser/resources/commerce/product_specifications/app.ts
+++ b/chrome/browser/resources/commerce/product_specifications/app.ts
@@ -618,6 +618,30 @@
     return aggregatedDatas;
   }
 
+  private async loadSet_(uuid: Uuid): Promise<boolean> {
+    const {set} =
+        await this.shoppingApi_.getProductSpecificationsSetByUuid(uuid);
+    if (set) {
+      const {disclosureShown} =
+          await this.productSpecificationsProxy_.maybeShowDisclosure(
+              /* urls= */[], /* name= */ '', uuid.value);
+      if (disclosureShown) {
+        this.updateEmptyState_(true);
+        this.id_ = null;
+        return false;
+      }
+      this.id_ = set.uuid;
+      document.title = set.name;
+      this.setName_ = set.name;
+      this.populateTable_(set.urls.map(url => (url.url)));
+      return true;
+    }
+
+    this.updateEmptyState_(true);
+    this.id_ = null;
+    return false;
+  }
+
   private deleteSet_(uuid: Uuid|null = this.id_) {
     if (this.isOffline_) {
       this.showOfflineToast_();
@@ -1017,28 +1041,8 @@
     this.deleteSet_(event.detail.uuid);
   }
 
-  private async loadSet_(uuid: Uuid): Promise<boolean> {
-    const {set} =
-        await this.shoppingApi_.getProductSpecificationsSetByUuid(uuid);
-    if (set) {
-      const {disclosureShown} =
-          await this.productSpecificationsProxy_.maybeShowDisclosure(
-              /* urls= */[], /* name= */ '', uuid.value);
-      if (disclosureShown) {
-        this.updateEmptyState_(true);
-        this.id_ = null;
-        return false;
-      }
-      this.id_ = set.uuid;
-      document.title = set.name;
-      this.setName_ = set.name;
-      this.populateTable_(set.urls.map(url => (url.url)));
-      return true;
-    }
-
-    this.updateEmptyState_(true);
-    this.id_ = null;
-    return false;
+  private onHeaderMenuDeleteClick_() {
+    this.deleteSet_();
   }
 }
 
diff --git a/chrome/browser/resources/commerce/product_specifications/comparison_table_list.css b/chrome/browser/resources/commerce/product_specifications/comparison_table_list.css
index 309e8781..820521a6 100644
--- a/chrome/browser/resources/commerce/product_specifications/comparison_table_list.css
+++ b/chrome/browser/resources/commerce/product_specifications/comparison_table_list.css
@@ -24,15 +24,66 @@
   width: var(--comparison-table-list-width);
 }
 
+:host:has(#footer:not([hidden])) {
+  padding-bottom: 0px;
+}
+
+cr-icon-button {
+  --cr-icon-button-icon-size: 16px;
+  --cr-icon-button-size: 20px;
+}
+
+cr-toolbar-selection-overlay {
+  background: transparent;
+  border: 0;
+}
+
+cr-toolbar-selection-overlay::part(clearIcon) {
+  --cr-icon-button-icon-size: 16px;
+  height: 20px;
+  margin-inline-end: 4px;
+  margin-inline-start: -12px;
+  width: 20px;
+}
+
+#edit {
+  float: inline-end;
+  margin-inline-end: var(--container-padding);
+}
+
+#footer {
+  align-items: center;
+  display: flex;
+  font-size: 12px;
+  font-weight: 500;
+  height: 48px;
+  line-height: 16px;
+  padding: var(--container-padding);
+  margin-inline-end: var(--container-padding);
+  position: sticky;
+}
+
+#footer[hidden] {
+  display: none;
+}
+
+#header {
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 20px;
+  padding: var(--container-padding);
+}
+
 #listContainer {
   max-height: 100%;
   overflow: scroll;
+  padding-top: 4px;
   scrollbar-gutter: stable;
 }
 
 #listContainer::-webkit-scrollbar {
   background: transparent;
-  width: var(--scrollbar-width);
+  width: var(--container-padding);
   height: 0;
 }
 
@@ -40,11 +91,13 @@
   background:
     var(--color-product-specifications-horizontal-carousel-scrollbar-thumb);
   border-radius: 8px;
+  width: var(--scrollbar-width);
 }
 
-#header {
-  font-size: 13px;
-  font-weight: 500;
-  line-height: 20px;
-  padding: 8px;
+#numSelected {
+  margin-inline-start: 4px;
+}
+
+.sp-icon-buttons-row {
+  margin-inline-end: -4px;
 }
diff --git a/chrome/browser/resources/commerce/product_specifications/comparison_table_list.html.ts b/chrome/browser/resources/commerce/product_specifications/comparison_table_list.html.ts
index 80bc1594..88f6d6f 100644
--- a/chrome/browser/resources/commerce/product_specifications/comparison_table_list.html.ts
+++ b/chrome/browser/resources/commerce/product_specifications/comparison_table_list.html.ts
@@ -10,7 +10,13 @@
 
 export function getHtml(this: ComparisonTableListElement) {
   return html`<!--_html_template_start_-->
-  <div id="header">$i18n{yourComparisonTables}</div>
+  <div id="header">
+    $i18n{yourComparisonTables}
+    <cr-icon-button id="edit" iron-icon="product-specifications:edit"
+        @click="${this.onEditClick_}">
+    </cr-icon-button>
+  </div>
+
   <div id="listContainer">
     <cr-lazy-list id="list" .scrollTarget="${this}" .items="${this.tables}">
       ${this.tables.map(table => html`
@@ -18,9 +24,28 @@
           name="${table.name}"
           .uuid="${table.uuid}"
           num-urls="${table.numUrls}"
-          .imageUrl="${table.imageUrl}">
+          .imageUrl="${table.imageUrl}"
+          ?has-checkbox="${this.isEditing_}"
+          @checkbox-change="${this.onCheckboxChange_}"
+          @delete-table="${this.stopEditing_}">
         </comparison-table-list-item>`)}
     </cr-lazy-list>
   </div>
+
+  <div id="footer" ?hidden="${!this.isEditing_}">
+    <cr-toolbar-selection-overlay id="toolbar"
+        cancel-label="$i18n{cancelA11yLabel}"
+        selection-label="${this.getSelectionLabel_(this.numSelected_)}"
+        @clear-selected-items="${this.onClearClick_}"
+        ?show="${this.isEditing_}">
+      <div class="sp-icon-buttons-row">
+        <cr-icon-button id="delete" iron-icon="cr:delete"
+            title="$i18n{menuDelete}" aria-label="$i18n{menuDelete}"
+            ?disabled="${this.deletePending_ || this.numSelected_ === 0}"
+            @click="${this.onDeleteClick_}">
+        </cr-icon-button>
+      </div>
+    </cr-toolbar-selection-overlay>
+  </div>
   <!--_html_template_end_-->`;
 }
diff --git a/chrome/browser/resources/commerce/product_specifications/comparison_table_list.ts b/chrome/browser/resources/commerce/product_specifications/comparison_table_list.ts
index c90d650..39af181b 100644
--- a/chrome/browser/resources/commerce/product_specifications/comparison_table_list.ts
+++ b/chrome/browser/resources/commerce/product_specifications/comparison_table_list.ts
@@ -3,13 +3,22 @@
 // found in the LICENSE file.
 
 import 'chrome://resources/cr_elements/cr_lazy_list/cr_lazy_list.js';
+import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
+import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_selection_overlay.js';
+import './images/icons.html.js';
 
+import type {ShoppingServiceBrowserProxy} from '//resources/cr_components/commerce/shopping_service_browser_proxy.js';
+import {ShoppingServiceBrowserProxyImpl} from '//resources/cr_components/commerce/shopping_service_browser_proxy.js';
+import {loadTimeData} from '//resources/js/load_time_data.js';
 import type {Uuid} from '//resources/mojo/mojo/public/mojom/base/uuid.mojom-webui.js';
 import type {Url} from '//resources/mojo/url/mojom/url.mojom-webui.js';
+import type {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
+import type {CrToolbarSelectionOverlayElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_selection_overlay.js';
 import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
 
 import {getCss} from './comparison_table_list.css.js';
 import {getHtml} from './comparison_table_list.html.js';
+import type {ComparisonTableListItemCheckboxChangeEvent} from './comparison_table_list_item.js';
 
 export interface ComparisonTableDetails {
   name: string;
@@ -18,6 +27,14 @@
   imageUrl: Url|null;
 }
 
+export interface ComparisonTableListElement {
+  $: {
+    delete: CrIconButtonElement,
+    edit: CrIconButtonElement,
+    toolbar: CrToolbarSelectionOverlayElement,
+  };
+}
+
 export class ComparisonTableListElement extends CrLitElement {
   static get is() {
     return 'comparison-table-list';
@@ -34,10 +51,69 @@
   static override get properties() {
     return {
       tables: {type: Array},
+      isEditing_: {type: Boolean},
+      numSelected_: {type: Number},
+      deletePending_: {type: Boolean},
     };
   }
 
   tables: ComparisonTableDetails[] = [];
+
+  protected isEditing_: boolean = false;
+  protected numSelected_: number = 0;
+  protected deletePending_: boolean = false;
+  private selectedUuids_: Set<Uuid> = new Set();
+  private shoppingApi_: ShoppingServiceBrowserProxy =
+      ShoppingServiceBrowserProxyImpl.getInstance();
+
+  protected getSelectionLabel_(numSelected: number): string {
+    return loadTimeData.getStringF('numSelected', numSelected);
+  }
+
+  protected onEditClick_() {
+    if (!this.isEditing_) {
+      this.isEditing_ = true;
+      return;
+    }
+
+    this.stopEditing_();
+  }
+
+  protected onClearClick_() {
+    this.stopEditing_();
+  }
+
+  protected async onDeleteClick_() {
+    // Disable the delete button while sets are being deleted.
+    this.deletePending_ = true;
+    const promises: void[] = [];
+    this.selectedUuids_.forEach(uuid => {
+      promises.push(this.shoppingApi_.deleteProductSpecificationsSet(uuid));
+    });
+    this.deletePending_ = false;
+
+    await Promise.all(promises);
+    this.stopEditing_();
+
+    this.fire('delete-finished-for-testing');
+  }
+
+  protected onCheckboxChange_(event:
+                                  ComparisonTableListItemCheckboxChangeEvent) {
+    if (event.detail.checked) {
+      this.selectedUuids_.add(event.detail.uuid);
+    } else {
+      this.selectedUuids_.delete(event.detail.uuid);
+    }
+
+    this.numSelected_ = this.selectedUuids_.size;
+  }
+
+  protected stopEditing_() {
+    this.isEditing_ = false;
+    this.selectedUuids_.clear();
+    this.numSelected_ = 0;
+  }
 }
 
 declare global {
diff --git a/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.css b/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.css
index 01b42ae..7827e469 100644
--- a/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.css
+++ b/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.css
@@ -11,6 +11,13 @@
   --cr-url-list-item-padding: 12px;
 }
 
+/* Visually hide the label but allow the screen reader to pick it up. */
+cr-checkbox::part(label-container) {
+  clip: rect(0,0,0,0);
+  display: block;
+  position: fixed;
+}
+
 cr-input {
   --cr-input-background-color: transparent;
   --cr-input-error-display: none;
diff --git a/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.html.ts b/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.html.ts
index 12126b5..49c0ec9 100644
--- a/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.html.ts
+++ b/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.html.ts
@@ -22,16 +22,25 @@
         @contextmenu="${this.onContextMenu_}"
         always-show-suffix>
 
-      ${this.isRenaming_ ? html`
-        <cr-input slot="content" id="renameInput" value="${this.name}"
-            class="stroked" @blur="${this.onRenameInputBlur_}"
-            @keydown="${this.onRenameInputKeyDown_}">
-        </cr-input>`
-      : html`
-        <div id="numItems" slot="badges">${this.numItemsString_}</div>
-        <cr-icon-button id="trailingIconButton" slot="suffix"
-            iron-icon="cr:more-vert" @click="${this.onShowContextMenuClick_}">
-        </cr-icon-button>`}
+      ${this.hasCheckbox ? html`
+        <cr-checkbox id="checkbox" slot="prefix"
+            @checked-changed="${this.onCheckboxChange_}">
+        </cr-checkbox>` : ''}
+
+      ${this.isRenaming_ ?
+        html`
+          <cr-input slot="content" id="renameInput" value="${this.name}"
+              class="stroked" @blur="${this.onRenameInputBlur_}"
+              @keydown="${this.onRenameInputKeyDown_}">
+          </cr-input>`
+        : html`
+          <div id="numItems" slot="badges">${this.numItemsString_}</div>
+          <!-- Hide the trailing icon button if the item has a checkbox. -->
+          ${!this.hasCheckbox ? html`
+            <cr-icon-button id="trailingIconButton" slot="suffix"
+                iron-icon="cr:more-vert"
+                @click="${this.onShowContextMenuClick_}">
+            </cr-icon-button>` : ''}`}
     </cr-url-list-item>
   </div>
 
diff --git a/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.ts b/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.ts
index 5bb1065..382d5c3a 100644
--- a/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.ts
+++ b/chrome/browser/resources/commerce/product_specifications/comparison_table_list_item.ts
@@ -9,6 +9,7 @@
 import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render_lit.js';
 import 'chrome://resources/cr_elements/icons.html.js';
 import 'chrome://resources/cr_elements/cr_input/cr_input.js';
+import 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
 
 import type {CrActionMenuElement} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
 import type {CrIconButtonElement} from '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
@@ -20,6 +21,7 @@
 import type {Uuid} from '//resources/mojo/mojo/public/mojom/base/uuid.mojom-webui.js';
 import type {ProductSpecificationsBrowserProxy} from 'chrome://resources/cr_components/commerce/product_specifications_browser_proxy.js';
 import {ProductSpecificationsBrowserProxyImpl} from 'chrome://resources/cr_components/commerce/product_specifications_browser_proxy.js';
+import type {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
 import type {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
@@ -36,6 +38,10 @@
   name: string,
 }>;
 export type ComparisonTableListItemDeleteEvent = CustomEvent<{uuid: Uuid}>;
+export type ComparisonTableListItemCheckboxChangeEvent = CustomEvent<{
+  uuid: Uuid,
+  checked: boolean,
+}>;
 
 export interface ComparisonTableListItemElement {
   $: {
@@ -64,6 +70,7 @@
       uuid: {type: Object},
       numUrls: {type: Number},
       imageUrl: {type: Object},
+      hasCheckbox: {type: Boolean},
       tableUrl_: {type: Object},
       numItemsString_: {type: String},
       isMenuOpen_: {type: Boolean},
@@ -75,6 +82,7 @@
   uuid: Uuid = {value: ''};
   numUrls: number = 0;
   imageUrl: Url|null = null;
+  hasCheckbox: boolean = false;
 
   protected tableUrl_: Url = {url: ''};
   protected numItemsString_: string = '';
@@ -117,7 +125,16 @@
         await this.pluralStringProxy_.getPluralString('numItems', this.numUrls);
   }
 
-  protected onClick_() {
+  protected onClick_(event: MouseEvent) {
+    // Treat the item click as a checkbox click if it has checkbox.
+    if (this.hasCheckbox) {
+      event.preventDefault();
+      event.stopPropagation();
+
+      this.checkbox_.checked = !this.checkbox_.checked;
+      return;
+    }
+
     if (!this.isRenaming_) {
       this.fire('item-click', {
         uuid: this.uuid,
@@ -184,6 +201,16 @@
     }
   }
 
+  protected onCheckboxChange_(event: Event) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    this.fire('checkbox-change', {
+      uuid: this.uuid,
+      checked: (event.target as CrCheckboxElement).checked,
+    });
+  }
+
   private get trailingIconButton_() {
     const trailingIconButton =
         $$<CrIconButtonElement>(this, '#trailingIconButton');
@@ -196,6 +223,12 @@
     assert(input);
     return input;
   }
+
+  private get checkbox_(): CrCheckboxElement {
+    const checkbox = $$<CrCheckboxElement>(this, '#checkbox');
+    assert(checkbox);
+    return checkbox;
+  }
 }
 
 declare global {
diff --git a/chrome/browser/resources/commerce/product_specifications/product_selector.css b/chrome/browser/resources/commerce/product_specifications/product_selector.css
new file mode 100644
index 0000000..e961c7c
--- /dev/null
+++ b/chrome/browser/resources/commerce/product_specifications/product_selector.css
@@ -0,0 +1,77 @@
+/* Copyright 2025 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
+ * #scheme=relative
+ * #css_wrapper_metadata_end */
+
+:host {
+  --cr-url-list-item-padding: 0;
+  --iron-icon-gap: 8px;
+  --iron-icon-width: 16px;
+  --product-selector-border-radius: 10px;
+  --product-selector-height: 42px;
+  --product-selector-empty-state-padding: 6px;
+  --product-selector-padding: 8px;
+  --product-selector-width: 220px;
+  text-align: start;
+}
+
+#currentProduct {
+  --cr-url-list-item-icon-margin-end: 8px;
+  --cr-url-list-item-container-width: 16px;
+  --cr-url-list-item-container-height: 16px;
+  --cr-url-list-item-metadata-gap: 0;
+  --color-list-item-url-favicon-background: transparent;
+  pointer-events: none;
+  width: calc(var(--product-selector-width)
+      - 2 * var(--product-selector-padding)
+      - var(--iron-icon-width)
+      - var(--iron-icon-gap));
+}
+
+#currentProductContainer {
+  box-sizing: border-box;
+  overflow: clip;
+  width: var(--product-selector-width);
+}
+
+#currentProductContainer {
+  align-items: center;
+  background: var(--color-product-specifications-button-background);
+  border-radius: var(--product-selector-border-radius);
+  display: flex;
+  gap: var(--iron-icon-gap);
+  height: var(--product-selector-height);
+  padding: var(--product-selector-padding);
+  position: relative;
+}
+
+#emptyState {
+  flex: 1;
+  font-weight: 500;
+  padding-inline-start: var(--product-selector-empty-state-padding);
+}
+
+cr-icon {
+  right: 4px;
+}
+
+#hoverLayer {
+  display: none;
+}
+
+#currentProductContainer:active > #hoverLayer,
+#currentProductContainer:focus-within > #hoverLayer,
+#currentProductContainer:hover > #hoverLayer,
+#currentProductContainer.showing-menu > #hoverLayer {
+  background-color: var(--cr-hover-background-color);
+  border-radius: var(--product-selector-border-radius);
+  display: block;
+  height: var(--product-selector-height);
+  position: absolute;
+  transform: translateX(calc(-1 * var(--product-selector-padding)));
+  width: var(--product-selector-width);
+}
diff --git a/chrome/browser/resources/commerce/product_specifications/product_selector.html b/chrome/browser/resources/commerce/product_specifications/product_selector.html
deleted file mode 100644
index 466495a2..0000000
--- a/chrome/browser/resources/commerce/product_specifications/product_selector.html
+++ /dev/null
@@ -1,93 +0,0 @@
-<style include="cr-shared-style">
-  :host {
-    --cr-url-list-item-padding: 0;
-    --iron-icon-gap: 8px;
-    --iron-icon-width: 16px;
-    --product-selector-border-radius: 10px;
-    --product-selector-height: 42px;
-    --product-selector-empty-state-padding: 6px;
-    --product-selector-padding: 8px;
-    --product-selector-width: 220px;
-    text-align: start;
-  }
-
-  #currentProduct {
-    --cr-url-list-item-icon-margin-end: 8px;
-    --cr-url-list-item-container-width: 16px;
-    --cr-url-list-item-container-height: 16px;
-    --cr-url-list-item-metadata-gap: 0;
-    --color-list-item-url-favicon-background: transparent;
-    pointer-events: none;
-    width: calc(var(--product-selector-width)
-        - 2 * var(--product-selector-padding)
-        - var(--iron-icon-width)
-        - var(--iron-icon-gap));
-  }
-
-  #currentProductContainer {
-    box-sizing: border-box;
-    overflow: clip;
-    width: var(--product-selector-width);
-  }
-
-  #currentProductContainer {
-    align-items: center;
-    background: var(--color-product-specifications-button-background);
-    border-radius: var(--product-selector-border-radius);
-    display: flex;
-    gap: var(--iron-icon-gap);
-    height: var(--product-selector-height);
-    padding: var(--product-selector-padding);
-    position: relative;
-  }
-
-  #emptyState {
-    flex: 1;
-    font-weight: 500;
-    padding-inline-start: var(--product-selector-empty-state-padding);
-  }
-
-  cr-icon {
-    right: 4px;
-  }
-
-  #hoverLayer {
-    display: none;
-  }
-
-  #currentProductContainer:active > #hoverLayer,
-  #currentProductContainer:focus-within > #hoverLayer,
-  #currentProductContainer:hover > #hoverLayer,
-  #currentProductContainer.showing-menu > #hoverLayer {
-    background-color: var(--cr-hover-background-color);
-    border-radius: var(--product-selector-border-radius);
-    display: block;
-    height: var(--product-selector-height);
-    position: absolute;
-    transform: translateX(calc(-1 * var(--product-selector-padding)));
-    width: var(--product-selector-width);
-  }
-</style>
-
-<div id="currentProductContainer"
-    on-click="showMenu_"
-    on-keydown="onCurrentProductContainerKeyDown_"
-    tabindex="0">
-  <template is="dom-if" if="[[selectedItem]]" restamp>
-    <cr-url-list-item id="currentProduct" size="medium"
-      url="[[selectedItem.url]]" title="[[selectedItem.title]]"
-      description="[[getUrl_(selectedItem)]]" tabindex="-1">
-    </cr-url-list-item>
-  </template>
-  <template is="dom-if" if="[[!selectedItem]]" restamp>
-    <div id="emptyState">$i18n{emptyProductSelector}</div>
-  </template>
-  <cr-icon icon="cr:expand-more"></cr-icon>
-  <div id="hoverLayer"></div>
-</div>
-
-<product-selection-menu id="productSelectionMenu"
-    selected-url="[[getSelectedUrl_(selectedItem)]]"
-    excluded-urls="[[excludedUrls]]"
-    on-close-menu="onCloseMenu_">
-</product-selection-menu>
diff --git a/chrome/browser/resources/commerce/product_specifications/product_selector.html.ts b/chrome/browser/resources/commerce/product_specifications/product_selector.html.ts
new file mode 100644
index 0000000..adef609
--- /dev/null
+++ b/chrome/browser/resources/commerce/product_specifications/product_selector.html.ts
@@ -0,0 +1,36 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {html} from '//resources/lit/v3_0/lit.rollup.js';
+
+import type {ProductSelectorElement} from './product_selector.js';
+
+// clang-format off
+export function getHtml(this: ProductSelectorElement) {
+  return html`<!--_html_template_start_-->
+  <div id="currentProductContainer"
+      @click="${this.showMenu_}"
+      @keydown="${this.onCurrentProductContainerKeyDown_}"
+      tabindex="0">
+    ${this.selectedItem ? html`
+        <cr-url-list-item id="currentProduct" size="medium"
+          url="${this.selectedItem?.url || ''}"
+          title="${this.selectedItem?.title || ''}"
+          description="${this.getUrl_(this.selectedItem)}" tabindex="-1">
+        </cr-url-list-item>
+    ` : html`
+        <div id="emptyState">$i18n{emptyProductSelector}</div>
+    `}
+    <cr-icon icon="cr:expand-more"></cr-icon>
+    <div id="hoverLayer"></div>
+  </div>
+
+  <product-selection-menu id="productSelectionMenu"
+      .selectedUrl="${this.getSelectedUrl_()}"
+      .excludedUrls="${this.excludedUrls}"
+      @close-menu="${this.onCloseMenu_}">
+  </product-selection-menu>
+  <!--_html_template_end_-->`;
+}
+// clang-format on
diff --git a/chrome/browser/resources/commerce/product_specifications/product_selector.ts b/chrome/browser/resources/commerce/product_specifications/product_selector.ts
index e8721c2..bea6083 100644
--- a/chrome/browser/resources/commerce/product_specifications/product_selector.ts
+++ b/chrome/browser/resources/commerce/product_specifications/product_selector.ts
@@ -8,10 +8,11 @@
 import './product_selection_menu.js';
 
 import type {CrUrlListItemElement} from 'chrome://resources/cr_elements/cr_url_list_item/cr_url_list_item.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 {ProductSelectionMenuElement} from './product_selection_menu.js';
-import {getTemplate} from './product_selector.html.js';
+import {getCss} from './product_selector.css.js';
+import {getHtml} from './product_selector.html.js';
 import {getAbbreviatedUrl} from './utils.js';
 import type {UrlListEntry} from './utils.js';
 
@@ -23,42 +24,44 @@
   };
 }
 
-export class ProductSelectorElement extends PolymerElement {
+export class ProductSelectorElement extends CrLitElement {
   static get is() {
     return 'product-selector';
   }
 
-  static get template() {
-    return getTemplate();
+  static override get styles() {
+    return getCss();
   }
 
-  static get properties() {
+  static override get properties() {
     return {
       selectedItem: {
         type: Object,
-        value: null,
       },
 
       excludedUrls: {
         type: Array,
-        value: () => [],
       },
     };
   }
 
-  selectedItem: UrlListEntry|null;
-  excludedUrls: string[];
+  selectedItem: UrlListEntry|null = null;
+  excludedUrls: string[] = [];
 
-  private showMenu_() {
+  override render() {
+    return getHtml.bind(this)();
+  }
+
+  protected showMenu_() {
     this.$.productSelectionMenu.showAt(this.$.currentProductContainer);
     this.$.currentProductContainer.classList.add('showing-menu');
   }
 
-  private onCloseMenu_() {
+  protected onCloseMenu_() {
     this.$.currentProductContainer.classList.remove('showing-menu');
   }
 
-  private onCurrentProductContainerKeyDown_(e: KeyboardEvent) {
+  protected onCurrentProductContainerKeyDown_(e: KeyboardEvent) {
     if (e.key === ' ' || e.key === 'Enter') {
       e.preventDefault();
       e.stopPropagation();
@@ -66,11 +69,11 @@
     }
   }
 
-  private getUrl_(item: UrlListEntry) {
+  protected getUrl_(item: UrlListEntry) {
     return getAbbreviatedUrl(item.url);
   }
 
-  private getSelectedUrl_() {
+  protected getSelectedUrl_() {
     return this.selectedItem?.url ?? '';
   }
 }
diff --git a/chrome/browser/resources/glic/BUILD.gn b/chrome/browser/resources/glic/BUILD.gn
index 7eaeb09..5b7d23e9 100644
--- a/chrome/browser/resources/glic/BUILD.gn
+++ b/chrome/browser/resources/glic/BUILD.gn
@@ -35,10 +35,8 @@
   extra_grdp_deps = [ ":glic_api_injection_grdp" ]
   extra_grdp_files = [ "$target_gen_dir/glic_api_injection.grdp" ]
 
-  mojo_files_deps =
-      [ "//chrome/browser/ui/webui/glic:mojo_bindings_ts__generator" ]
-  mojo_files =
-      [ "$root_gen_dir/chrome/browser/ui/webui/glic/glic.mojom-webui.ts" ]
+  mojo_files_deps = [ "//chrome/browser/glic:mojo_bindings_ts__generator" ]
+  mojo_files = [ "$root_gen_dir/chrome/browser/glic/glic.mojom-webui.ts" ]
 
   ts_definitions = [
     # These files are necessary for chrome.webviewTag:
diff --git a/chrome/browser/resources/lens/overlay/lens_overlay_app.ts b/chrome/browser/resources/lens/overlay/lens_overlay_app.ts
index 9e7282f..8938ea2 100644
--- a/chrome/browser/resources/lens/overlay/lens_overlay_app.ts
+++ b/chrome/browser/resources/lens/overlay/lens_overlay_app.ts
@@ -142,8 +142,12 @@
       },
       showGhostLoader: {
         type: Boolean,
-        computed: `computeShowGhostLoader(isSearchboxFocused,
-            suppressGhostLoader)`,
+        computed:
+            `computeShowGhostLoader(
+                isSearchboxFocused,
+                autocompleteRequestStarted,
+                showErrorState,
+                suppressGhostLoader)`,
         reflectToAttribute: true,
       },
       showErrorState: {
@@ -164,6 +168,8 @@
   suppressGhostLoader: boolean;
   // Whether the ghost loader should show its error state.
   showErrorState: boolean;
+  // Whether this is an in flight request to autocomplete.
+  private autocompleteRequestStarted: boolean = false;
   // Whether the translate button is enabled.
   private isTranslateButtonEnabled: boolean;
   // Whether the image has finished rendering.
@@ -278,6 +284,9 @@
       }
     });
     this.eventTracker_.add(
+        document, 'query-autocomplete',
+        this.handleQueryAutocomplete.bind(this));
+    this.eventTracker_.add(
         document, 'pointermove', this.updateCursorPosition.bind(this));
     this.eventTracker_.add(this.$.searchbox, 'mousedown', () => {
       this.suppressGhostLoader = false;
@@ -339,6 +348,11 @@
     this.searchboxBoundingClientRectObserver.observe(this.$.selectionOverlay);
   }
 
+  // Called when the searchbox requests autocomplete suggestions.
+  private handleQueryAutocomplete() {
+    this.autocompleteRequestStarted = true;
+  }
+
   private focusShimmerOnSearchbox() {
     const suggestionsContainer = this.$.searchbox.getSuggestionsElement();
     const areSuggestionsShowing =
@@ -375,6 +389,7 @@
 
   private handleSearchboxBlurred() {
     this.isSearchboxFocused = false;
+    this.autocompleteRequestStarted = false;
     this.showErrorState = false;
     this.$.translateButtonContainer.classList.add('searchbox-unfocused');
 
@@ -432,7 +447,13 @@
   }
 
   private computeShowGhostLoader(): boolean {
-    return this.isSearchboxFocused && !this.suppressGhostLoader;
+    if (this.suppressGhostLoader) {
+      return false;
+    }
+    // Show the ghost loader if there is focus on the searchbox, and there is
+    // autcomplete is loading or if autocomplete failed.
+    return this.isSearchboxFocused &&
+        (this.autocompleteRequestStarted || this.showErrorState);
   }
 
   private suppressGhostLoader_() {
diff --git a/chrome/browser/resources/lens/overlay/side_panel/side_panel_app.ts b/chrome/browser/resources/lens/overlay/side_panel/side_panel_app.ts
index 939b5836..e8110af 100644
--- a/chrome/browser/resources/lens/overlay/side_panel/side_panel_app.ts
+++ b/chrome/browser/resources/lens/overlay/side_panel/side_panel_app.ts
@@ -99,8 +99,13 @@
       },
       showGhostLoader: {
         type: Boolean,
-        computed: `computeShowGhostLoader(isSearchboxFocused,
-              suppressGhostLoader, isContextualSearchbox)`,
+        computed:
+            `computeShowGhostLoader(
+                isSearchboxFocused,
+                autocompleteRequestStarted,
+                showErrorState,
+                suppressGhostLoader,
+                isContextualSearchbox)`,
         reflectToAttribute: true,
       },
       showErrorState: {
@@ -128,6 +133,8 @@
   suppressGhostLoader: boolean;
   // Whether the ghost loader should show its error state.
   showErrorState: boolean;
+  // Whether this is an in flight request to autocomplete.
+  private autocompleteRequestStarted: boolean = false;
   private isErrorPageVisible: boolean;
   // Whether the results iframe is currently loading. This needs to be done via
   // browser because the iframe is cross-origin. Default true since the side
@@ -197,6 +204,9 @@
         onSearchboxKeydown(this, this.$.searchbox);
       }
     });
+    this.eventTracker_.add(
+        document, 'query-autocomplete',
+        this.handleQueryAutocomplete.bind(this));
   }
 
   override disconnectedCallback() {
@@ -247,6 +257,11 @@
     this.wasBackArrowAvailable = visible;
   }
 
+  // Called when the searchbox requests autocomplete suggestions.
+  private handleQueryAutocomplete() {
+    this.autocompleteRequestStarted = true;
+  }
+
   private setShowErrorPage(shouldShowErrorPage: boolean) {
     this.isErrorPageVisible =
         shouldShowErrorPage && loadTimeData.getBoolean('enableErrorPage');
@@ -264,12 +279,18 @@
   private onSearchboxFocusOut_() {
     this.isBackArrowVisible = this.wasBackArrowAvailable;
     this.isSearchboxFocused = false;
+    this.autocompleteRequestStarted = false;
     this.showErrorState = false;
   }
 
   private computeShowGhostLoader(): boolean {
-    return this.isSearchboxFocused && !this.suppressGhostLoader &&
-        this.isContextualSearchbox;
+    if (!this.isContextualSearchbox || this.suppressGhostLoader) {
+      return false;
+    }
+    // Show the ghost loader if there is focus on the searchbox, and there is
+    // autcomplete is loading or if autocomplete failed.
+    return this.isSearchboxFocused &&
+        (this.autocompleteRequestStarted || this.showErrorState);
   }
 
   private computePlaceholderText(): string {
@@ -293,6 +314,7 @@
     this.isContextualSearchbox = true;
     this.suppressGhostLoader = false;
     this.isSearchboxFocused = true;
+    this.autocompleteRequestStarted = true;
   }
 }
 
diff --git a/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.html.ts b/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.html.ts
index b0c32d6..311ceb8 100644
--- a/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.html.ts
+++ b/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.html.ts
@@ -44,7 +44,8 @@
         <a class="auto-tab-groups-link"
             role="link"
             tabindex="0"
-            @click="${this.onLearnMoreClick_}">
+            @click="${this.onLearnMoreClick_}"
+            @keydown="${this.onLearnMoreKeyDown_}">
           $i18n{learnMore}
         </a>
       ` : ''}
diff --git a/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.ts b/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.ts
index aa03f9f..311c4bc 100644
--- a/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.ts
+++ b/chrome/browser/resources/tab_search/auto_tab_groups/auto_tab_groups_not_started.ts
@@ -122,6 +122,12 @@
     this.fire('learn-more-click');
   }
 
+  protected onLearnMoreKeyDown_(event: KeyboardEvent) {
+    if (event.key === 'Enter') {
+      this.onLearnMoreClick_();
+    }
+  }
+
   protected onModelStrategyChange_(
       e: CustomEvent<{value: TabOrganizationModelStrategy}>) {
     const modelStrategy = e.detail.value;
diff --git a/chrome/browser/resources/tts_engine/BUILD.gn b/chrome/browser/resources/tts_engine/BUILD.gn
index 92819c7..12e5314 100644
--- a/chrome/browser/resources/tts_engine/BUILD.gn
+++ b/chrome/browser/resources/tts_engine/BUILD.gn
@@ -6,8 +6,6 @@
 
 assert(is_win || is_mac || is_linux)
 
-out_dir = "$root_out_dir/resources/tts_engine"
-
 generate_grd("build_bindings_grdp") {
   grd_prefix = "tts_engine"
   resource_path_prefix = "tts_engine"
@@ -20,7 +18,6 @@
   grd_prefix = "tts_engine"
   grd_resource_path_prefix = "tts_engine"
   non_web_component_files = [ "background.ts" ]
-  ts_out_dir = "$out_dir"
   extra_grdp_files = [ "$target_gen_dir/bindings_resources.grdp" ]
   extra_grdp_deps = [ ":build_bindings_grdp" ]
 }
diff --git a/chrome/browser/status_icons/status_icon.cc b/chrome/browser/status_icons/status_icon.cc
index e4a1332e..59e1904 100644
--- a/chrome/browser/status_icons/status_icon.cc
+++ b/chrome/browser/status_icons/status_icon.cc
@@ -48,6 +48,8 @@
 #if BUILDFLAG(IS_MAC)
 void StatusIcon::SetOpenMenuWithSecondaryClick(
     bool open_menu_with_secondary_click) {}
+
+void StatusIcon::SetImageTemplate(bool is_template) {}
 #endif
 
 void StatusIcon::SetContextMenu(std::unique_ptr<StatusIconMenuModel> menu) {
diff --git a/chrome/browser/status_icons/status_icon.h b/chrome/browser/status_icons/status_icon.h
index 83d7a9b..612fd7e5 100644
--- a/chrome/browser/status_icons/status_icon.h
+++ b/chrome/browser/status_icons/status_icon.h
@@ -83,6 +83,10 @@
   // secondary click, and dispatch the click event on left click.
   virtual void SetOpenMenuWithSecondaryClick(
       bool open_menu_with_secondary_click);
+
+  // Use template property on the status icon image so that it changes color
+  // based on contrast with the wallpaper.
+  virtual void SetImageTemplate(bool is_template);
 #endif
 
  protected:
diff --git a/chrome/browser/tabmodel/internal/android/java/src/org/chromium/chrome/browser/tabmodel/AsyncTabParamsManagerImpl.kt b/chrome/browser/tabmodel/internal/android/java/src/org/chromium/chrome/browser/tabmodel/AsyncTabParamsManagerImpl.kt
index f630427..5ec947e 100644
--- a/chrome/browser/tabmodel/internal/android/java/src/org/chromium/chrome/browser/tabmodel/AsyncTabParamsManagerImpl.kt
+++ b/chrome/browser/tabmodel/internal/android/java/src/org/chromium/chrome/browser/tabmodel/AsyncTabParamsManagerImpl.kt
@@ -41,6 +41,7 @@
     return data
   }
 
+  @SuppressWarnings("UseKtx")
   private inline fun forEachTab(action: (tab: Tab) -> Unit) {
     val params = mAsyncTabParams
     for (i in 0 until params.size()) {
@@ -62,6 +63,7 @@
       return false
     }
 
+    @SuppressWarnings("UseKtx")
     override fun closeAllIncognitoTabs() {
       val params = mAsyncTabParamsManager.mAsyncTabParams
       // removeAt() does not invalidate indices so long as no read operations are made.
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 04a41ce..adf83290 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -3218,10 +3218,6 @@
       ]
 
       deps += [ "webui/signin/batch_upload:mojo_bindings" ]
-
-      if (enable_glic) {
-        deps += [ "webui/glic:mojo_bindings" ]
-      }
     }
   }
 
@@ -5615,12 +5611,6 @@
   }
 
   if (enable_glic) {
-    sources += [
-      "webui/glic/glic_page_handler.cc",
-      "webui/glic/glic_page_handler.h",
-      "webui/glic/glic_ui.cc",
-      "webui/glic/glic_ui.h",
-    ]
     deps += [
       "//chrome/browser/glic",
       "//chrome/browser/glic:enabling",
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 78e39d1..de8c25c 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -3825,9 +3825,6 @@
       <message name="IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_BACK_OF_CARD_MENU_SIGNIN" desc="The subtitle for the account picker bottom sheet that tells the user what happens if the Continue button is clicked, when signin from BoC triggered the UI.">
         Manage your interests and preferences
       </message>
-      <message name="IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_BACK_OF_CARD_MENU_SIGNIN_OLD" desc="The subtitle for the account picker bottom sheet that tells the user what happens if the Continue button is clicked, when signin from BoC triggered the UI.">
-        Sign in to manage your preferences
-      </message>
       <message name="IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_BENEFITS_SUBTITLE" desc="This string is on a sign-in page, after the user taps on a promotion about signing in to Chrome with the user's Google Account. It explains the overarching user benefit of signing in (access to bookmarks, passwords, history, settings, etc.). We want users to understand why signing in is beneficial, and click 'Continue as name' to sign in to their Google Account. The tone should be informative and lightweight.">
         Get your bookmarks, passwords, and more on all your devices
       </message>
@@ -3835,14 +3832,6 @@
         Sign in to this site and Chrome to get your bookmarks and more on all your devices
       </message>
 
-      <!-- Cormorant Signin Strings -->
-      <message name="IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_TITLE_FOR_CORMORANT_SIGNIN" desc="The title for the bottom sheet that shows the list of accounts on the device and asks the user to select one of these accounts, when signin from Cormorant triggered the UI. [CHAR_LIMIT=27]">
-        Get better suggestions
-      </message>
-      <message name="IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_CORMORANT_SIGNIN" desc="The subtitle for the account picker bottom sheet that tells the user what happens if the Continue button is clicked, when signin from Cormorant triggered the UI.">
-        Sync to get the most relevant content from Google
-      </message>
-
       <!-- Personalized Signin Promos Strings -->
       <message name="IDS_SYNC_PROMO_CONTINUE_AS" desc="Button to sign into Chrome with the displayed account and without having to reenter a password. ‘John’ is replaced with the user’s given name, or the user’s full name if the given name is not available. Ensure consistency with related OneGoogle sign-in buttons (see e.g. TC ID 5569230012177947065).">
         Continue as <ph name="USER_FULL_NAME">%1$s<ex>John</ex></ph>
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_BACK_OF_CARD_MENU_SIGNIN_OLD.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_BACK_OF_CARD_MENU_SIGNIN_OLD.png.sha1
deleted file mode 100644
index 296b985..0000000
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_BACK_OF_CARD_MENU_SIGNIN_OLD.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-efc82ef28e535ecd93eeddedf88a5baa79ad5ba8
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_CORMORANT_SIGNIN.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_CORMORANT_SIGNIN.png.sha1
deleted file mode 100644
index 29eeb0f..0000000
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_SUBTITLE_FOR_CORMORANT_SIGNIN.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-f08c823e5e00e27379c0ec5a35a86c39dfb2a8e5
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_TITLE_FOR_CORMORANT_SIGNIN.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_TITLE_FOR_CORMORANT_SIGNIN.png.sha1
deleted file mode 100644
index 29eeb0f..0000000
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SIGNIN_ACCOUNT_PICKER_BOTTOM_SHEET_TITLE_FOR_CORMORANT_SIGNIN.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-f08c823e5e00e27379c0ec5a35a86c39dfb2a8e5
\ No newline at end of file
diff --git a/chrome/browser/ui/ash/test/accelerator_metadata_unittest.cc b/chrome/browser/ui/ash/test/accelerator_metadata_unittest.cc
index 55bffad..d3a0cb1 100644
--- a/chrome/browser/ui/ash/test/accelerator_metadata_unittest.cc
+++ b/chrome/browser/ui/ash/test/accelerator_metadata_unittest.cc
@@ -8,6 +8,7 @@
 #include "base/hash/md5.h"
 #include "base/hash/md5_boringssl.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "build/branding_buildflags.h"
 #include "chrome/browser/ui/views/accelerator_table.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -36,16 +37,13 @@
     "accelerator_layout_table.h and the following value(s) on the "
     "top of this file:\n";
 
-const char* BooleanToString(bool value) {
-  return value ? "true" : "false";
-}
-
 std::string ModifiersToString(int modifiers) {
-  return base::StringPrintf("shift=%s control=%s alt=%s search=%s",
-                            BooleanToString(modifiers & ui::EF_SHIFT_DOWN),
-                            BooleanToString(modifiers & ui::EF_CONTROL_DOWN),
-                            BooleanToString(modifiers & ui::EF_ALT_DOWN),
-                            BooleanToString(modifiers & ui::EF_COMMAND_DOWN));
+  return base::StringPrintf(
+      "shift=%s control=%s alt=%s search=%s",
+      base::ToString<bool>(modifiers & ui::EF_SHIFT_DOWN),
+      base::ToString<bool>(modifiers & ui::EF_CONTROL_DOWN),
+      base::ToString<bool>(modifiers & ui::EF_ALT_DOWN),
+      base::ToString<bool>(modifiers & ui::EF_COMMAND_DOWN));
 }
 
 struct ChromeAcceleratorMappingCmp {
diff --git a/chrome/browser/ui/cocoa/status_icons/status_icon_mac.h b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.h
index eb020cb..f4266217 100644
--- a/chrome/browser/ui/cocoa/status_icons/status_icon_mac.h
+++ b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.h
@@ -37,6 +37,7 @@
                       const message_center::NotifierId& notifier_id) override;
   void SetOpenMenuWithSecondaryClick(
       bool open_menu_with_secondary_click) override;
+  void SetImageTemplate(bool is_template) override;
 
   // StatusIconMenuModel::Observer overrides:
   void OnMenuStateChanged() override;
diff --git a/chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm
index e5c6870b..0e2aea2 100644
--- a/chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm
+++ b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm
@@ -201,6 +201,10 @@
       sendActionOn:(NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown)];
 }
 
+void StatusIconMac::SetImageTemplate(bool is_template) {
+  [item().button.image setTemplate:is_template];
+}
+
 void StatusIconMac::OnMenuStateChanged() {
   // Recreate menu to reflect changes to the menu model.
   CreateMenu(menu_model_, tool_tip_);
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.cc b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.cc
index dd013d3..3a4af77 100644
--- a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.cc
+++ b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.cc
@@ -68,6 +68,7 @@
 namespace tab_groups {
 
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SavedTabGroupUtils, kDeleteGroupMenuItem);
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SavedTabGroupUtils, kLeaveGroupMenuItem);
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SavedTabGroupUtils,
                                       kMoveGroupToNewWindowMenuItem);
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SavedTabGroupUtils,
@@ -457,19 +458,36 @@
           browser, saved_group->saved_guid()),
       ui::DialogModelMenuItem::Params().SetId(kToggleGroupPinStateMenuItem));
 
-  dialog_model
-      .AddMenuItem(
-          ui::ImageModel::FromVectorIcon(kCloseGroupRefreshIcon,
-                                         ui::kColorMenuIcon, kUIUpdateIconSize),
-          l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_DELETE_GROUP),
-          base::BindRepeating(
-              [](const Browser* browser, const base::Uuid& saved_group_guid,
-                 int event_flags) {
-                SavedTabGroupUtils::DeleteSavedGroup(browser, saved_group_guid);
-              },
-              browser, saved_group->saved_guid()),
-          ui::DialogModelMenuItem::Params().SetId(kDeleteGroupMenuItem))
-      .AddSeparator();
+  // Only the owner of a tab group is allowed to delete it, members will have
+  // the option to leave instead.
+  if (IsOwnerOfSharedTabGroup(browser->profile(), saved_group->saved_guid())) {
+    dialog_model.AddMenuItem(
+        ui::ImageModel::FromVectorIcon(kCloseGroupRefreshIcon,
+                                       ui::kColorMenuIcon, kUIUpdateIconSize),
+        l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_DELETE_GROUP),
+        base::BindRepeating(
+            [](const Browser* browser, const base::Uuid& saved_group_guid,
+               int event_flags) {
+              SavedTabGroupUtils::DeleteSavedGroup(browser, saved_group_guid);
+            },
+            browser, saved_group->saved_guid()),
+        ui::DialogModelMenuItem::Params().SetId(kDeleteGroupMenuItem));
+  } else {
+    dialog_model.AddMenuItem(
+        ui::ImageModel::FromVectorIcon(kCloseGroupRefreshIcon,
+                                       ui::kColorMenuIcon, kUIUpdateIconSize),
+        l10n_util::GetStringUTF16(
+            IDS_DATA_SHARING_MEMBER_DELETE_LAST_TAB_CONFIRM),
+        base::BindRepeating(
+            [](const Browser* browser, const base::Uuid& saved_group_guid,
+               int event_flags) {
+              SavedTabGroupUtils::LeaveSharedGroup(browser, saved_group_guid);
+            },
+            browser, saved_group->saved_guid()),
+        ui::DialogModelMenuItem::Params().SetId(kLeaveGroupMenuItem));
+  }
+
+  dialog_model.AddSeparator();
 
   dialog_model.AddTitleItem(l10n_util::GetStringUTF16(IDS_TABS_TITLE_CXMENU),
                             kTabsTitleItem);
diff --git a/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.cc b/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.cc
index bb968e7..73f8e8b 100644
--- a/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.cc
+++ b/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.cc
@@ -208,7 +208,7 @@
 
   submenu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
   int parent_command_id = parent->GetCommand();
-  auto group_id = GetTabGroupIdFromCommandId(parent_command_id);
+  base::Uuid group_id = GetTabGroupIdFromCommandId(parent_command_id);
   TabGroupSyncService* tab_group_service =
       tab_groups::SavedTabGroupUtils::GetServiceForProfile(browser_->profile());
 
@@ -281,17 +281,31 @@
   command_id_to_action_.emplace(
       latest_command_id, Action{Action::Type::PIN_OR_UNPIN_GROUP, group_id});
 
-  // Add item: delete group.
   latest_command_id = GetAndIncrementLatestCommandId();
-  submenu_model_->AddItemWithStringIdAndIcon(
-      latest_command_id, IDS_TAB_GROUP_HEADER_CXMENU_DELETE_GROUP,
-      ui::ImageModel::FromVectorIcon(kCloseGroupRefreshIcon, ui::kColorMenuIcon,
-                                     kUIUpdateIconSize));
-  submenu_model_->SetElementIdentifierAt(
-      submenu_model_->GetIndexOfCommandId(latest_command_id).value(),
-      SavedTabGroupUtils::kDeleteGroupMenuItem);
-  command_id_to_action_.emplace(latest_command_id,
-                                Action{Action::Type::DELETE_GROUP, group_id});
+  if (SavedTabGroupUtils::IsOwnerOfSharedTabGroup(browser_->profile(),
+                                                  group_id)) {
+    // Add item: delete group.
+    submenu_model_->AddItemWithStringIdAndIcon(
+        latest_command_id, IDS_TAB_GROUP_HEADER_CXMENU_DELETE_GROUP,
+        ui::ImageModel::FromVectorIcon(kCloseGroupRefreshIcon,
+                                       ui::kColorMenuIcon, kUIUpdateIconSize));
+    submenu_model_->SetElementIdentifierAt(
+        submenu_model_->GetIndexOfCommandId(latest_command_id).value(),
+        SavedTabGroupUtils::kDeleteGroupMenuItem);
+    command_id_to_action_.emplace(latest_command_id,
+                                  Action{Action::Type::DELETE_GROUP, group_id});
+  } else {
+    // Add item: leave group.
+    submenu_model_->AddItemWithStringIdAndIcon(
+        latest_command_id, IDS_DATA_SHARING_MEMBER_DELETE_LAST_TAB_CONFIRM,
+        ui::ImageModel::FromVectorIcon(kCloseGroupRefreshIcon,
+                                       ui::kColorMenuIcon, kUIUpdateIconSize));
+    submenu_model_->SetElementIdentifierAt(
+        submenu_model_->GetIndexOfCommandId(latest_command_id).value(),
+        SavedTabGroupUtils::kLeaveGroupMenuItem);
+    command_id_to_action_.emplace(latest_command_id,
+                                  Action{Action::Type::LEAVE_GROUP, group_id});
+  }
 
   // Add a separator and title.
   submenu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
@@ -394,7 +408,11 @@
       case Action::Type::DELETE_GROUP:
         SavedTabGroupUtils::DeleteSavedGroup(browser_, uuid);
         break;
-      default:
+      case Action::Type::LEAVE_GROUP:
+        SavedTabGroupUtils::LeaveSharedGroup(browser_, uuid);
+        break;
+      case Action::Type::OPEN_URL:
+      case Action::Type::DEFAULT:
         break;
     }
   } else if (command_id == IDC_CREATE_NEW_TAB_GROUP) {
diff --git a/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.h b/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.h
index 31d0de3..d344bb9 100644
--- a/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.h
+++ b/chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.h
@@ -42,6 +42,7 @@
       OPEN_OR_MOVE_TO_NEW_WINDOW,
       PIN_OR_UNPIN_GROUP,
       DELETE_GROUP,
+      LEAVE_GROUP,
       OPEN_URL,
     };
 
diff --git a/chrome/browser/ui/views/bookmarks/saved_tab_groups/shared_tab_group_interactive_uitest.cc b/chrome/browser/ui/views/bookmarks/saved_tab_groups/shared_tab_group_interactive_uitest.cc
index 43d46554..53aae2a5 100644
--- a/chrome/browser/ui/views/bookmarks/saved_tab_groups/shared_tab_group_interactive_uitest.cc
+++ b/chrome/browser/ui/views/bookmarks/saved_tab_groups/shared_tab_group_interactive_uitest.cc
@@ -9,6 +9,7 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_metrics.h"
+#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
 #include "chrome/browser/ui/tabs/tab_group.h"
 #include "chrome/browser/ui/tabs/tab_group_model.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -420,4 +421,25 @@
       WaitForHide(kTabGroupHeaderElementId), FinishTabstripAnimations());
 }
 
+// Verify members see the leave group button instead of the delete button in the
+// context menu of a tab group. Pressing the button should delete the group.
+IN_PROC_BROWSER_TEST_F(SharedTabGroupInteractiveUiTest,
+                       LeaveGroupPressedFromContextMenu) {
+  TabGroupId group_id = CreateNewTabGroup();
+  ShareTabGroup(group_id, "fake_collaboration_id",
+                data_sharing::MemberRole::kMember, /*should_sign_in=*/true);
+
+  RunTestSequence(
+      WaitForShow(kTabGroupHeaderElementId), FinishTabstripAnimations(),
+      PressButton(kToolbarAppMenuButtonElementId),
+      WaitForShow(AppMenuModel::kTabGroupsMenuItem),
+      SelectMenuItem(AppMenuModel::kTabGroupsMenuItem),
+      SelectMenuItem(STGEverythingMenu::kTabGroup),
+      EnsurePresent(SavedTabGroupUtils::kLeaveGroupMenuItem),
+      SelectMenuItem(SavedTabGroupUtils::kLeaveGroupMenuItem),
+      WaitForShow(kDeletionDialogOkButtonId),
+      PressButton(kDeletionDialogOkButtonId), FinishTabstripAnimations(),
+      WaitForHide(kTabGroupHeaderElementId));
+}
+
 }  // namespace tab_groups
diff --git a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.cc b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.cc
index a140213..0c5cfbb 100644
--- a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.cc
+++ b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.cc
@@ -130,11 +130,6 @@
       std::make_unique<DownloadBubbleUIController>(browser_view_->browser());
 
   BrowserList::GetInstance()->AddObserver(this);
-
-  // Wait until we're done with everything else before creating `controller_`
-  // since it can call `Show()` synchronously.
-  controller_ = std::make_unique<DownloadDisplayController>(
-      this, browser_view_->browser(), bubble_controller_.get());
 }
 
 DownloadToolbarUIController::~DownloadToolbarUIController() {
@@ -143,8 +138,18 @@
   bubble_controller_.reset();
 }
 
+void DownloadToolbarUIController::Init() {
+  // `controller_` can call `Show()` synchronously so it must be initialized
+  // separately at a point where the PinnedToolbarActionsContainer will exist.
+  controller_ = std::make_unique<DownloadDisplayController>(
+      this, browser_view_->browser(), bubble_controller_.get());
+}
+
 void DownloadToolbarUIController::Show() {
   auto* container = GetPinnedToolbarActionsContainer(browser_view_);
+  if (!container) {
+    return;
+  }
   container->ShowActionEphemerallyInToolbar(kActionShowDownloads, true);
 }
 
diff --git a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.h b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.h
index 28c4984..0ce1a55d 100644
--- a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.h
+++ b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.h
@@ -51,6 +51,11 @@
       delete;
   ~DownloadToolbarUIController() override;
 
+  // Create the DownloadDisplayController, this must be called once the
+  // PinnedToolbarActionsContainer is available since the
+  // DownloadDisplayController can call Show() immediately.
+  void Init();
+
   // DownloadDisplay:
   void Show() override;
   void Hide() override;
diff --git a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc
index 34f0687..0242e543 100644
--- a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc
+++ b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc
@@ -9,6 +9,7 @@
 #include "chrome/browser/download/download_browsertest_utils.h"
 #include "chrome/browser/download/offline_item_utils.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.h"
 #include "chrome/browser/ui/ui_features.h"
@@ -44,14 +45,14 @@
     DownloadTestBase::SetUp();
   }
 
-  PinnedToolbarActionsContainer* toolbar_container() {
-    return BrowserView::GetBrowserViewForBrowser(browser())
+  PinnedToolbarActionsContainer* toolbar_container(Browser* browser) {
+    return BrowserView::GetBrowserViewForBrowser(browser)
         ->toolbar()
         ->pinned_toolbar_actions_container();
   }
 
-  ToolbarButton* toolbar_button() {
-    auto* container = toolbar_container();
+  ToolbarButton* toolbar_button(Browser* browser) {
+    auto* container = toolbar_container(browser);
     return container ? container->GetButtonFor(kActionShowDownloads) : nullptr;
   }
 
@@ -72,65 +73,84 @@
 // ChromeOS Ash. See https://crbug.com/1323505.
 #if !BUILDFLAG(IS_CHROMEOS_ASH)
 IN_PROC_BROWSER_TEST_F(DownloadToolbarUIControllerBrowserTest, ShowHide) {
-  EXPECT_EQ(toolbar_button(), nullptr);
+  EXPECT_EQ(toolbar_button(browser()), nullptr);
   controller()->Show();
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
-  EXPECT_NE(toolbar_button(), nullptr);
-  EXPECT_TRUE(toolbar_button()->GetVisible());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
   controller()->Hide();
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
-  EXPECT_EQ(toolbar_button(), nullptr);
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_EQ(toolbar_button(browser()), nullptr);
 }
 
 IN_PROC_BROWSER_TEST_F(DownloadToolbarUIControllerBrowserTest,
                        HideDoesNotRemoveButtonIfPinned) {
-  EXPECT_EQ(toolbar_button(), nullptr);
+  EXPECT_EQ(toolbar_button(browser()), nullptr);
   PinnedToolbarActionsModel* const actions_model =
       PinnedToolbarActionsModel::Get(browser()->profile());
   actions_model->UpdatePinnedState(kActionShowDownloads, true);
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
-  EXPECT_NE(toolbar_button(), nullptr);
-  EXPECT_TRUE(toolbar_button()->GetVisible());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
   // Verify calling Hide does not change the visibility of the button when
   // pinned.
   controller()->Hide();
-  EXPECT_NE(toolbar_button(), nullptr);
-  EXPECT_TRUE(toolbar_button()->GetVisible());
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
 }
 
 IN_PROC_BROWSER_TEST_F(DownloadToolbarUIControllerBrowserTest,
                        ControllerUpdatesButtonEnabledState) {
   // Pin downloads to the toolbar.
-  EXPECT_EQ(toolbar_button(), nullptr);
+  EXPECT_EQ(toolbar_button(browser()), nullptr);
   PinnedToolbarActionsModel* const actions_model =
       PinnedToolbarActionsModel::Get(browser()->profile());
   actions_model->UpdatePinnedState(kActionShowDownloads, true);
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
-  EXPECT_NE(toolbar_button(), nullptr);
-  EXPECT_TRUE(toolbar_button()->GetVisible());
-  EXPECT_TRUE(toolbar_button()->GetEnabled());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
+  EXPECT_TRUE(toolbar_button(browser())->GetEnabled());
   // Disable the via the controller and verify the button's enabled state.
   controller()->Disable();
-  EXPECT_FALSE(toolbar_button()->GetEnabled());
+  EXPECT_FALSE(toolbar_button(browser())->GetEnabled());
   // Enable the via the controller and verify the button's enabled state.
   controller()->Enable();
-  EXPECT_TRUE(toolbar_button()->GetEnabled());
+  EXPECT_TRUE(toolbar_button(browser())->GetEnabled());
 }
 
 IN_PROC_BROWSER_TEST_F(DownloadToolbarUIControllerBrowserTest,
                        ButtonShowsForDownloadingItems) {
-  EXPECT_EQ(toolbar_button(), nullptr);
+  EXPECT_EQ(toolbar_button(browser()), nullptr);
   // Download a file and verify the download button appears after the download.
   ui_test_utils::DownloadURL(
       browser(), ui_test_utils::GetTestUrl(
                      base::FilePath().AppendASCII("downloads"),
                      base::FilePath().AppendASCII("a_zip_file.zip")));
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
-  EXPECT_NE(toolbar_button(), nullptr);
-  EXPECT_TRUE(toolbar_button()->GetVisible());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
   // Verify calling Hide does change the visibility of the button.
   controller()->Hide();
-  EXPECT_EQ(toolbar_button(), nullptr);
+  EXPECT_EQ(toolbar_button(browser()), nullptr);
+}
+
+IN_PROC_BROWSER_TEST_F(DownloadToolbarUIControllerBrowserTest,
+                       ButtonShowsInNewBrowserWithRecentDownload) {
+  EXPECT_EQ(toolbar_button(browser()), nullptr);
+  // Download a file and verify the download button appears after the download.
+  ui_test_utils::DownloadURL(
+      browser(), ui_test_utils::GetTestUrl(
+                     base::FilePath().AppendASCII("downloads"),
+                     base::FilePath().AppendASCII("a_zip_file.zip")));
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
+  // Create another browser and set it as active so the button becomes dormant.
+  Browser* extra_browser = CreateBrowser(browser()->profile());
+  BrowserList::SetLastActive(extra_browser);
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(extra_browser), nullptr);
+  EXPECT_TRUE(toolbar_button(extra_browser)->GetVisible());
 }
 
 IN_PROC_BROWSER_TEST_F(DownloadToolbarUIControllerBrowserTest,
@@ -139,13 +159,13 @@
   PinnedToolbarActionsModel* const actions_model =
       PinnedToolbarActionsModel::Get(browser()->profile());
   actions_model->UpdatePinnedState(kActionShowDownloads, true);
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
-  EXPECT_NE(toolbar_button(), nullptr);
-  EXPECT_TRUE(toolbar_button()->GetVisible());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
   {
     content::LoadStopObserver observer(
         browser()->tab_strip_model()->GetActiveWebContents());
-    ClickButton(toolbar_button());
+    ClickButton(toolbar_button(browser()));
     observer.Wait();
   }
   EXPECT_EQ(GURL(chrome::kChromeUIDownloadsURL),
@@ -158,10 +178,10 @@
       browser(), ui_test_utils::GetTestUrl(
                      base::FilePath().AppendASCII("downloads"),
                      base::FilePath().AppendASCII("a_zip_file.zip")));
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
-  EXPECT_NE(toolbar_button(), nullptr);
-  EXPECT_TRUE(toolbar_button()->GetVisible());
-  ClickButton(toolbar_button());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
+  EXPECT_NE(toolbar_button(browser()), nullptr);
+  EXPECT_TRUE(toolbar_button(browser())->GetVisible());
+  ClickButton(toolbar_button(browser()));
   EXPECT_EQ(controller()->bubble_contents_for_testing()->VisiblePage(),
             DownloadBubbleContentsView::Page::kPrimary);
 }
@@ -172,7 +192,7 @@
       browser(), ui_test_utils::GetTestUrl(
                      base::FilePath().AppendASCII("downloads"),
                      base::FilePath().AppendASCII("a_zip_file.zip")));
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
   controller()->ShowDetails();
   controller()->OpenPrimaryDialog();
   EXPECT_EQ(controller()->bubble_contents_for_testing()->VisiblePage(),
@@ -189,7 +209,7 @@
       browser(), ui_test_utils::GetTestUrl(
                      base::FilePath().AppendASCII("downloads"),
                      base::FilePath().AppendASCII("a_zip_file.zip")));
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
   controller()->ShowDetails();
   controller()->OpenPrimaryDialog();
   EXPECT_EQ(controller()->bubble_contents_for_testing()->VisiblePage(),
@@ -219,7 +239,7 @@
       download_items;
   GetDownloads(browser(), &download_items);
   ASSERT_EQ(1UL, download_items.size());
-  views::test::WaitForAnimatingLayoutManager(toolbar_container());
+  views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
 
   offline_items_collection::ContentId content_id =
       OfflineItemUtils::GetContentIdForDownload(download_items[0].get());
diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
index d2ff010..7dfc7e36 100644
--- a/chrome/browser/ui/views/frame/browser_view.cc
+++ b/chrome/browser/ui/views/frame/browser_view.cc
@@ -4665,6 +4665,11 @@
 #endif
 
   toolbar_->Init();
+  if (download::IsDownloadBubbleEnabled() &&
+      features::IsToolbarPinningEnabled() &&
+      base::FeatureList::IsEnabled(features::kPinnableDownloadsButton)) {
+    browser_->GetFeatures().download_toolbar_ui_controller()->Init();
+  }
 
   // TODO(pbos): Investigate whether the side panels should be creatable when
   // the ToolbarView does not create a button for them. This specifically seems
diff --git a/chrome/browser/ui/views/side_panel/side_panel_web_ui_view.cc b/chrome/browser/ui/views/side_panel/side_panel_web_ui_view.cc
index 4713131a..6be7803 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_web_ui_view.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_web_ui_view.cc
@@ -8,6 +8,7 @@
 #include "base/metrics/user_metrics_action.h"
 #include "chrome/browser/extensions/api/bookmark_manager_private/bookmark_manager_private_api.h"
 #include "chrome/browser/feature_engagement/tracker_factory.h"
+#include "chrome/browser/performance_manager/public/side_panel_loading_policy.h"
 #include "chrome/browser/ui/bookmarks/bookmark_utils.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_window.h"
@@ -38,6 +39,11 @@
   contents_wrapper_->SetHost(weak_factory_.GetWeakPtr());
   SetWebContents(contents_wrapper_->web_contents());
 
+  // The mechanism that ensure the Side Panel will load at high priority even
+  // while it is still not visible requires the WebContents to be tagged.
+  performance_manager::execution_context_priority::MarkAsSidePanel(
+      contents_wrapper_->web_contents());
+
   // For per-window side panels the scoped browser does not change. The browser
   // is cleared automatically when the browser is closed.
   if (scope.get_scope_type() == SidePanelEntryScope::ScopeType::kBrowser) {
diff --git a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_controller_20_unittest.cc b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_controller_20_unittest.cc
index fca6cdf..ac875e7 100644
--- a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_controller_20_unittest.cc
+++ b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_controller_20_unittest.cc
@@ -87,6 +87,11 @@
 using ::testing::Return;
 
 namespace {
+
+// Somewhere around 2020.
+const base::Time kSessionStartTime =
+    base::Time::FromDeltaSinceWindowsEpoch(420 * base::Days(365));
+
 BASE_FEATURE(kTestIPHFeature,
              "TEST_TestIPHFeature",
              base::FEATURE_ENABLED_BY_DEFAULT);
@@ -113,6 +118,7 @@
              base::FEATURE_ENABLED_BY_DEFAULT);
 constexpr char kTestTutorialIdentifier[] = "Test Tutorial";
 DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kOneOffIPHElementId);
+
 }  // namespace
 
 using user_education::FeaturePromoClosedReason;
@@ -139,7 +145,7 @@
 using ShowPromoCallback =
     BrowserFeaturePromoController20::ShowPromoResultCallback;
 
-class BrowserFeaturePromoController20Test : public TestWithBrowserView {
+class BrowserFeaturePromoController20TestBase : public TestWithBrowserView {
  public:
   void SetUp() override {
     std::vector<base::test::FeatureRef> enabled_features;
@@ -150,15 +156,6 @@
       disabled_features.emplace_back(*feature);
     }
 
-    // Enable or disable V2.
-    if (UseV2()) {
-      enabled_features.emplace_back(
-          user_education::features::kUserEducationExperienceVersion2);
-    } else {
-      disabled_features.emplace_back(
-          user_education::features::kUserEducationExperienceVersion2);
-    }
-
     // Do the enabling or disabling.
     scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
 
@@ -226,7 +223,7 @@
             kCustomActionIPHFeature, kToolbarAppMenuButtonElementId,
             IDS_CHROME_TIP, IDS_CHROME_TIP,
             base::BindRepeating(
-                &BrowserFeaturePromoController20Test::OnCustomPromoAction,
+                &BrowserFeaturePromoController20TestBase::OnCustomPromoAction,
                 base::Unretained(this),
                 base::Unretained(&kCustomActionIPHFeature))));
 
@@ -234,7 +231,7 @@
         kDefaultCustomActionIPHFeature, kToolbarAppMenuButtonElementId,
         IDS_CHROME_TIP, IDS_CHROME_TIP,
         base::BindRepeating(
-            &BrowserFeaturePromoController20Test::OnCustomPromoAction,
+            &BrowserFeaturePromoController20TestBase::OnCustomPromoAction,
             base::Unretained(this),
             base::Unretained(&kDefaultCustomActionIPHFeature)));
     default_custom.SetCustomActionIsDefault(true);
@@ -243,6 +240,7 @@
   }
 
   void TearDown() override {
+    test_util_.reset();
     TestWithBrowserView::TearDown();
     lock_.reset();
   }
@@ -296,7 +294,7 @@
     factories.emplace_back(
         feature_engagement::TrackerFactory::GetInstance(),
         base::BindRepeating(
-            BrowserFeaturePromoController20Test::MakeTestTracker));
+            BrowserFeaturePromoController20TestBase::MakeTestTracker));
     return factories;
   }
 
@@ -388,11 +386,43 @@
     return result;
   }
 
+  // These are public so derived classes can access them.
+
+  void ResetSessionDataImpl(base::TimeDelta since_session_start,
+                            base::TimeDelta idle_time,
+                            BrowserView* browser_view) {
+    UserEducationSessionData session_data;
+    session_data.start_time = kSessionStartTime;
+    session_data.most_recent_active_time =
+        kSessionStartTime + since_session_start;
+    now_ = session_data.most_recent_active_time + idle_time;
+    FeaturePromoPolicyData policy_data;
+    test_util_ =
+        std::make_unique<user_education::test::UserEducationSessionTestUtil>(
+            UserEducationServiceFactory::GetForBrowserContext(
+                browser_view->GetProfile())
+                ->user_education_session_manager(),
+            session_data, policy_data, session_data.most_recent_active_time,
+            now_);
+  }
+
+  void AdvanceTimeImpl(std::optional<base::TimeDelta> until_new_last_active,
+                       base::TimeDelta until_new_now,
+                       bool send_update) {
+    const auto new_active_time =
+        until_new_last_active
+            ? std::make_optional(now_ + *until_new_last_active)
+            : std::nullopt;
+    now_ = new_active_time.value_or(now_) + until_new_now;
+    test_util_->SetNow(now_);
+    if (new_active_time) {
+      test_util_->UpdateLastActiveTime(*new_active_time, send_update);
+    }
+  }
+
  protected:
   FeaturePromoController* controller() { return controller_.get(); }
 
-  virtual bool UseV2() const { return false; }
-
   user_education::FeaturePromoParams MakeParams(
       const base::Feature& feature,
       user_education::FeaturePromoController::BubbleCloseCallback
@@ -455,6 +485,25 @@
               controller_->GetPromoStatus(*feature));
   }
 
+  const base::TimeDelta kLessThanGracePeriod =
+      user_education::features::GetSessionStartGracePeriod() / 4;
+  const base::TimeDelta kMoreThanGracePeriod =
+      user_education::features::GetSessionStartGracePeriod() + base::Minutes(5);
+  const base::TimeDelta kLessThanCooldown =
+      user_education::features::GetLowPriorityCooldown() / 4;
+  const base::TimeDelta kMoreThanCooldown =
+      user_education::features::GetLowPriorityCooldown() + base::Hours(1);
+  const base::TimeDelta kMoreThanSnooze =
+      user_education::features::GetSnoozeDuration() + base::Hours(1);
+  const base::TimeDelta kLessThanAbortCooldown =
+      user_education::features::GetAbortCooldown() / 2;
+  const base::TimeDelta kMoreThanAbortCooldown =
+      user_education::features::GetAbortCooldown() + base::Minutes(5);
+  const base::TimeDelta kLessThanNewSession =
+      user_education::features::GetIdleTimeBetweenSessions() / 4;
+  const base::TimeDelta kMoreThanNewSession =
+      user_education::features::GetIdleTimeBetweenSessions() + base::Hours(1);
+
   raw_ptr<BrowserFeaturePromoController20, DanglingUntriaged> controller_;
   raw_ptr<NiceMock<feature_engagement::test::MockTracker>, DanglingUntriaged>
       mock_tracker_;
@@ -481,11 +530,30 @@
 
   base::test::ScopedFeatureList scoped_feature_list_;
   base::UserActionTester user_action_tester_;
+  std::unique_ptr<user_education::test::UserEducationSessionTestUtil>
+      test_util_;
+  base::Time now_;
 };
 
 using BubbleCloseCallback =
     BrowserFeaturePromoController20::BubbleCloseCallback;
 
+class BrowserFeaturePromoController20Test
+    : public BrowserFeaturePromoController20TestBase {
+ public:
+  BrowserFeaturePromoController20Test() = default;
+  ~BrowserFeaturePromoController20Test() override = default;
+
+  void SetUp() override {
+    BrowserFeaturePromoController20TestBase::SetUp();
+
+    // Ensure that tests start after the grace period. The grace period itself
+    // will be tested in the policy tests.
+    ResetSessionDataImpl(kMoreThanGracePeriod, base::TimeDelta(),
+                         browser()->window()->AsBrowserView());
+  }
+};
+
 TEST_F(BrowserFeaturePromoController20Test, NotifyFeatureUsedIfValidIsValid) {
   EXPECT_CALL(*mock_tracker_, NotifyUsedEvent(testing::Ref(kTestIPHFeature)))
       .Times(1);
@@ -528,7 +596,8 @@
   // If the backend says yes, the controller says yes.
   EXPECT_CALL(*mock_tracker_, WouldTriggerHelpUI(Ref(kTestIPHFeature)))
       .WillOnce(Return(true));
-  EXPECT_TRUE(controller_->CanShowPromo(kTestIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(),
+            controller_->CanShowPromo(kTestIPHFeature));
 }
 
 TEST_F(BrowserFeaturePromoController20Test, AsksBackendToShowPromo) {
@@ -537,7 +606,8 @@
 
   UNCALLED_MOCK_CALLBACK(BubbleCloseCallback, close_callback);
 
-  EXPECT_FALSE(GetPromoResult(kTestIPHFeature, close_callback.Get()));
+  EXPECT_EQ(FeaturePromoResult::kBlockedByConfig,
+            GetPromoResult(kTestIPHFeature, close_callback.Get()));
   EXPECT_FALSE(controller_->IsPromoActive(kTestIPHFeature));
   EXPECT_FALSE(GetPromoBubble());
 }
@@ -564,7 +634,7 @@
   EXPECT_CALL(*mock_tracker_, ShouldTriggerHelpUI(Ref(kTestIPHFeature)))
       .WillOnce(Return(true));
   const auto result = GetPromoResult(kTestIPHFeature);
-  EXPECT_TRUE(result);
+  EXPECT_EQ(FeaturePromoResult::Success(), result);
   CheckNotShownMetrics(kTestIPHFeature, result, /*not_shown_count=*/0);
   EXPECT_TRUE(controller_->IsPromoActive(kTestIPHFeature));
   EXPECT_TRUE(GetPromoBubble());
@@ -575,13 +645,14 @@
       .WillOnce(Return(true));
   EXPECT_CALL(*mock_tracker_, WouldTriggerHelpUI(Ref(kTutorialIPHFeature)))
       .WillRepeatedly(Return(true));
-  EXPECT_TRUE(GetPromoResult(kTestIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(), GetPromoResult(kTestIPHFeature));
   EXPECT_EQ(FeaturePromoResult::kBlockedByPromo,
             controller_->CanShowPromo(kTutorialIPHFeature));
   EXPECT_CALL(*mock_tracker_, Dismissed(Ref(kTestIPHFeature))).Times(1);
   EXPECT_TRUE(controller_->EndPromo(
       kTestIPHFeature, user_education::EndFeaturePromoReason::kFeatureEngaged));
-  EXPECT_TRUE(controller_->CanShowPromo(kTutorialIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(),
+            controller_->CanShowPromo(kTutorialIPHFeature));
 }
 
 TEST_F(BrowserFeaturePromoController20Test, ShowsStartupBubble) {
@@ -732,7 +803,7 @@
       views::ElementTrackerViews::GetContextForWidget(widget.get());
   EXPECT_NE(browser_view()->GetElementContext(), widget_context);
 
-  EXPECT_TRUE(GetPromoResult(kOneOffIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(), GetPromoResult(kOneOffIPHFeature));
   EXPECT_TRUE(controller_->IsPromoActive(kOneOffIPHFeature));
   auto* const bubble = GetPromoBubble();
   ASSERT_TRUE(bubble);
@@ -765,7 +836,7 @@
       ->AddChildView(std::make_unique<views::View>())
       ->SetProperty(views::kElementIdentifierKey, kOneOffIPHElementId);
 
-  EXPECT_TRUE(GetPromoResult(kOneOffIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(), GetPromoResult(kOneOffIPHFeature));
   EXPECT_TRUE(controller_->IsPromoActive(kOneOffIPHFeature));
   auto* const bubble = GetPromoBubble();
   ASSERT_TRUE(bubble);
@@ -819,7 +890,7 @@
 
   EXPECT_NE(browser_view()->GetElementContext(), widget_context);
 
-  EXPECT_TRUE(GetPromoResult(kOneOffIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(), GetPromoResult(kOneOffIPHFeature));
   EXPECT_TRUE(controller_->IsPromoActive(kOneOffIPHFeature));
   auto* const bubble = GetPromoBubble();
   ASSERT_TRUE(bubble);
@@ -833,7 +904,7 @@
        DismissNonCriticalBubbleInRegion_RegionDoesNotOverlap) {
   EXPECT_CALL(*mock_tracker_, ShouldTriggerHelpUI(Ref(kTestIPHFeature)))
       .WillOnce(Return(true));
-  EXPECT_TRUE(GetPromoResult(kTestIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(), GetPromoResult(kTestIPHFeature));
 
   const gfx::Rect bounds =
       GetPromoBubble()->GetWidget()->GetWindowBoundsInScreen();
@@ -850,7 +921,7 @@
        DismissNonCriticalBubbleInRegion_RegionOverlaps) {
   EXPECT_CALL(*mock_tracker_, ShouldTriggerHelpUI(Ref(kTestIPHFeature)))
       .WillOnce(Return(true));
-  EXPECT_TRUE(GetPromoResult(kTestIPHFeature));
+  EXPECT_EQ(FeaturePromoResult::Success(), GetPromoResult(kTestIPHFeature));
 
   const gfx::Rect bounds =
       GetPromoBubble()->GetWidget()->GetWindowBoundsInScreen();
@@ -858,7 +929,7 @@
   gfx::Rect overlapping_region(bounds.x() + 1, bounds.y() + 1, 10, 10);
   const bool result =
       controller_->DismissNonCriticalBubbleInRegion(overlapping_region);
-  EXPECT_TRUE(result);
+  EXPECT_EQ(FeaturePromoResult::Success(), result);
   EXPECT_FALSE(controller_->IsPromoActive(kTestIPHFeature));
 }
 
@@ -897,7 +968,7 @@
       storage_service()->GetCurrentTime() - base::Hours(12));
 
   auto result = GetPromoResult(kTutorialIPHFeature);
-  EXPECT_FALSE(result);
+  EXPECT_EQ(FeaturePromoResult::kBlockedByNewProfile, result);
   CheckNotShownMetrics(kTutorialIPHFeature, result, /*not_shown_count=*/1);
   EXPECT_FALSE(controller_->IsPromoActive(kTutorialIPHFeature));
   EXPECT_FALSE(GetPromoBubble());
@@ -915,7 +986,7 @@
   storage_service()->SavePromoData(kTutorialIPHFeature, data);
 
   auto result = GetPromoResult(kTutorialIPHFeature);
-  EXPECT_FALSE(result);
+  EXPECT_EQ(FeaturePromoResult::kSnoozed, result);
   CheckNotShownMetrics(kTutorialIPHFeature, result, /*not_shown_count=*/1);
   EXPECT_FALSE(controller_->IsPromoActive(kTutorialIPHFeature));
   EXPECT_FALSE(GetPromoBubble());
@@ -1369,13 +1440,13 @@
 
 class BrowserFeaturePromoController20ViewsTest
     : public views::test::InteractiveViewsTestT<
-          BrowserFeaturePromoController20Test> {
+          BrowserFeaturePromoController20TestBase> {
  public:
   BrowserFeaturePromoController20ViewsTest() = default;
   ~BrowserFeaturePromoController20ViewsTest() override = default;
 
   void SetUp() override {
-    InteractiveViewsTestT<BrowserFeaturePromoController20Test>::SetUp();
+    InteractiveViewsTestT<BrowserFeaturePromoController20TestBase>::SetUp();
     SetContextWidget(browser_view()->GetWidget());
   }
 
@@ -1941,9 +2012,6 @@
 }
 
 namespace {
-// Somewhere around 2020.
-const base::Time kSessionStartTime =
-    base::Time::FromDeltaSinceWindowsEpoch(420 * base::Days(365));
 
 BASE_FEATURE(kLegalNoticeFeature,
              "LegalNoticeFeature",
@@ -2047,13 +2115,10 @@
   ~BrowserFeaturePromoController20PriorityTest() override = default;
 
   void TearDown() override {
-    test_util_.reset();
     BrowserFeaturePromoController20ViewsTest::TearDown();
   }
 
  protected:
-  bool UseV2() const override { return true; }
-
   void RegisterIPH() override {
     BrowserFeaturePromoController20ViewsTest::RegisterIPH();
 
@@ -2110,38 +2175,21 @@
   auto ResetSessionData(base::TimeDelta since_session_start,
                         base::TimeDelta idle_time = base::Seconds(1)) {
     return std::move(
-        WithView(kBrowserViewElementId, [this, since_session_start,
-                                         idle_time](BrowserView* browser_view) {
-          UserEducationSessionData session_data;
-          session_data.start_time = kSessionStartTime;
-          session_data.most_recent_active_time =
-              kSessionStartTime + since_session_start;
-          now_ = session_data.most_recent_active_time + idle_time;
-          FeaturePromoPolicyData policy_data;
-          test_util_ = std::make_unique<
-              user_education::test::UserEducationSessionTestUtil>(
-              UserEducationServiceFactory::GetForBrowserContext(
-                  browser_view->GetProfile())
-                  ->user_education_session_manager(),
-              session_data, policy_data, session_data.most_recent_active_time,
-              now_);
-        }).AddDescriptionPrefix("ResetSessionData()"));
+        WithView(
+            kBrowserViewElementId,
+            base::BindOnce(
+                &BrowserFeaturePromoController20TestBase::ResetSessionDataImpl,
+                base::Unretained(this), since_session_start, idle_time))
+            .AddDescriptionPrefix("ResetSessionData()"));
   }
 
   auto AdvanceTime(std::optional<base::TimeDelta> until_new_last_active,
                    base::TimeDelta until_new_now = base::Milliseconds(500),
                    bool send_update = true) {
-    return Do([this, until_new_last_active, until_new_now, send_update]() {
-      const auto new_active_time =
-          until_new_last_active
-              ? std::make_optional(now_ + *until_new_last_active)
-              : std::nullopt;
-      now_ = new_active_time.value_or(now_) + until_new_now;
-      test_util_->SetNow(now_);
-      if (new_active_time) {
-        test_util_->UpdateLastActiveTime(*new_active_time, send_update);
-      }
-    });
+    return Do(base::BindRepeating(
+        &BrowserFeaturePromoController20TestBase::AdvanceTimeImpl,
+        base::Unretained(this), until_new_last_active, until_new_now,
+        send_update));
   }
 
   auto CheckPromoStatus(const base::Feature& iph_feature,
@@ -2155,25 +2203,6 @@
                       base::ToString(status), " )"}));
   }
 
-  const base::TimeDelta kLessThanGracePeriod =
-      user_education::features::GetSessionStartGracePeriod() / 4;
-  const base::TimeDelta kMoreThanGracePeriod =
-      user_education::features::GetSessionStartGracePeriod() + base::Minutes(5);
-  const base::TimeDelta kLessThanCooldown =
-      user_education::features::GetLowPriorityCooldown() / 4;
-  const base::TimeDelta kMoreThanCooldown =
-      user_education::features::GetLowPriorityCooldown() + base::Hours(1);
-  const base::TimeDelta kMoreThanSnooze =
-      user_education::features::GetSnoozeDuration() + base::Hours(1);
-  const base::TimeDelta kLessThanAbortCooldown =
-      user_education::features::GetAbortCooldown() / 2;
-  const base::TimeDelta kMoreThanAbortCooldown =
-      user_education::features::GetAbortCooldown() + base::Minutes(5);
-  const base::TimeDelta kLessThanNewSession =
-      user_education::features::GetIdleTimeBetweenSessions() / 4;
-  const base::TimeDelta kMoreThanNewSession =
-      user_education::features::GetIdleTimeBetweenSessions() + base::Hours(1);
-
  private:
   // Ensures some basic orderings of values to avoid triggering unexpected
   // behavior.
@@ -2184,10 +2213,6 @@
     CHECK_LT(kMoreThanAbortCooldown + kMoreThanGracePeriod,
              user_education::features::GetSnoozeDuration());
   }
-
-  std::unique_ptr<user_education::test::UserEducationSessionTestUtil>
-      test_util_;
-  base::Time now_;
 };
 
 TEST_F(BrowserFeaturePromoController20PriorityTest,
@@ -2544,8 +2569,7 @@
 }
 
 class BrowserFeaturePromoController20PolicyTest
-    : public BrowserFeaturePromoController20PriorityTest,
-      public testing::WithParamInterface<bool> {
+    : public BrowserFeaturePromoController20PriorityTest {
  public:
   BrowserFeaturePromoController20PolicyTest() = default;
 
@@ -2584,21 +2608,11 @@
         "ShowHelpBubble()");
   }
 
- protected:
-  bool UseV2() const override { return GetParam(); }
-
  private:
   std::unique_ptr<user_education::HelpBubble> help_bubble_;
 };
 
-INSTANTIATE_TEST_SUITE_P(,
-                         BrowserFeaturePromoController20PolicyTest,
-                         testing::Bool(),
-                         [](const testing::TestParamInfo<bool>& param) {
-                           return param.param ? "V2" : "Legacy";
-                         });
-
-TEST_P(BrowserFeaturePromoController20PolicyTest, TwoLowPriorityPromos) {
+TEST_F(BrowserFeaturePromoController20PolicyTest, TwoLowPriorityPromos) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTestIPHFeature),
                   ExpectShowingPromo(&kTestIPHFeature),
@@ -2607,7 +2621,7 @@
                   ExpectShowingPromo(&kTestIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        ActionableAlertOverridesLowPriority) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTestIPHFeature),
@@ -2615,7 +2629,7 @@
                   ExpectShowingPromo(&kActionableAlertIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest, TwoActionableAlerts) {
+TEST_F(BrowserFeaturePromoController20PolicyTest, TwoActionableAlerts) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kActionableAlertIPHFeature),
                   ExpectShowingPromo(&kActionableAlertIPHFeature),
@@ -2624,7 +2638,7 @@
                   ExpectShowingPromo(&kActionableAlertIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        LegalNoticeOverridesLowPriority) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTestIPHFeature),
@@ -2632,7 +2646,7 @@
                   ExpectShowingPromo(&kLegalNoticeFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        LegalNoticeOverridesActionableAlert) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kActionableAlertIPHFeature),
@@ -2640,7 +2654,7 @@
                   ExpectShowingPromo(&kLegalNoticeFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest, TwoLegalNotices) {
+TEST_F(BrowserFeaturePromoController20PolicyTest, TwoLegalNotices) {
   RunTestSequence(
       ResetSessionData(kMoreThanGracePeriod),
       MaybeShowPromo(kLegalNoticeFeature),
@@ -2649,46 +2663,43 @@
       ExpectShowingPromo(&kLegalNoticeFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        GracePeriodBlocksHeavyweightInV2) {
-  RunTestSequence(
-      ResetSessionData(kLessThanGracePeriod),
-      MaybeShowPromo(kTutorialIPHFeature,
-                     UseV2() ? FeaturePromoResult::kBlockedByGracePeriod
-                             : FeaturePromoResult::Success()));
+  RunTestSequence(ResetSessionData(kLessThanGracePeriod),
+                  MaybeShowPromo(kTutorialIPHFeature,
+                                 FeaturePromoResult::kBlockedByGracePeriod));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        GracePeriodDoesNotBlockLightweightInV2) {
   RunTestSequence(
       ResetSessionData(kLessThanGracePeriod),
       MaybeShowPromo(kTestIPHFeature, FeaturePromoResult::Success()));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        GracePeriodDoesNotBlockHeavyweightLegalNotice) {
   RunTestSequence(
       ResetSessionData(kLessThanGracePeriod),
       MaybeShowPromo(kLegalNoticeFeature2, FeaturePromoResult::Success()));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        GracePeriodDoesNotBlockActionableAlert) {
   RunTestSequence(ResetSessionData(kLessThanGracePeriod),
                   MaybeShowPromo(kActionableAlertIPHFeature2,
                                  FeaturePromoResult::Success()));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        GracePeriodBlocksHeavyweightInV2AfterNewSession) {
-  RunTestSequence(
-      ResetSessionData(kLessThanGracePeriod), AdvanceTime(kMoreThanNewSession),
-      MaybeShowPromo(kTutorialIPHFeature,
-                     UseV2() ? FeaturePromoResult::kBlockedByGracePeriod
-                             : FeaturePromoResult::Success()));
+  RunTestSequence(ResetSessionData(kLessThanGracePeriod),
+                  AdvanceTime(kMoreThanNewSession),
+                  MaybeShowPromo(kTutorialIPHFeature,
+                                 FeaturePromoResult::kBlockedByGracePeriod));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        GracePeriodDoesNotBlocksHeavyweightLongAfterNewSession) {
   RunTestSequence(
       ResetSessionData(base::Seconds(60)), AdvanceTime(kMoreThanNewSession),
@@ -2696,17 +2707,16 @@
       MaybeShowPromo(kTutorialIPHFeature, FeaturePromoResult::Success()));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest, CooldownPreventsPromoInV2) {
-  RunTestSequence(
-      ResetSessionData(kMoreThanGracePeriod),
-      MaybeShowPromo(kTutorialIPHFeature), ClosePromo(),
-      AdvanceTime(kLessThanCooldown), AdvanceTime(kMoreThanGracePeriod),
-      MaybeShowPromo(kCustomActionIPHFeature,
-                     UseV2() ? FeaturePromoResult::kBlockedByCooldown
-                             : FeaturePromoResult::Success()));
+TEST_F(BrowserFeaturePromoController20PolicyTest, CooldownPreventsPromoInV2) {
+  RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
+                  MaybeShowPromo(kTutorialIPHFeature), ClosePromo(),
+                  AdvanceTime(kLessThanCooldown),
+                  AdvanceTime(kMoreThanGracePeriod),
+                  MaybeShowPromo(kCustomActionIPHFeature,
+                                 FeaturePromoResult::kBlockedByCooldown));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        CooldownDoesNotPreventLightweightPromo) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTutorialIPHFeature), ClosePromo(),
@@ -2715,7 +2725,7 @@
                   MaybeShowPromo(kTestIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        LightweightPromoDoesNotTriggerCooldown) {
   RunTestSequence(
       ResetSessionData(kMoreThanGracePeriod), MaybeShowPromo(kTestIPHFeature),
@@ -2723,7 +2733,7 @@
       AdvanceTime(kMoreThanGracePeriod), MaybeShowPromo(kTutorialIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        CooldownDoesNotPreventLegalNotice) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTutorialIPHFeature), ClosePromo(),
@@ -2732,7 +2742,7 @@
                   MaybeShowPromo(kLegalNoticeFeature2));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        CooldownDoesNotPreventActionableAlert) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTutorialIPHFeature), ClosePromo(),
@@ -2741,7 +2751,7 @@
                   MaybeShowPromo(kActionableAlertIPHFeature2));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        ExpiredCooldownDoesNotPreventPromo) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTutorialIPHFeature), ClosePromo(),
@@ -2750,7 +2760,7 @@
                   MaybeShowPromo(kCustomActionIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        AbortedPromoDoesNotTriggerCooldown) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   // Show an immediately close the promo without user
@@ -2760,7 +2770,7 @@
                   MaybeShowPromo(kCustomActionIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        AbortedPromoDoesTriggerIndividualCooldown) {
   RunTestSequence(ResetSessionData(kMoreThanGracePeriod),
                   MaybeShowPromo(kTutorialIPHFeature), AbortPromo(),
@@ -2770,19 +2780,17 @@
                                  FeaturePromoResult::kRecentlyAborted));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        AbortedPromoDoesNotTriggerSnooze) {
   RunTestSequence(
       ResetSessionData(kMoreThanGracePeriod),
       MaybeShowPromo(kTutorialIPHFeature), AbortPromo(),
       AdvanceTime(kMoreThanAbortCooldown), AdvanceTime(kMoreThanGracePeriod),
       // V1 uses full snooze time for aborted promos.
-      MaybeShowPromo(kTutorialIPHFeature,
-                     UseV2() ? FeaturePromoResult::Success()
-                             : FeaturePromoResult::kRecentlyAborted));
+      MaybeShowPromo(kTutorialIPHFeature, FeaturePromoResult::Success()));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest, SnoozeButtonDisappearsInV2) {
+TEST_F(BrowserFeaturePromoController20PolicyTest, SnoozeButtonDisappearsInV2) {
   RunTestSequence(
       ResetSessionData(kMoreThanGracePeriod),
       // Simulate N-1 snoozes at some distant time in the past.
@@ -2802,12 +2810,10 @@
       // button is *not* present.
       MaybeShowPromo(kSnoozeIPHFeature),
       WaitForShow(HelpBubbleView::kHelpBubbleElementIdForTesting),
-      If([this] { return UseV2(); },
-         EnsureNotPresent(HelpBubbleView::kFirstNonDefaultButtonIdForTesting),
-         EnsurePresent(HelpBubbleView::kFirstNonDefaultButtonIdForTesting)));
+      EnsureNotPresent(HelpBubbleView::kFirstNonDefaultButtonIdForTesting));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        TutorialSnoozeButtonChangesInV2) {
   RunTestSequence(
       ResetSessionData(kMoreThanGracePeriod),
@@ -2828,14 +2834,12 @@
       // button is *not* present.
       MaybeShowPromo(kTutorialIPHFeature),
       WaitForShow(HelpBubbleView::kHelpBubbleElementIdForTesting),
-      CheckViewProperty(
-          HelpBubbleView::kFirstNonDefaultButtonIdForTesting,
-          &views::LabelButton::GetText,
-          l10n_util::GetStringUTF16(UseV2() ? IDS_PROMO_DISMISS_BUTTON
-                                            : IDS_PROMO_SNOOZE_BUTTON)));
+      CheckViewProperty(HelpBubbleView::kFirstNonDefaultButtonIdForTesting,
+                        &views::LabelButton::GetText,
+                        l10n_util::GetStringUTF16(IDS_PROMO_DISMISS_BUTTON)));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        IdleAtStartupStillShowsPromo) {
   RunTestSequence(
       ResetSessionData(base::TimeDelta()),
@@ -2844,13 +2848,12 @@
       MaybeShowPromo(kTutorialIPHFeature));
 }
 
-TEST_P(BrowserFeaturePromoController20PolicyTest,
+TEST_F(BrowserFeaturePromoController20PolicyTest,
        IdleAtStartupPromoBlockedByNewSession) {
   RunTestSequence(
       ResetSessionData(base::TimeDelta()),
       AdvanceTime(std::nullopt, kMoreThanNewSession, true),
       AdvanceTime(base::Seconds(15), base::Milliseconds(100), false),
       MaybeShowPromo(kTutorialIPHFeature,
-                     UseV2() ? FeaturePromoResult::kBlockedByGracePeriod
-                             : FeaturePromoResult::Success()));
+                     FeaturePromoResult::kBlockedByGracePeriod));
 }
diff --git a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.cc b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.cc
index b9b91785..3af78696 100644
--- a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.cc
+++ b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.cc
@@ -4,10 +4,12 @@
 
 #include "chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.h"
 
+#include "base/time/default_clock.h"
 #include "chrome/browser/privacy_sandbox/privacy_sandbox_service.h"
 #include "chrome/browser/privacy_sandbox/privacy_sandbox_service_factory.h"
 #include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h"
 #include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service_factory.h"
+#include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/location_bar/location_bar.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
@@ -20,9 +22,11 @@
 #include "components/user_education/common/feature_promo/feature_promo_result.h"
 #include "components/user_education/common/feature_promo/impl/common_preconditions.h"
 #include "components/user_education/common/feature_promo/impl/precondition_data.h"
+#include "components/user_education/common/user_education_features.h"
 #include "components/user_education/webui/help_bubble_handler.h"
 #include "components/user_education/webui/tracked_element_webui.h"
 #include "content/public/browser/web_contents.h"
+#include "ui/events/types/event_type.h"
 #include "ui/views/interaction/element_tracker_views.h"
 #include "ui/views/widget/widget.h"
 
@@ -34,6 +38,7 @@
     kBrowserNotClosingPrecondition);
 DEFINE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(
     kNoCriticalNoticeShowingPrecondition);
+DEFINE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(kUserNotActivePrecondition);
 
 WindowActivePrecondition::WindowActivePrecondition()
     : FeaturePromoPreconditionBase(kWindowActivePrecondition,
@@ -143,3 +148,55 @@
 
   return user_education::FeaturePromoResult::Success();
 }
+
+UserNotActivePrecondition::UserNotActivePrecondition(
+    BrowserView& browser_view,
+    const user_education::UserEducationTimeProvider& time_provider)
+    : FeaturePromoPreconditionBase(
+          kUserNotActivePrecondition,
+          "The user is not actively trying sending input"),
+      browser_view_(browser_view),
+      time_provider_(time_provider),
+      last_active_time_(time_provider_->GetCurrentTime()) {
+  // Note that null is a valid value for the second parameter here; if for
+  // some reason there is no native window it simply falls back to
+  // application-wide event-sniffing, which for this case is better than not
+  // watching events at all.
+  event_monitor_ = views::EventMonitor::CreateWindowMonitor(
+      this, browser_view_->GetWidget()->GetTopLevelWidget()->GetNativeWindow(),
+      {ui::EventType::kKeyPressed, ui::EventType::kKeyReleased,
+       ui::EventType::kMousePressed, ui::EventType::kMouseReleased,
+       ui::EventType::kTouchPressed, ui::EventType::kTouchReleased,
+       ui::EventType::kGestureBegin, ui::EventType::kGestureEnd,
+       ui::EventType::kMouseMoved});
+}
+
+UserNotActivePrecondition::~UserNotActivePrecondition() = default;
+
+void UserNotActivePrecondition::OnEvent(const ui::Event& event) {
+  if (event.type() == ui::EventType::kMouseMoved) {
+    // For mouse moves, do not set the delay timer unless the mouse is being
+    // moved in the top container (toolbar, tabstrip, etc.). Other mouse moves
+    // are not significant enough to warrant delaying an IPH.
+    bool in_top_container = false;
+    if (auto* const top_container =
+            views::ElementTrackerViews::GetInstance()->GetFirstMatchingView(
+                kTopContainerElementId, browser_view_->GetElementContext())) {
+      in_top_container = top_container->GetBoundsInScreen().Contains(
+          event.AsMouseEvent()->root_location());
+    }
+    if (!in_top_container) {
+      return;
+    }
+  }
+  // Delay heavyweight IPH for the prescribed amount of time.
+  last_active_time_ = time_provider_->GetCurrentTime();
+}
+
+user_education::FeaturePromoResult UserNotActivePrecondition::CheckPrecondition(
+    ComputedData&) const {
+  const auto elapsed = time_provider_->GetCurrentTime() - last_active_time_;
+  return elapsed < user_education::features::GetIdleTimeBeforeHeavyweightPromo()
+             ? user_education::FeaturePromoResult::kBlockedByUi
+             : user_education::FeaturePromoResult::Success();
+}
diff --git a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.h b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.h
index 74c36b9..f84fb17 100644
--- a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.h
+++ b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.h
@@ -5,10 +5,19 @@
 #ifndef CHROME_BROWSER_UI_VIEWS_USER_EDUCATION_IMPL_BROWSER_FEATURE_PROMO_PRECONDITIONS_H_
 #define CHROME_BROWSER_UI_VIEWS_USER_EDUCATION_IMPL_BROWSER_FEATURE_PROMO_PRECONDITIONS_H_
 
+#include <memory>
+
 #include "base/memory/raw_ref.h"
+#include "base/time/clock.h"
+#include "base/time/default_clock.h"
+#include "base/time/time.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "components/user_education/common/feature_promo/feature_promo_precondition.h"
 #include "components/user_education/common/feature_promo/feature_promo_result.h"
+#include "components/user_education/common/user_education_storage_service.h"
+#include "ui/events/event.h"
+#include "ui/events/event_observer.h"
+#include "ui/views/event_monitor.h"
 
 DECLARE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(kWindowActivePrecondition);
 DECLARE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(
@@ -19,6 +28,7 @@
     kBrowserNotClosingPrecondition);
 DECLARE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(
     kNoCriticalNoticeShowingPrecondition);
+DECLARE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(kUserNotActivePrecondition);
 
 // Requires that the window a promo will be shown in is active.
 class WindowActivePrecondition
@@ -96,4 +106,28 @@
   const raw_ref<BrowserView> browser_view_;
 };
 
+// Don't show heavyweight notices while the user is typing.
+class UserNotActivePrecondition
+    : public user_education::FeaturePromoPreconditionBase,
+      public ui::EventObserver {
+ public:
+  explicit UserNotActivePrecondition(
+      BrowserView& browser_view,
+      const user_education::UserEducationTimeProvider& time_provider);
+  ~UserNotActivePrecondition() override;
+
+  // FeaturePromoPreconditionBase:
+  user_education::FeaturePromoResult CheckPrecondition(
+      ComputedData& data) const override;
+
+ private:
+  // ui::EventObserver:
+  void OnEvent(const ui::Event& event) override;
+
+  const raw_ref<BrowserView> browser_view_;
+  const raw_ref<const user_education::UserEducationTimeProvider> time_provider_;
+  std::unique_ptr<views::EventMonitor> event_monitor_;
+  base::Time last_active_time_;
+};
+
 #endif  // CHROME_BROWSER_UI_VIEWS_USER_EDUCATION_IMPL_BROWSER_FEATURE_PROMO_PRECONDITIONS_H_
diff --git a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions_interactive_uitest.cc b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions_interactive_uitest.cc
index 50aacfa..25df3cd 100644
--- a/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions_interactive_uitest.cc
+++ b/chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions_interactive_uitest.cc
@@ -2,17 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <ctime>
 #include <memory>
 
+#include "base/run_loop.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/simple_test_clock.h"
+#include "base/time/time.h"
 #include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
 #include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/toolbar_controller_util.h"
+#include "chrome/browser/ui/views/frame/contents_web_view.h"
 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
 #include "chrome/browser/ui/views/toolbar/toolbar_controller.h"
 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
 #include "chrome/browser/ui/views/user_education/impl/browser_feature_promo_preconditions.h"
 #include "chrome/common/webui_url_constants.h"
+#include "chrome/test/base/interactive_test_utils.h"
 #include "chrome/test/interaction/interactive_browser_test.h"
 #include "components/omnibox/browser/autocomplete_controller.h"
 #include "components/omnibox/browser/autocomplete_input.h"
@@ -21,13 +27,21 @@
 #include "components/user_education/common/feature_promo/feature_promo_precondition.h"
 #include "components/user_education/common/feature_promo/feature_promo_result.h"
 #include "components/user_education/common/feature_promo/impl/common_preconditions.h"
+#include "components/user_education/common/user_education_features.h"
+#include "components/user_education/common/user_education_storage_service.h"
+#include "content/public/test/browser_task_environment.h"
 #include "content/public/test/browser_test.h"
+#include "ui/base/accelerators/accelerator.h"
 #include "ui/base/interaction/element_identifier.h"
+#include "ui/base/test/ui_controls.h"
+#include "ui/events/test/event_generator.h"
+#include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/views/interaction/element_tracker_views.h"
 #include "ui/views/test/views_test_utils.h"
 #include "ui/views/view.h"
+#include "ui/views/widget/widget_utils.h"
 #include "url/gurl.h"
 
 namespace {
@@ -298,3 +312,139 @@
           user_education::FeaturePromoResult::kBlockedByUi)
           .SetMustRemainVisible(false));
 }
+
+class BrowserNotActivePreconditionUiTest
+    : public BrowserFeaturePromoPreconditionsUiTest {
+ public:
+  BrowserNotActivePreconditionUiTest() = default;
+  ~BrowserNotActivePreconditionUiTest() override = default;
+
+  void SetUpOnMainThread() override {
+    BrowserFeaturePromoPreconditionsUiTest::SetUpOnMainThread();
+    less_than_activity_time_ =
+        user_education::features::GetIdleTimeBeforeHeavyweightPromo() / 2;
+    more_than_activity_time_ =
+        user_education::features::GetIdleTimeBeforeHeavyweightPromo() +
+        base::Seconds(1);
+    auto* const browser_view = BrowserView::GetBrowserViewForBrowser(browser());
+    time_provider_.set_clock_for_testing(&test_clock_);
+    precondition_ = std::make_unique<UserNotActivePrecondition>(*browser_view,
+                                                                time_provider_);
+    test_clock_.Advance(more_than_activity_time_);
+    event_generator_ = std::make_unique<ui::test::EventGenerator>(
+        views::GetRootWindow(browser_view->GetWidget()),
+        browser_view->GetNativeWindow());
+  }
+
+  void TearDownOnMainThread() override {
+    precondition_.reset();
+    BrowserFeaturePromoPreconditionsUiTest::TearDownOnMainThread();
+  }
+
+  auto Advance(base::TimeDelta time) {
+    return std::move(Do([this, time]() {
+                       test_clock_.Advance(time);
+                     }).SetDescription("Advance()"));
+  }
+
+  auto CheckPrecondResult(user_education::FeaturePromoResult result) {
+    return CheckView(
+        kBrowserViewElementId,
+        [this](BrowserView* browser_view) {
+          user_education::FeaturePromoPrecondition::ComputedData data;
+          return precondition_->CheckPrecondition(data);
+        },
+        result);
+  }
+
+ protected:
+  base::TimeDelta less_than_activity_time_;
+  base::TimeDelta more_than_activity_time_;
+
+  base::SimpleTestClock test_clock_;
+  user_education::UserEducationTimeProvider time_provider_;
+  std::unique_ptr<UserNotActivePrecondition> precondition_;
+  std::unique_ptr<ui::test::EventGenerator> event_generator_;
+};
+
+IN_PROC_BROWSER_TEST_F(BrowserNotActivePreconditionUiTest, ReturnsSuccess) {
+  RunTestSequence(
+      WaitForShow(kBrowserViewElementId),
+      CheckPrecondResult(user_education::FeaturePromoResult::Success()));
+}
+
+IN_PROC_BROWSER_TEST_F(BrowserNotActivePreconditionUiTest,
+                       ReturnsBlockedAfterMouseClick) {
+  RunTestSequence(
+      WaitForShow(kBrowserViewElementId),
+      MoveMouseTo(ContentsWebView::kContentsWebViewElementId), ClickMouse(),
+      CheckPrecondResult(user_education::FeaturePromoResult::kBlockedByUi),
+      Advance(less_than_activity_time_),
+      CheckPrecondResult(user_education::FeaturePromoResult::kBlockedByUi),
+      Advance(more_than_activity_time_),
+      CheckPrecondResult(user_education::FeaturePromoResult::Success()));
+}
+
+// TODO(https://crbug.com/369403281): Mac doesn't properly respond to hover
+// events due to injected mouse moves in interactive tests, so this test won't
+// work on that platform (yet).
+#if BUILDFLAG(IS_MAC)
+#define MAYBE_ReturnsBlockedAfterMouseHoverOverTabstrip \
+  DISABLED_ReturnsBlockedAfterMouseHoverOverTabstrip
+#else
+#define MAYBE_ReturnsBlockedAfterMouseHoverOverTabstrip \
+  ReturnsBlockedAfterMouseHoverOverTabstrip
+#endif
+IN_PROC_BROWSER_TEST_F(BrowserNotActivePreconditionUiTest,
+                       MAYBE_ReturnsBlockedAfterMouseHoverOverTabstrip) {
+  RunTestSequence(
+      WaitForShow(kBrowserViewElementId),
+      // Hovering the tabstrip does cause a delay.
+      MoveMouseTo(kTabStripElementId),
+      CheckPrecondResult(user_education::FeaturePromoResult::kBlockedByUi),
+      Advance(less_than_activity_time_),
+      CheckPrecondResult(user_education::FeaturePromoResult::kBlockedByUi),
+      Advance(more_than_activity_time_),
+      CheckPrecondResult(user_education::FeaturePromoResult::Success()));
+}
+
+IN_PROC_BROWSER_TEST_F(BrowserNotActivePreconditionUiTest,
+                       ReturnsSuccessWhenHoveringOutsideTopContainer) {
+  gfx::Point start;
+  gfx::Point finish;
+  RunTestSequence(
+      WaitForShow(kBrowserViewElementId),
+      WithView(ContentsWebView::kContentsWebViewElementId,
+               [&](views::View* contents) {
+                 // Pick a start and end point at opposite corners of the
+                 // contents pane, inset into the pane slightly.
+                 auto bounds = contents->GetBoundsInScreen();
+                 bounds.Inset(3);
+                 start = bounds.origin();
+                 finish = bounds.bottom_right();
+               }),
+      // Move to the starting point.
+      MoveMouseTo(std::ref(start)),
+      // Since the move might pass through the top container, wait long enough
+      // that it doesn't matter.
+      Advance(more_than_activity_time_),
+      CheckPrecondResult(user_education::FeaturePromoResult::Success()),
+      // Move to the ending point. Since the move does not pass through the top
+      // container, this should not affect the precondition.
+      MoveMouseTo(std::ref(finish)),
+      CheckPrecondResult(user_education::FeaturePromoResult::Success()));
+}
+
+IN_PROC_BROWSER_TEST_F(BrowserNotActivePreconditionUiTest,
+                       ReturnsBlockedAfterKeyPress) {
+  RunTestSequence(
+      WaitForShow(kBrowserViewElementId), Check([this]() {
+        return ui_test_utils::SendKeyPressSync(
+            browser(), ui::KeyboardCode::VKEY_A, false, false, false, false);
+      }),
+      CheckPrecondResult(user_education::FeaturePromoResult::kBlockedByUi),
+      Advance(less_than_activity_time_),
+      CheckPrecondResult(user_education::FeaturePromoResult::kBlockedByUi),
+      Advance(more_than_activity_time_),
+      CheckPrecondResult(user_education::FeaturePromoResult::Success()));
+}
diff --git a/chrome/browser/ui/webauthn/passkey_upgrade_request_controller.cc b/chrome/browser/ui/webauthn/passkey_upgrade_request_controller.cc
index b838944..985d800 100644
--- a/chrome/browser/ui/webauthn/passkey_upgrade_request_controller.cc
+++ b/chrome/browser/ui/webauthn/passkey_upgrade_request_controller.cc
@@ -91,26 +91,36 @@
   CHECK(pending_callback_);
   password_manager::LoginsResult result =
       password_manager::GetLoginsOrEmptyListOnFailure(results_or_error);
-  bool found = false;
-  // Passwords must have been used within the last 90 days in order to be
-  // eligible.
-  const auto min_last_used = base::Time::Now() - base::Days(90);
+  bool upgrade_eligible = false;
+  bool match_not_recent = false;
+  // A password with a matching username must have been used within the last 5
+  // minutes in order for the automatic passkey upgrade to succeed.
+  base::TimeDelta kLastUsedThreshold = base::Minutes(5);
+  const auto min_last_used = base::Time::Now() - kLastUsedThreshold;
   for (const password_manager::PasswordForm& password_form : result) {
-    if (password_form.username_value == user_name_ &&
-        password_form.date_last_used >= min_last_used) {
-      found = true;
-      break;
+    if (password_form.username_value != user_name_) {
+      continue;
     }
+    if (password_form.date_last_used < min_last_used) {
+      match_not_recent = true;
+      continue;
+    }
+    upgrade_eligible = true;
+    break;
   }
 
-  if (!found) {
-    FIDO_LOG(EVENT) << "Passkey upgrade request failed, no matching password";
+  if (!upgrade_eligible) {
+    if (match_not_recent) {
+      FIDO_LOG(EVENT) << "Passkey upgrade request failed, matching password "
+                         "not recently used";
+    } else {
+      FIDO_LOG(EVENT) << "Passkey upgrade request failed, no matching password";
+    }
     std::move(pending_callback_).Run(false);
     return;
   }
 
   CHECK(enclave_request_callback_);
-  // TODO(crbug.com/377758786): Make the request up=0.
   enclave_transaction_ = std::make_unique<GPMEnclaveTransaction>(
       /*delegate=*/this, PasskeyModelFactory::GetForProfile(profile()),
       device::FidoRequestType::kMakeCredential, rp_id_,
diff --git a/chrome/browser/ui/webui/BUILD.gn b/chrome/browser/ui/webui/BUILD.gn
index 0d928ba9..1bd1c82 100644
--- a/chrome/browser/ui/webui/BUILD.gn
+++ b/chrome/browser/ui/webui/BUILD.gn
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//build/config/chromeos/ui_mode.gni")
+import("//build/config/features.gni")
 import("//chromeos/dbus/config/use_real_dbus_clients.gni")
 import("//extensions/buildflags/buildflags.gni")
 import("//tools/metrics/generate_allowlist_from_histograms_file.gni")
@@ -65,6 +66,10 @@
       "//chromeos/constants",
     ]
   }
+
+  if (enable_glic) {
+    deps += [ "//chrome/browser/glic" ]
+  }
 }
 
 source_set("webui") {
diff --git a/chrome/browser/ui/webui/certificate_manager/client_cert_sources_writable_unittest.cc b/chrome/browser/ui/webui/certificate_manager/client_cert_sources_writable_unittest.cc
index b521141..c086bf5 100644
--- a/chrome/browser/ui/webui/certificate_manager/client_cert_sources_writable_unittest.cc
+++ b/chrome/browser/ui/webui/certificate_manager/client_cert_sources_writable_unittest.cc
@@ -111,6 +111,8 @@
       const std::vector<certificate_manager_v2::mojom::CertificateSource>&
           sources) override {}
 
+  void TriggerMetadataUpdate() override {}
+
   void set_mocked_import_password(std::optional<std::string> password) {
     password_ = std::move(password);
   }
diff --git a/chrome/browser/ui/webui/certificate_manager/user_cert_sources.cc b/chrome/browser/ui/webui/certificate_manager/user_cert_sources.cc
index 7d9e1360..1a5e7a5 100644
--- a/chrome/browser/ui/webui/certificate_manager/user_cert_sources.cc
+++ b/chrome/browser/ui/webui/certificate_manager/user_cert_sources.cc
@@ -165,36 +165,6 @@
                               std::move(export_certs), file_name);
 }
 
-void DeleteCertificateResultAsync(
-    CertificateManagerPageHandler::DeleteCertificateCallback callback,
-    bool result) {
-  if (result) {
-    std::move(callback).Run(
-        certificate_manager_v2::mojom::ActionResult::NewSuccess(
-            certificate_manager_v2::mojom::SuccessResult::kSuccess));
-    return;
-  }
-  std::move(callback).Run(certificate_manager_v2::mojom::ActionResult::NewError(
-      "Error deleting certificate"));
-}
-
-void GotDeleteConfirmation(
-    const std::string& sha256hash_hex,
-    CertificateManagerPageHandler::DeleteCertificateCallback callback,
-    base::WeakPtr<Profile> profile,
-    bool confirmed) {
-  if (confirmed && profile) {
-    net::ServerCertificateDatabaseService* server_cert_service =
-        net::ServerCertificateDatabaseServiceFactory::GetForBrowserContext(
-            profile.get());
-    server_cert_service->DeleteCertificate(
-        sha256hash_hex,
-        base::BindOnce(&DeleteCertificateResultAsync, std::move(callback)));
-    return;
-  }
-  std::move(callback).Run(nullptr);
-}
-
 }  // namespace
 
 UserCertSource::UserCertSource(
@@ -272,8 +242,42 @@
               base::UTF8ToUTF16(display_name)),
           l10n_util::GetStringUTF8(
               IDS_SETTINGS_CERTIFICATE_MANAGER_V2_DELETE_SERVER_CERT_DESCRIPTION),
-          base::BindOnce(&GotDeleteConfirmation, sha256hash_hex,
-                         std::move(callback), profile_->GetWeakPtr()));
+          base::BindOnce(&UserCertSource::GotDeleteConfirmation,
+                         weak_ptr_factory_.GetWeakPtr(), sha256hash_hex,
+                         std::move(callback)));
+}
+
+void UserCertSource::GotDeleteConfirmation(
+    const std::string& sha256hash_hex,
+    CertificateManagerPageHandler::DeleteCertificateCallback callback,
+    bool confirmed) {
+  if (confirmed) {
+    net::ServerCertificateDatabaseService* server_cert_service =
+        net::ServerCertificateDatabaseServiceFactory::GetForBrowserContext(
+            profile_.get());
+    server_cert_service->DeleteCertificate(
+        sha256hash_hex,
+        base::BindOnce(&UserCertSource::DeleteCertificateResultAsync,
+                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+    return;
+  }
+  std::move(callback).Run(nullptr);
+}
+
+void UserCertSource::DeleteCertificateResultAsync(
+    CertificateManagerPageHandler::DeleteCertificateCallback callback,
+    bool result) {
+  if (result) {
+    // Trigger metadata refresh on local certs page to update count.
+    (*remote_client_)->TriggerMetadataUpdate();
+
+    std::move(callback).Run(
+        certificate_manager_v2::mojom::ActionResult::NewSuccess(
+            certificate_manager_v2::mojom::SuccessResult::kSuccess));
+    return;
+  }
+  std::move(callback).Run(certificate_manager_v2::mojom::ActionResult::NewError(
+      "Error deleting certificate"));
 }
 
 void UserCertSource::ImportCertificate(
@@ -327,6 +331,7 @@
       base::BindOnce(&UserCertSource::FileRead,
                      weak_ptr_factory_.GetWeakPtr()));
 }
+
 void UserCertSource::FileSelectionCanceled() {
   select_file_dialog_ = nullptr;
   std::move(import_callback_).Run(nullptr);
@@ -379,6 +384,9 @@
 
 void UserCertSource::ImportCertificateResult(bool success) {
   if (success) {
+    // Trigger metadata refresh on local certs page to update count.
+    (*remote_client_)->TriggerMetadataUpdate();
+
     std::move(import_callback_)
         .Run(certificate_manager_v2::mojom::ActionResult::NewSuccess(
             certificate_manager_v2::mojom::SuccessResult::kSuccess));
diff --git a/chrome/browser/ui/webui/certificate_manager/user_cert_sources.h b/chrome/browser/ui/webui/certificate_manager/user_cert_sources.h
index ba82574..bc379d8 100644
--- a/chrome/browser/ui/webui/certificate_manager/user_cert_sources.h
+++ b/chrome/browser/ui/webui/certificate_manager/user_cert_sources.h
@@ -57,6 +57,13 @@
  private:
   void FileRead(std::optional<std::vector<uint8_t>> file_bytes);
   void ImportCertificateResult(bool success);
+  void GotDeleteConfirmation(
+      const std::string& sha256hash_hex,
+      CertificateManagerPageHandler::DeleteCertificateCallback callback,
+      bool confirmed);
+  void DeleteCertificateResultAsync(
+      CertificateManagerPageHandler::DeleteCertificateCallback callback,
+      bool result);
 
   std::string export_file_name_;
   chrome_browser_server_certificate_database::CertificateTrust::
diff --git a/chrome/browser/ui/webui/certificate_manager/user_cert_sources_unittest.cc b/chrome/browser/ui/webui/certificate_manager/user_cert_sources_unittest.cc
index 78b2c13c..e4b4bf7 100644
--- a/chrome/browser/ui/webui/certificate_manager/user_cert_sources_unittest.cc
+++ b/chrome/browser/ui/webui/certificate_manager/user_cert_sources_unittest.cc
@@ -51,10 +51,14 @@
       const std::vector<certificate_manager_v2::mojom::CertificateSource>&
           sources) override {}
 
+  void TriggerMetadataUpdate() override { metadata_update_called_ = true; }
+  bool metadata_update_called() { return metadata_update_called_; }
+
   void SetConfirmationResult(bool result) { confirmation_result_ = result; }
 
  private:
   bool confirmation_result_;
+  bool metadata_update_called_ = false;
   mojo::Receiver<certificate_manager_v2::mojom::CertificateManagerPage>
       receiver_;
 };
@@ -153,11 +157,18 @@
       select_file_dialog_opened_waiter.GetRepeatingCallback());
   base::test::TestFuture<certificate_manager_v2::mojom::ActionResultPtr>
       import_future;
+
+  mojo::Remote<certificate_manager_v2::mojom::CertificateManagerPage>
+      fake_page_remote;
+  std::unique_ptr<FakeCertificateManagerPage> fake_page =
+      std::make_unique<FakeCertificateManagerPage>(
+          fake_page_remote.BindNewPipeAndPassReceiver());
+
   UserCertSource source(
       "",
       chrome_browser_server_certificate_database::
           CertificateTrust_CertificateTrustType_CERTIFICATE_TRUST_TYPE_TRUSTED,
-      profile(), nullptr);
+      profile(), &fake_page_remote);
   source.ImportCertificate(web_contents()->GetWeakPtr(),
                            import_future.GetCallback());
   EXPECT_TRUE(select_file_dialog_opened_waiter.Wait());
@@ -179,6 +190,7 @@
       certs[0].cert_metadata.trust().trust_type(),
       chrome_browser_server_certificate_database::
           CertificateTrust_CertificateTrustType_CERTIFICATE_TRUST_TYPE_TRUSTED);
+  EXPECT_TRUE(fake_page->metadata_update_called());
 }
 
 #if !BUILDFLAG(IS_ANDROID)
@@ -336,6 +348,7 @@
                 base::HexEncode(net::X509Certificate::CalculateFingerprint256(
                                     test_cert_2->cert_buffer())
                                     .data)));
+  EXPECT_TRUE(fake_page->metadata_update_called());
 }
 
 TEST_F(UserCertSourcesUnitTest, TestDeleteCertificateConfirmationRejected) {
diff --git a/chrome/browser/ui/webui/chrome_web_ui_configs.cc b/chrome/browser/ui/webui/chrome_web_ui_configs.cc
index 68ee790c..f0dc996 100644
--- a/chrome/browser/ui/webui/chrome_web_ui_configs.cc
+++ b/chrome/browser/ui/webui/chrome_web_ui_configs.cc
@@ -191,7 +191,7 @@
 #endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
 
 #if BUILDFLAG(ENABLE_GLIC)
-#include "chrome/browser/ui/webui/glic/glic_ui.h"
+#include "chrome/browser/glic/glic_ui.h"
 #endif
 
 void RegisterChromeWebUIConfigs() {
diff --git a/chrome/browser/ui/webui/commerce/product_specifications_ui.cc b/chrome/browser/ui/webui/commerce/product_specifications_ui.cc
index ba23dd4..f85ee16 100644
--- a/chrome/browser/ui/webui/commerce/product_specifications_ui.cc
+++ b/chrome/browser/ui/webui/commerce/product_specifications_ui.cc
@@ -85,6 +85,7 @@
       // Main UI strings:
       {"addNewColumn", IDS_COMPARE_ADD_NEW_COLUMN},
       {"buyingOptions", IDS_SHOPPING_INSIGHTS_BUYING_OPTIONS},
+      {"cancelA11yLabel", IDS_CANCEL},
       {"citationA11yLabel", IDS_COMPARE_CITATION_A11Y_LABEL},
       {"compareErrorDescription", IDS_COMPARE_ERROR_DESCRIPTION},
       {"compareErrorMessage", IDS_COMPARE_ERROR_TITLE},
@@ -104,6 +105,7 @@
       {"menuDelete", IDS_COMPARE_CONTEXT_MENU_DELETE},
       {"menuOpenInNewTab", IDS_COMPARE_CONTEXT_MENU_OPEN_IN_NEW_TAB},
       {"menuRename", IDS_COMPARE_CONTEXT_MENU_RENAME},
+      {"numSelected", IDS_COMPARE_NUM_ITEMS_SELECTED},
       {"offlineMessage", IDS_COMPARE_OFFLINE_TOAST_MESSAGE},
       {"openProductPage", IDS_COMPARE_OPEN_PRODUCT_PAGE_IN_NEW_TAB},
       {"pageTitle", IDS_COMPARE_DEFAULT_PAGE_TITLE},
diff --git a/chrome/browser/ui/webui/glic/BUILD.gn b/chrome/browser/ui/webui/glic/BUILD.gn
deleted file mode 100644
index 90eaab3..0000000
--- a/chrome/browser/ui/webui/glic/BUILD.gn
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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("//chrome/common/features.gni")
-import("//mojo/public/tools/bindings/mojom.gni")
-
-assert(enable_glic)
-
-mojom("mojo_bindings") {
-  sources = [ "glic.mojom" ]
-  webui_module_path = "/"
-
-  public_deps = [
-    "//mojo/public/mojom/base",
-    "//skia/public/mojom",
-    "//ui/gfx/geometry/mojom",
-    "//url/mojom:url_mojom_gurl",
-    "//url/mojom:url_mojom_origin",
-  ]
-}
diff --git a/chrome/browser/ui/webui/glic/DIR_METADATA b/chrome/browser/ui/webui/glic/DIR_METADATA
deleted file mode 100644
index d52b354..0000000
--- a/chrome/browser/ui/webui/glic/DIR_METADATA
+++ /dev/null
@@ -1,4 +0,0 @@
-team_email: "glic@google.com"
-buganizer: {
-  component_id: 1676913
-}
diff --git a/chrome/browser/ui/webui/glic/OWNERS b/chrome/browser/ui/webui/glic/OWNERS
deleted file mode 100644
index 958efb2..0000000
--- a/chrome/browser/ui/webui/glic/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-file://chrome/browser/glic/OWNERS
-
-per-file *.mojom=set noparent
-per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
index 20ba4df..3f4c087 100644
--- a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
+++ b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
@@ -1716,7 +1716,13 @@
   EXPECT_THAT(result, testing::HasSubstr(encoded_challenge));
 }
 
-IN_PROC_BROWSER_TEST_F(ChallengeUrlBrowserTest, ChallengeUrlEmptyChallenge) {
+#if BUILDFLAG(IS_MAC)
+#define MAYBE_ChallengeUrlEmptyChallenge DISABLED_ChallengeUrlEmptyChallenge
+#else
+#define MAYBE_ChallengeUrlEmptyChallenge ChallengeUrlEmptyChallenge
+#endif
+IN_PROC_BROWSER_TEST_F(ChallengeUrlBrowserTest,
+                       MAYBE_ChallengeUrlEmptyChallenge) {
   SetRequestHandlerOverride(base::BindLambdaForTesting(
       [](const net::test_server::HttpRequest& request)
           -> std::unique_ptr<net::test_server::HttpResponse> {
@@ -1746,7 +1752,13 @@
   EXPECT_THAT(result, testing::HasSubstr("NotAllowedError"));
 }
 
-IN_PROC_BROWSER_TEST_F(ChallengeUrlBrowserTest, ChallengeUrlWrongContentType) {
+#if BUILDFLAG(IS_MAC)
+#define MAYBE_ChallengeUrlWrongContentType DISABLED_ChallengeUrlWrongContentType
+#else
+#define MAYBE_ChallengeUrlWrongContentType ChallengeUrlWrongContentType
+#endif
+IN_PROC_BROWSER_TEST_F(ChallengeUrlBrowserTest,
+                       MAYBE_ChallengeUrlWrongContentType) {
   SetRequestHandlerOverride(base::BindLambdaForTesting(
       [](const net::test_server::HttpRequest& request)
           -> std::unique_ptr<net::test_server::HttpResponse> {
diff --git a/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java b/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java
index 1032ea2..07054ee9 100644
--- a/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java
+++ b/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java
@@ -195,15 +195,21 @@
 
     /**
      * Opens a specific WebFeed by name with a specific entrypoint.
+     *
      * @param webFeedName the relevant web feed name.
      * @param entryPoint the entry point used to launch the feed.
      */
     default void openWebFeed(String webFeedName, @OpenWebFeedEntryPoint int entryPoint) {}
 
-    /** Requests that a sync consent prompt be shown. */
+    /**
+     * Requests that sign-in flow be started.
+     *
+     * @deprecated Use startSigninFlow() instead.
+     */
+    @Deprecated
     default void showSyncConsentPrompt() {}
 
-    /** Requests that sign in flow be started. */
+    /** Requests that sign-in flow be started. */
     default void startSigninFlow() {}
 
     /** Requests that a sign-in interstitial bottom sheet be shown. */
diff --git a/chrome/browser_exposed_mojom_targets.gni b/chrome/browser_exposed_mojom_targets.gni
index 5d15f3b..511b68b 100644
--- a/chrome/browser_exposed_mojom_targets.gni
+++ b/chrome/browser_exposed_mojom_targets.gni
@@ -14,6 +14,7 @@
 browser_exposed_mojom_targets = [
   "//cc/mojom:layer_type",
   "//cc/mojom:mojom",
+  "//chrome/browser/glic:mojo_bindings",
   "//chrome/browser/lens/core/mojom:mojo_bindings",
   "//chrome/browser/media:mojo_bindings",
   "//chrome/browser/new_tab_page/modules/file_suggestion:mojo_bindings",
@@ -29,7 +30,6 @@
   "//chrome/browser/ui/webui/data_sharing_internals:mojo_bindings",
   "//chrome/browser/ui/webui/discards:mojo_bindings",
   "//chrome/browser/ui/webui/downloads:mojo_bindings",
-  "//chrome/browser/ui/webui/glic:mojo_bindings",
   "//chrome/browser/ui/webui/user_education_internals:mojo_bindings",
   "//chrome/browser/ui/webui/location_internals:mojo_bindings",
   "//chrome/browser/ui/webui/new_tab_page_third_party:mojo_bindings",
diff --git a/chrome/build/android-arm32.pgo.txt b/chrome/build/android-arm32.pgo.txt
index 2c0136fb..55beb6d 100644
--- a/chrome/build/android-arm32.pgo.txt
+++ b/chrome/build/android-arm32.pgo.txt
@@ -1 +1 @@
-chrome-android32-main-1736769296-5b1b6d713431240387bbb0cf12bf0926b0902e59-03b712dbd057f11864ee41f655aaa4cfdb1bf517.profdata
+chrome-android32-main-1736791001-f5a46224bb83ceb9e6b2cc5ccb8546d988065895-10021c5159e4121954dcc274c5eb9646ee6dad38.profdata
diff --git a/chrome/build/android-arm64.pgo.txt b/chrome/build/android-arm64.pgo.txt
index f68d264c..b9008e5b 100644
--- a/chrome/build/android-arm64.pgo.txt
+++ b/chrome/build/android-arm64.pgo.txt
@@ -1 +1 @@
-chrome-android64-main-1736779203-b991293f5dd8ca1c0390f56d694faa058ba52f45-24ca284b572fb69ea18465e3cda70b4bcf7e1bcc.profdata
+chrome-android64-main-1736794606-3b5562cc78b8a8c80dd894f57f809a19850c456e-ad66aa0ac8ad7552127ac34c2c812efe194edc69.profdata
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index b79beb1..cebb897d 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1736769296-e37364023af018acd260136418079d6e11e44258-03b712dbd057f11864ee41f655aaa4cfdb1bf517.profdata
+chrome-linux-main-1736791001-5d39fbca4ac2117546a3612f6feea454b4da00ca-10021c5159e4121954dcc274c5eb9646ee6dad38.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 72af63968..90c46af 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1736776734-9ded1333de55019459132194eeea2783e49e6e2d-40b178a956066500eae776782d7cbbee7a288806.profdata
+chrome-mac-arm-main-1736798394-2eb45a99e528bce7e950ac6b6ad2a9c82ac72a2c-b80efb0bc63804804321a2b178d9a39df90c4d87.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 2acec16..31c16c14 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1736746872-7795a7842de50f7cf78dfa930de27c5d37bb3c57-77a482e6d2f3b2d4f7dd93761999631398997b02.profdata
+chrome-mac-main-1736791001-dbb79b7f528af1f8875ddc3eb1b9998eed86ca74-10021c5159e4121954dcc274c5eb9646ee6dad38.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 523f5ed3..a4b18f9 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1736769296-98fdc7bdfda3199744393c6e383b3552dfdc0071-03b712dbd057f11864ee41f655aaa4cfdb1bf517.profdata
+chrome-win32-main-1736780267-6e6643332bea5129499835f448ff5a8f433c3030-121cfb01b818f003b8fd5ec34f347b123fa610a8.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 4121a56..96e1c84 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1736746872-1dc50767b9fc351df05db35092a278010dc81440-77a482e6d2f3b2d4f7dd93761999631398997b02.profdata
+chrome-win64-main-1736769296-8be801aea43e420f465ae089961d0021065f5f4c-03b712dbd057f11864ee41f655aaa4cfdb1bf517.profdata
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index c5ca87ef..78f92f6 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -7528,6 +7528,7 @@
       "../browser/password_manager/generated_password_leak_detection_pref_unittest.cc",
       "../browser/password_manager/password_change_delegate_mock.cc",
       "../browser/password_manager/password_change_delegate_mock.h",
+      "../browser/performance_manager/execution_context_priority/side_panel_loading_voter_unittest.cc",
       "../browser/platform_util_unittest.cc",
       "../browser/policy/policy_path_parser_unittest.cc",
       "../browser/policy/serial_allow_usb_devices_for_urls_policy_handler_unittest.cc",
diff --git a/chrome/test/base/ash/interactive/cellular/apn_ui_interactive_uitest.cc b/chrome/test/base/ash/interactive/cellular/apn_ui_interactive_uitest.cc
index 99793cb..5470047 100644
--- a/chrome/test/base/ash/interactive/cellular/apn_ui_interactive_uitest.cc
+++ b/chrome/test/base/ash/interactive/cellular/apn_ui_interactive_uitest.cc
@@ -5,6 +5,7 @@
 #include <string>
 
 #include "ash/constants/ash_features.h"
+#include "base/strings/to_string.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/test/base/ash/interactive/cellular/cellular_util.h"
 #include "chrome/test/base/ash/interactive/cellular/esim_interactive_uitest_base.h"
@@ -144,8 +145,8 @@
                                 settings::cellular::ApnDialogAttachCheckbox()),
 
         Log(base::StringPrintf("Select APN type: Default: %s, Attach: %s",
-                               is_default ? "true" : "false",
-                               is_attach ? "true" : "false")),
+                               base::ToString(is_default),
+                               base::ToString(is_attach))),
 
         SelectApnTypeInDialog(is_default, is_attach),
 
diff --git a/chrome/test/base/ash/interactive/interactive_ash_test.cc b/chrome/test/base/ash/interactive/interactive_ash_test.cc
index 3dd410c..8232a6cb 100644
--- a/chrome/test/base/ash/interactive/interactive_ash_test.cc
+++ b/chrome/test/base/ash/interactive/interactive_ash_test.cc
@@ -17,6 +17,7 @@
 #include "base/json/json_writer.h"
 #include "base/json/string_escape.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/test/test_switches.h"
 #include "base/values.h"
 #include "chrome/browser/ash/app_restore/full_restore_app_launch_handler.h"
@@ -596,7 +597,7 @@
              el.managedProperties_.%s.activeValue === %s;
     }
   )",
-                         property.c_str(), expected_value ? "true" : "false");
+                         property.c_str(), base::ToString(expected_value));
   return WaitForStateChange(element_id, managed_boolean_change);
 }
 
diff --git a/chrome/test/chromedriver/chrome/devtools_client_impl_unittest.cc b/chrome/test/chromedriver/chrome/devtools_client_impl_unittest.cc
index 5b39073..9dedd89 100644
--- a/chrome/test/chromedriver/chrome/devtools_client_impl_unittest.cc
+++ b/chrome/test/chromedriver/chrome/devtools_client_impl_unittest.cc
@@ -20,6 +20,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/strings/pattern.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/time/time.h"
 #include "base/values.h"
 #include "chrome/test/chromedriver/chrome/devtools_client.h"
@@ -1809,7 +1810,7 @@
       "\"result\": %s,"
       "\"userInput\": \"%s\""
       "}}",
-      (result ? "true" : "false"), user_input.c_str());
+      base::ToString(result), user_input.c_str());
 }
 
 }  // namespace
diff --git a/chrome/test/chromedriver/chrome/web_view_impl.cc b/chrome/test/chromedriver/chrome/web_view_impl.cc
index e81a2f72..4d7d704 100644
--- a/chrome/test/chromedriver/chrome/web_view_impl.cc
+++ b/chrome/test/chromedriver/chrome/web_view_impl.cc
@@ -27,6 +27,7 @@
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/threading/platform_thread.h"
 #include "base/time/time.h"
 #include "base/uuid.h"
@@ -1033,7 +1034,7 @@
 
   std::string json;
   base::JSONWriter::Write(args, &json);
-  std::string w3c = w3c_compliant_ ? "true" : "false";
+  std::string w3c = base::ToString(w3c_compliant_);
   // TODO(zachconrad): Second null should be array of shadow host ids.
   std::string wrapper_function = base::StringPrintf(
       "function(){ return (%s).apply(null, [%s, %s, %s, arguments]); }",
diff --git a/chrome/test/data/webui/commerce/product_specifications/app_test.ts b/chrome/test/data/webui/commerce/product_specifications/app_test.ts
index 242186e..0d54c203 100644
--- a/chrome/test/data/webui/commerce/product_specifications/app_test.ts
+++ b/chrome/test/data/webui/commerce/product_specifications/app_test.ts
@@ -1808,7 +1808,13 @@
 
     const uuid =
         shoppingServiceApi.getArgs('addProductSpecificationsSet')[0][2];
-    appElement.$.header.dispatchEvent(new CustomEvent('delete-click'));
+    const header = appElement.$.header;
+    header.$.menuButton.click();
+    const menu = header.$.menu.$.menu;
+    const menuItemButton = menu.get().querySelector<HTMLElement>('#delete');
+    assertTrue(!!menuItemButton);
+    menuItemButton.click();
+    await flushTasks();
 
     assertEquals(
         1, shoppingServiceApi.getCallCount('deleteProductSpecificationsSet'));
diff --git a/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_item_test.ts b/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_item_test.ts
index 41653f3b..b1e97844 100644
--- a/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_item_test.ts
+++ b/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_item_test.ts
@@ -6,12 +6,13 @@
 
 import {ProductSpecificationsBrowserProxyImpl} from '//resources/cr_components/commerce/product_specifications_browser_proxy.js';
 import type {CrActionMenuElement} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
+import type {CrCheckboxElement} from '//resources/cr_elements/cr_checkbox/cr_checkbox.js';
 import type {CrIconButtonElement} from '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
 import type {CrInputElement} from '//resources/cr_elements/cr_input/cr_input.js';
 import {PluralStringProxyImpl} from '//resources/js/plural_string_proxy.js';
 import type {ComparisonTableListItemElement} from 'chrome://compare/comparison_table_list_item.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
-import {assertEquals, assertStringContains, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {assertEquals, assertFalse, assertStringContains, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {TestPluralStringProxy} from 'chrome://webui-test/test_plural_string_proxy.js';
 import {$$, eventToPromise, microtasksFinished} from 'chrome://webui-test/test_util.js';
 
@@ -41,6 +42,12 @@
     return trailingIconButton;
   }
 
+  function getCheckbox(): CrCheckboxElement {
+    const checkbox = $$<CrCheckboxElement>(itemElement, '#checkbox');
+    assertTrue(!!checkbox);
+    return checkbox;
+  }
+
   setup(async () => {
     loadTimeData.overrideValues({
       'tableListItemTitle': `Compare ${TABLE_NAME}`,
@@ -143,4 +150,52 @@
       assertEquals(TABLE_UUID, event.detail.uuid);
     });
   });
+
+  suite('checkbox', () => {
+    let checkbox: CrCheckboxElement;
+
+    setup(async () => {
+      itemElement.hasCheckbox = true;
+      await microtasksFinished();
+
+      checkbox = getCheckbox();
+    });
+
+    test(
+        'click on checkbox emits event with UUID and checked state',
+        async () => {
+          let checkboxChangePromise =
+              eventToPromise('checkbox-change', document);
+          checkbox.click();
+
+          let event = await checkboxChangePromise;
+          assertEquals(TABLE_UUID, event.detail.uuid);
+          assertTrue(event.detail.checked);
+
+          // Uncheck.
+          checkboxChangePromise = eventToPromise('checkbox-change', document);
+          checkbox.click();
+
+          event = await checkboxChangePromise;
+          assertEquals(TABLE_UUID, event.detail.uuid);
+          assertFalse(event.detail.checked);
+        });
+
+    test('click on item emits event with UUID and checked state', async () => {
+      let checkboxChangePromise = eventToPromise('checkbox-change', document);
+      checkbox.click();
+
+      let event = await checkboxChangePromise;
+      assertEquals(TABLE_UUID, event.detail.uuid);
+      assertTrue(event.detail.checked);
+
+      // Uncheck.
+      checkboxChangePromise = eventToPromise('checkbox-change', document);
+      checkbox.click();
+
+      event = await checkboxChangePromise;
+      assertEquals(TABLE_UUID, event.detail.uuid);
+      assertFalse(event.detail.checked);
+    });
+  });
 });
diff --git a/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_test.ts b/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_test.ts
index 17276ed..1c8971c 100644
--- a/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_test.ts
+++ b/chrome/test/data/webui/commerce/product_specifications/comparison_table_list_test.ts
@@ -5,16 +5,21 @@
 import 'chrome://compare/comparison_table_list.js';
 
 import {ProductSpecificationsBrowserProxyImpl} from '//resources/cr_components/commerce/product_specifications_browser_proxy.js';
+import {ShoppingServiceBrowserProxyImpl} from '//resources/cr_components/commerce/shopping_service_browser_proxy.js';
 import {PluralStringProxyImpl} from '//resources/js/plural_string_proxy.js';
 import type {ComparisonTableListElement} from 'chrome://compare/comparison_table_list.js';
-import {assertEquals} from 'chrome://webui-test/chai_assert.js';
+import type {ComparisonTableListItemElement} from 'chrome://compare/comparison_table_list_item.js';
+import {assertEquals, assertFalse, assertStringContains, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {TestMock} from 'chrome://webui-test/test_mock.js';
 import {TestPluralStringProxy} from 'chrome://webui-test/test_plural_string_proxy.js';
-import {microtasksFinished} from 'chrome://webui-test/test_util.js';
+import {$$, eventToPromise, microtasksFinished} from 'chrome://webui-test/test_util.js';
 
 import {TestProductSpecificationsBrowserProxy} from './test_product_specifications_browser_proxy.js';
 
 suite('ComparisonTableListTest', () => {
   let listElement: ComparisonTableListElement;
+  const shoppingServiceApi =
+      TestMock.fromClass(ShoppingServiceBrowserProxyImpl);
 
   const TABLES = [
     {
@@ -31,6 +36,21 @@
     },
   ];
 
+  async function toggleCheckboxAtIndex(index: number) {
+    const items =
+        listElement.shadowRoot!.querySelectorAll('comparison-table-list-item');
+    assertTrue(index >= 0 && index < items.length);
+
+    const checkboxChangePromise =
+        eventToPromise('checkbox-change', listElement);
+    const checkbox = $$(items[index]!, '#checkbox');
+    assertTrue(!!checkbox);
+    checkbox.click();
+
+    const event = await checkboxChangePromise;
+    assertTrue(!!event);
+  }
+
   setup(async () => {
     // Used by the item elements in the list.
     const pluralStringProxy = new TestPluralStringProxy();
@@ -39,6 +59,11 @@
     const productSpecsProxy = new TestProductSpecificationsBrowserProxy();
     ProductSpecificationsBrowserProxyImpl.setInstance(productSpecsProxy);
 
+    shoppingServiceApi.reset();
+    shoppingServiceApi.setResultFor(
+        'deleteProductSpecificationsSet', Promise.resolve());
+    ShoppingServiceBrowserProxyImpl.setInstance(shoppingServiceApi);
+
     document.body.innerHTML = window.trustedTypes!.emptyHTML;
     listElement = document.createElement('comparison-table-list');
     listElement.tables = TABLES;
@@ -59,4 +84,53 @@
       assertEquals(TABLES[i]!.imageUrl, items[i]!.imageUrl);
     }
   });
+
+  suite('multi-select', () => {
+    let items: NodeListOf<ComparisonTableListItemElement>;
+
+    setup(async () => {
+      listElement.$.edit.click();
+      await microtasksFinished();
+
+      items = listElement.shadowRoot!.querySelectorAll(
+          'comparison-table-list-item');
+      assertEquals(TABLES.length, items.length);
+      await toggleCheckboxAtIndex(0);
+      await toggleCheckboxAtIndex(1);
+    });
+
+    test('displays the number of selected items', async () => {
+      assertStringContains(listElement.$.toolbar.selectionLabel, '2');
+    });
+
+    test('can delete multiple comparison tables', async () => {
+      const deleteFinishedPromise =
+          eventToPromise('delete-finished-for-testing', listElement);
+      listElement.$.delete.click();
+      await deleteFinishedPromise;
+
+      assertEquals(
+          2, shoppingServiceApi.getCallCount('deleteProductSpecificationsSet'));
+      assertEquals(
+          TABLES[0]!.uuid,
+          shoppingServiceApi.getArgs('deleteProductSpecificationsSet')[0]);
+      assertEquals(
+          TABLES[1]!.uuid,
+          shoppingServiceApi.getArgs('deleteProductSpecificationsSet')[1]);
+    });
+
+    test(
+        'deleting a single table when in multi-select hides all checkboxes',
+        async () => {
+          const menu = items[0]!.$.menu.get();
+          const deleteButton = menu.querySelector<HTMLButtonElement>('#delete');
+          assertTrue(!!deleteButton);
+          deleteButton.click();
+          await microtasksFinished();
+
+          for (let i = 0; i < items.length; i++) {
+            assertFalse(items[i]!.hasCheckbox);
+          }
+        });
+  });
 });
diff --git a/chrome/test/data/webui/commerce/product_specifications/product_selector_test.ts b/chrome/test/data/webui/commerce/product_specifications/product_selector_test.ts
index c05068dbe..c27a87a9 100644
--- a/chrome/test/data/webui/commerce/product_specifications/product_selector_test.ts
+++ b/chrome/test/data/webui/commerce/product_specifications/product_selector_test.ts
@@ -8,9 +8,8 @@
 import {ShoppingServiceBrowserProxyImpl} from 'chrome://resources/cr_components/commerce/shopping_service_browser_proxy.js';
 import {stringToMojoUrl} from 'chrome://resources/js/mojo_type_util.js';
 import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
-import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
 import {TestMock} from 'chrome://webui-test/test_mock.js';
-import {eventToPromise} from 'chrome://webui-test/test_util.js';
+import {eventToPromise, microtasksFinished} from 'chrome://webui-test/test_util.js';
 
 suite('ProductSelectorTest', () => {
   const shoppingServiceApi =
@@ -24,7 +23,7 @@
       imageUrl: 'https://current-selection-image.com',
     };
     document.body.appendChild(selector);
-    await flushTasks();
+    await microtasksFinished();
     return selector;
   }
 
@@ -62,7 +61,7 @@
 
     selector.$.currentProductContainer.dispatchEvent(
         new KeyboardEvent('keydown', {key: 'Enter'}));
-    await flushTasks();
+    await microtasksFinished();
 
     assertNotEquals(menu.$.menu.getIfExists(), null);
     assertTrue(selector.$.currentProductContainer.classList.contains(
@@ -78,7 +77,7 @@
         showingMenuClass));
 
     selector.$.currentProductContainer.click();
-    await flushTasks();
+    await microtasksFinished();
 
     assertTrue(selector.$.currentProductContainer.classList.contains(
         showingMenuClass));
diff --git a/chrome/test/data/webui/lens/overlay/searchbox_test.ts b/chrome/test/data/webui/lens/overlay/searchbox_test.ts
index d4d42e29..10f770c 100644
--- a/chrome/test/data/webui/lens/overlay/searchbox_test.ts
+++ b/chrome/test/data/webui/lens/overlay/searchbox_test.ts
@@ -6,10 +6,10 @@
 
 import {BrowserProxyImpl} from 'chrome-untrusted://lens-overlay/browser_proxy.js';
 import type {LensOverlayAppElement} from 'chrome-untrusted://lens-overlay/lens_overlay_app.js';
+import {loadTimeData} from 'chrome-untrusted://resources/js/load_time_data.js';
+import {assertFalse, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
 import {waitAfterNextRender} from 'chrome-untrusted://webui-test/polymer_test_util.js';
 import {isVisible} from 'chrome-untrusted://webui-test/test_util.js';
-import {assertTrue, assertFalse} from 'chrome-untrusted://webui-test/chai_assert.js';
-import {loadTimeData} from 'chrome-untrusted://resources/js/load_time_data.js';
 
 import {TestLensOverlayBrowserProxy} from './test_overlay_browser_proxy.js';
 
@@ -51,8 +51,11 @@
     assertTrue(isVisible(lensOverlayElement.$.searchbox));
     lensOverlayElement.$.searchbox.$.input.value = 'hello';
 
-    // Simulate searchbox being focused
+    // Simulate searchbox being focused and the autocomplete request being
+    // started.
     lensOverlayElement.setSearchboxFocusForTesting(true);
+    document.dispatchEvent(new CustomEvent('query-autocomplete'));
+    await waitAfterNextRender(lensOverlayElement);
     assertTrue(isVisible(lensOverlayElement.$.searchboxGhostLoader));
 
     // Simulate escape being pressed from the searchbox with empty input.
diff --git a/chrome/test/data/webui/tab_search/auto_tab_groups_page_test.ts b/chrome/test/data/webui/tab_search/auto_tab_groups_page_test.ts
index 69f9fe99..03acf01c 100644
--- a/chrome/test/data/webui/tab_search/auto_tab_groups_page_test.ts
+++ b/chrome/test/data/webui/tab_search/auto_tab_groups_page_test.ts
@@ -425,6 +425,28 @@
     assertEquals(1, testApiProxy.getCallCount('startTabGroupTutorial'));
   });
 
+  test('Learn more action activates on Enter', async () => {
+    loadTimeData.overrideValues({
+      showTabOrganizationFRE: true,
+    });
+
+    await autoTabGroupsPageSetup();
+
+    assertEquals(0, testApiProxy.getCallCount('openHelpPage'));
+
+    const notStarted = autoTabGroupsPage.shadowRoot!.querySelector(
+        'auto-tab-groups-not-started');
+    assertTrue(!!notStarted);
+    const links = notStarted.shadowRoot!.querySelectorAll<HTMLElement>(
+        '.auto-tab-groups-link');
+    assertEquals(1, links.length);
+    const learnMore = links[0]!;
+    learnMore.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
+    await microtasksFinished();
+
+    assertEquals(1, testApiProxy.getCallCount('openHelpPage'));
+  });
+
   test('Active tab missing from organization shows error', async () => {
     const errorString = 'error';
     const successString = 'success';
diff --git a/chrome/test/interaction/interactive_browser_test.cc b/chrome/test/interaction/interactive_browser_test.cc
index 7c337dd..3672c05 100644
--- a/chrome/test/interaction/interactive_browser_test.cc
+++ b/chrome/test/interaction/interactive_browser_test.cc
@@ -567,7 +567,7 @@
                     .SetMustBeVisibleAtStart(fail_on_close)));
   AddDescriptionPrefix(
       steps, base::StrCat({"WaitForStateChange( ", base::ToString(state_change),
-                           ", ", (expect_timeout ? "true" : "false"), " )"}));
+                           ", ", base::ToString(expect_timeout), " )"}));
   return steps;
 }
 
@@ -777,7 +777,7 @@
   const bool ctrl = modifiers & ui_controls::kControl;
   const bool meta = modifiers & ui_controls::kCommand;
 
-  auto b2s = [](bool b) { return b ? "true" : "false"; };
+  auto b2s = [](bool b) { return base::ToString(b); };
 
   const std::string command = base::StringPrintf(
       R"(
diff --git a/chrome/test/interaction/webcontents_interaction_test_util.cc b/chrome/test/interaction/webcontents_interaction_test_util.cc
index 2768cca6..5abb7c1d 100644
--- a/chrome/test/interaction/webcontents_interaction_test_util.cc
+++ b/chrome/test/interaction/webcontents_interaction_test_util.cc
@@ -24,6 +24,7 @@
 #include "base/scoped_observation.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/to_string.h"
 #include "base/time/time.h"
 #include "base/timer/elapsed_timer.h"
 #include "base/values.h"
@@ -1168,7 +1169,7 @@
   *os << ", test_function: \"" << state_change.test_function << "\""
       << ", where: " << state_change.where << ", event: " << state_change.event
       << ", continue_across_navigation: "
-      << (state_change.continue_across_navigation ? "true" : "false")
+      << base::ToString(state_change.continue_across_navigation)
       << ", timeout: " << state_change.timeout.value_or(base::TimeDelta())
       << ", timeout_event: " << state_change.timeout_event << " }";
 }
diff --git a/chrome/test/user_education/interactive_feature_promo_test.cc b/chrome/test/user_education/interactive_feature_promo_test.cc
index 813ee0a..ecec633 100644
--- a/chrome/test/user_education/interactive_feature_promo_test.cc
+++ b/chrome/test/user_education/interactive_feature_promo_test.cc
@@ -229,7 +229,7 @@
           })
           .SetDescription(base::StringPrintf("CheckPromoIsActive(%s, %s)",
                                              iph_feature.name,
-                                             active ? "true" : "false")));
+                                             base::ToString(active))));
 }
 
 InteractiveFeaturePromoTestApi::MultiStep
diff --git a/chromeos/ash/components/memory/userspace_swap/swap_storage.cc b/chromeos/ash/components/memory/userspace_swap/swap_storage.cc
index 82f27e43..9077df45 100644
--- a/chromeos/ash/components/memory/userspace_swap/swap_storage.cc
+++ b/chromeos/ash/components/memory/userspace_swap/swap_storage.cc
@@ -342,7 +342,7 @@
       compression::GetUncompressedSize(src.AsStringPiece());
   CHECK_EQ(dest.length, uncompressed_size);
 
-  if (!compression::GzipUncompress(src.AsStringPiece(), dest.AsStringPiece())) {
+  if (!compression::GzipUncompress(src.AsStringPiece(), dest.AsSpan<char>())) {
     errno = EIO;
     return -1;
   }
diff --git a/chromeos/ui/frame/frame_header.cc b/chromeos/ui/frame/frame_header.cc
index 2f26708..05de497f 100644
--- a/chromeos/ui/frame/frame_header.cc
+++ b/chromeos/ui/frame/frame_header.cc
@@ -19,6 +19,7 @@
 #include "ui/compositor/layer.h"
 #include "ui/compositor/layer_animation_observer.h"
 #include "ui/compositor/layer_tree_owner.h"
+#include "ui/compositor/layer_type.h"
 #include "ui/compositor/scoped_layer_animation_settings.h"
 #include "ui/gfx/canvas.h"
 #include "ui/gfx/color_utils.h"
@@ -115,7 +116,9 @@
   old_layer->SetTransform(gfx::Transform());
   // Layer in maximized / fullscreen / snapped state is set to
   // opaque, which can prevent resterizing the new layer immediately.
-  old_layer->SetFillsBoundsOpaquely(false);
+  if (old_layer->type() != ui::LAYER_SOLID_COLOR) {
+    old_layer->SetFillsBoundsOpaquely(false);
+  }
 
   layer_owner_ = std::move(old_layer_owner);
 
diff --git a/clank b/clank
index bbbe2cd..ff16da3 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit bbbe2cd7740a857fb0ba49ec0df3bc6131a604e8
+Subproject commit ff16da349347e90984ba1bc206cafccad280e3c6
diff --git a/components/autofill/core/browser/crowdsourcing/determine_possible_field_types.cc b/components/autofill/core/browser/crowdsourcing/determine_possible_field_types.cc
index 3dab59c9..9b8e7df 100644
--- a/components/autofill/core/browser/crowdsourcing/determine_possible_field_types.cc
+++ b/components/autofill/core/browser/crowdsourcing/determine_possible_field_types.cc
@@ -16,6 +16,7 @@
 #include "components/autofill/core/browser/field_type_utils.h"
 #include "components/autofill/core/browser/field_types.h"
 #include "components/autofill/core/browser/form_structure.h"
+#include "components/autofill/core/browser/foundations/autofill_client.h"
 #include "components/autofill/core/common/autofill_features.h"
 #include "components/autofill/core/common/autofill_regex_constants.h"
 #include "components/autofill/core/common/autofill_regexes.h"
diff --git a/components/autofill/core/browser/data_manager/payments/payments_data_manager.cc b/components/autofill/core/browser/data_manager/payments/payments_data_manager.cc
index 4c81d65..72ca2c7 100644
--- a/components/autofill/core/browser/data_manager/payments/payments_data_manager.cc
+++ b/components/autofill/core/browser/data_manager/payments/payments_data_manager.cc
@@ -38,6 +38,7 @@
 #include "components/autofill/core/browser/webdata/autofill_webdata_service.h"
 #include "components/autofill/core/browser/webdata/autofill_webdata_service_observer.h"
 #include "components/autofill/core/common/autofill_clock.h"
+#include "components/autofill/core/common/autofill_constants.h"
 #include "components/autofill/core/common/autofill_payments_features.h"
 #include "components/autofill/core/common/autofill_prefs.h"
 #include "components/autofill/core/common/credit_card_number_validation.h"
diff --git a/components/autofill/core/browser/foundations/autofill_manager.h b/components/autofill/core/browser/foundations/autofill_manager.h
index 95b6a91..f307553 100644
--- a/components/autofill/core/browser/foundations/autofill_manager.h
+++ b/components/autofill/core/browser/foundations/autofill_manager.h
@@ -25,6 +25,8 @@
 #include "build/build_config.h"
 #include "components/autofill/core/browser/autofill_trigger_source.h"
 #include "components/autofill/core/browser/crowdsourcing/autofill_crowdsourcing_manager.h"
+#include "components/autofill/core/browser/filling/form_filler.h"
+#include "components/autofill/core/browser/foundations/autofill_client.h"
 #include "components/autofill/core/browser/foundations/autofill_driver.h"
 #include "components/autofill/core/common/dense_set.h"
 #include "components/autofill/core/common/form_data.h"
diff --git a/components/autofill/core/browser/metrics/form_events/address_form_event_logger.cc b/components/autofill/core/browser/metrics/form_events/address_form_event_logger.cc
index 8557442..acad8f7 100644
--- a/components/autofill/core/browser/metrics/form_events/address_form_event_logger.cc
+++ b/components/autofill/core/browser/metrics/form_events/address_form_event_logger.cc
@@ -16,6 +16,7 @@
 #include "components/autofill/core/browser/autofill_trigger_source.h"
 #include "components/autofill/core/browser/data_quality/autofill_data_util.h"
 #include "components/autofill/core/browser/field_types.h"
+#include "components/autofill/core/browser/foundations/autofill_driver.h"
 #include "components/autofill/core/browser/logging/log_manager.h"
 #include "components/autofill/core/browser/metrics/autofill_metrics_utils.h"
 #include "components/autofill/core/browser/metrics/form_interactions_ukm_logger.h"
diff --git a/components/autofill/core/browser/metrics/log_event.h b/components/autofill/core/browser/metrics/log_event.h
index caaa39b4..cf4ed548c 100644
--- a/components/autofill/core/browser/metrics/log_event.h
+++ b/components/autofill/core/browser/metrics/log_event.h
@@ -7,7 +7,7 @@
 
 #include "base/time/time.h"
 #include "base/types/id_type.h"
-#include "components/autofill/core/browser/filling/form_filler.h"
+#include "components/autofill/core/browser/filling/field_filling_skip_reason.h"
 #include "components/autofill/core/browser/heuristic_source.h"
 #include "components/autofill/core/browser/proto/api_v1.pb.h"
 #include "components/autofill/core/browser/studies/autofill_ablation_study.h"
diff --git a/components/autofill/core/browser/metrics/prediction_quality_metrics.h b/components/autofill/core/browser/metrics/prediction_quality_metrics.h
index e1dc284..b822582 100644
--- a/components/autofill/core/browser/metrics/prediction_quality_metrics.h
+++ b/components/autofill/core/browser/metrics/prediction_quality_metrics.h
@@ -9,6 +9,7 @@
 #include "components/autofill/core/browser/form_parsing/autofill_parsing_utils.h"
 #include "components/autofill/core/browser/form_structure.h"
 #include "components/autofill/core/common/dense_set.h"
+#include "services/metrics/public/cpp/ukm_source_id.h"
 
 namespace autofill::autofill_metrics {
 
diff --git a/components/commerce_strings.grdp b/components/commerce_strings.grdp
index cdcc437a..a48c1253 100644
--- a/components/commerce_strings.grdp
+++ b/components/commerce_strings.grdp
@@ -694,6 +694,9 @@
   <message name="IDS_COMPARE_CONTEXT_MENU_DELETE" desc="The label for the comparison table list dropdown menu item for deleting a comparison table.">
     Delete
   </message>
+  <message name="IDS_COMPARE_NUM_ITEMS_SELECTED" desc="The number of selected items in the comparison table list when editing it.">
+    <ph name="ITEMS">$1<ex>3</ex></ph> selected
+  </message>
   <!-- For Compare Disclosure -->
   <message name="IDS_COMPARE_DISCLOSURE_TITLE" desc="The text for the title of the Compare first-run experience disclosure bubble.">
     Compare similar products side-by-side
diff --git a/components/commerce_strings_grdp/IDS_COMPARE_NUM_ITEMS_SELECTED.png.sha1 b/components/commerce_strings_grdp/IDS_COMPARE_NUM_ITEMS_SELECTED.png.sha1
new file mode 100644
index 0000000..4cc69b51
--- /dev/null
+++ b/components/commerce_strings_grdp/IDS_COMPARE_NUM_ITEMS_SELECTED.png.sha1
@@ -0,0 +1 @@
+277a4f18edd53458b65e1a6bd3179ce5b6b0078e
\ No newline at end of file
diff --git a/components/cronet/android/cronet_combined_impl_native_proguard_golden.cfg b/components/cronet/android/cronet_combined_impl_native_proguard_golden.cfg
index c3794801..74f36dc 100644
--- a/components/cronet/android/cronet_combined_impl_native_proguard_golden.cfg
+++ b/components/cronet/android/cronet_combined_impl_native_proguard_golden.cfg
@@ -237,3 +237,5 @@
 -keepclasseswithmembers class !cr_allowunused,**J.N {
   public long *_HASH;
 }
+# -------- Config Path: obj/third_party/androidx/androidx_annotation_annotation_experimental_java/proguard.txt --------
+# Intentionally empty proguard rules to indicate this library is safe to shrink
diff --git a/components/dbus/xdg/request.cc b/components/dbus/xdg/request.cc
index f369e21..d92ac47 100644
--- a/components/dbus/xdg/request.cc
+++ b/components/dbus/xdg/request.cc
@@ -41,8 +41,13 @@
                  const std::string& method_name,
                  const DbusType& arguments,
                  DbusDictionary&& options,
-                 ResponseCallback callback)
-    : bus_(bus), callback_(std::move(callback)) {
+                 ResponseCallback callback,
+                 const std::string& test_portal_service_name)
+    : bus_(bus),
+      callback_(std::move(callback)),
+      portal_service_name_(test_portal_service_name.empty()
+                               ? kPortalServiceName
+                               : test_portal_service_name) {
   CHECK(bus_);
   CHECK(callback_);
 
@@ -51,7 +56,7 @@
       dbus::ObjectPath(base::nix::XdgDesktopPortalRequestPath(
           bus->GetConnectionName(), handle_token));
   auto* request_proxy =
-      bus->GetObjectProxy(kPortalServiceName, request_object_path_);
+      bus->GetObjectProxy(portal_service_name_, request_object_path_);
 
   // Connect to the "Response" signal before making the method call to avoid a
   // race condition.
@@ -79,17 +84,18 @@
   }
 
   auto* request_proxy =
-      bus_->GetObjectProxy(kPortalServiceName, request_object_path_);
+      bus_->GetObjectProxy(portal_service_name_, request_object_path_);
   dbus::MethodCall method_call(kRequestInterface, "Close");
   request_proxy->CallMethod(
       &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
       base::BindOnce(
           [](scoped_refptr<dbus::Bus> bus, dbus::ObjectPath request_object_path,
-             dbus::Response*) {
-            bus->RemoveObjectProxy(kPortalServiceName, request_object_path,
+             std::string portal_service_name, dbus::Response*) {
+            bus->RemoveObjectProxy(portal_service_name, request_object_path,
                                    base::DoNothing());
           },
-          std::move(bus_), std::move(request_object_path_)));
+          std::move(bus_), std::move(request_object_path_),
+          std::move(portal_service_name_)));
 }
 
 void Request::OnMethodResponse(dbus::Response* response) {
@@ -127,11 +133,11 @@
   // expectation, and reconnect the signal handler to the returned object path
   // if not. In the wild, nearly all xdg-desktop-portal implementations are
   // version 0.9 or later.
-  bus_->RemoveObjectProxy(kPortalServiceName, request_object_path_,
+  bus_->RemoveObjectProxy(portal_service_name_, request_object_path_,
                           base::DoNothing());
   request_object_path_ = new_object_path;
   auto* new_request_proxy =
-      bus_->GetObjectProxy(kPortalServiceName, request_object_path_);
+      bus_->GetObjectProxy(portal_service_name_, request_object_path_);
   new_request_proxy->ConnectToSignal(
       kRequestInterface, kSignalResponse,
       base::BindRepeating(&Request::OnResponseSignal,
@@ -192,7 +198,7 @@
     return;
   }
 
-  bus_->RemoveObjectProxy(kPortalServiceName, request_object_path_,
+  bus_->RemoveObjectProxy(portal_service_name_, request_object_path_,
                           base::DoNothing());
   bus_.reset();
   weak_ptr_factory_.InvalidateWeakPtrs();
diff --git a/components/dbus/xdg/request.h b/components/dbus/xdg/request.h
index ddcb8e7..44545e0 100644
--- a/components/dbus/xdg/request.h
+++ b/components/dbus/xdg/request.h
@@ -40,6 +40,7 @@
   // is of type DbusParameters, or can be any DbusType if there's exactly one
   // argument. The `options` dictionary contains any options except for
   // handle_token which will be set and managed internally.
+  // `test_portal_service_name` may be provided to override in tests.
   // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
   Request(scoped_refptr<dbus::Bus> bus,
           dbus::ObjectProxy* object_proxy,
@@ -47,7 +48,8 @@
           const std::string& method_name,
           const DbusType& arguments,
           DbusDictionary&& options,
-          ResponseCallback callback);
+          ResponseCallback callback,
+          const std::string& test_portal_service_name = std::string());
 
   Request(Request&& other) noexcept = delete;
   Request& operator=(Request&& other) noexcept = delete;
@@ -68,6 +70,7 @@
   scoped_refptr<dbus::Bus> bus_;
   dbus::ObjectPath request_object_path_;
   ResponseCallback callback_;
+  std::string portal_service_name_;
   base::WeakPtrFactory<Request> weak_ptr_factory_{this};
 };
 
diff --git a/components/facilitated_payments/core/metrics/facilitated_payments_metrics.cc b/components/facilitated_payments/core/metrics/facilitated_payments_metrics.cc
index 4dc93216..3c4b35f1 100644
--- a/components/facilitated_payments/core/metrics/facilitated_payments_metrics.cc
+++ b/components/facilitated_payments/core/metrics/facilitated_payments_metrics.cc
@@ -102,7 +102,7 @@
 
 void LogPixFopSelected() {
   // The histogram name should be in sync with
-  // `FacilitatedPaymentsPaymentMethodsMediator.FOP_SELECTOR_USER_ACTION_HISTOGRAM`.
+  // `FacilitatedPaymentsPaymentMethodsMediator.PIX_FOP_SELECTOR_USER_ACTION_HISTOGRAM`.
   base::UmaHistogramEnumeration(
       "FacilitatedPayments.Pix.FopSelector.UserAction",
       FopSelectorAction::kFopSelected);
@@ -110,7 +110,7 @@
 
 void LogEwalletFopSelected(AvailableEwalletsConfiguration type) {
   // The histogram name should be in sync with
-  // `FacilitatedPaymentsPaymentMethodsMediator.FOP_SELECTOR_USER_ACTION_HISTOGRAM`.
+  // `FacilitatedPaymentsPaymentMethodsMediator.EWALLET_FOP_SELECTOR_USER_ACTION_HISTOGRAM`.
   base::UmaHistogramEnumeration(
       base::StrCat({"FacilitatedPayments.Ewallet.FopSelector.UserAction.",
                     AvailableEwalletsConfigurationToString(type)}),
diff --git a/components/facilitated_payments_strings.grdp b/components/facilitated_payments_strings.grdp
index 9f200b1..4fb40b5d 100644
--- a/components/facilitated_payments_strings.grdp
+++ b/components/facilitated_payments_strings.grdp
@@ -74,12 +74,15 @@
   <message name="IDS_PIX_PAYMENT_ADDITIONAL_INFO" desc="Additional info for the bottom sheet displaying user's PIX bank accounts linked to their GPay wallet. The bottom sheet is shown to prompt the user to pay directly on Chrome when a PIX payment page is detected. This string provides supplemental information about the user's accounts and the transaction." formatter_data="android_java">
     To turn off Pix in Chrome, go to your <ph name="BEGIN_LINK1">&lt;link1&gt;</ph>payment settings<ph name="END_LINK1">&lt;/link1&gt;</ph>
   </message>
+  <message name="IDS_EWALLET_PAYMENT_ADDITIONAL_INFO" desc="Additional info for the bottom sheet displaying user's eWallet accounts linked to their GPay wallet. The bottom sheet is shown to prompt the user to pay directly on Chrome when an eWallet payment page is detected. This string provides supplemental information about the user's accounts and the transaction." formatter_data="android_java">
+    Your saved auto-pay method may be used for this payment. To turn off eWallets in Chrome, go to your <ph name="BEGIN_LINK1">&lt;link1&gt;</ph>payment settings<ph name="END_LINK1">&lt;/link1&gt;</ph>
+  </message>
   <message name="IDS_FACILITATED_PAYMENTS_PAYMENT_METHODS_BOTTOM_SHEET_CONTENT_DESCRIPTION" desc="Accessibility string read when the bottom sheet showing a list of the user's payment methods is opened." formatter_data="android_java">
     Payment methods available to be selected for proceeding with the payment. Keyboard hidden.
   </message>
   <message name="IDS_FACILITATED_PAYMENTS_PAYMENT_METHODS_BOTTOM_SHEET_FULL_HEIGHT" desc="Accessibility string read when the bottom sheet showing a list of the user's payment methods is opened at full height. The sheet will occupy the entire screen." formatter_data="android_java">
     Payment methods available to be selected for proceeding with the payment opened at full height.
-    </message>
+  </message>
   <message name="IDS_FACILITATED_PAYMENTS_PAYMENT_METHODS_BOTTOM_SHEET_CLOSED" desc="Accessibility string read when the bottom sheet is closed." formatter_data="android_java">
     The bottom sheet is closed.
   </message>
diff --git a/components/facilitated_payments_strings_grdp/IDS_EWALLET_PAYMENT_ADDITIONAL_INFO.png.sha1 b/components/facilitated_payments_strings_grdp/IDS_EWALLET_PAYMENT_ADDITIONAL_INFO.png.sha1
new file mode 100644
index 0000000..69d8e35
--- /dev/null
+++ b/components/facilitated_payments_strings_grdp/IDS_EWALLET_PAYMENT_ADDITIONAL_INFO.png.sha1
@@ -0,0 +1 @@
+b05eaf4cdc17d44e8fb45cdc01b252a571040859
\ No newline at end of file
diff --git a/components/guest_view/browser/DEPS b/components/guest_view/browser/DEPS
index 37c1a1e..f71f8d1 100644
--- a/components/guest_view/browser/DEPS
+++ b/components/guest_view/browser/DEPS
@@ -8,6 +8,7 @@
   "+third_party/blink/public/common/page/page_zoom.h",
   "+third_party/blink/public/common/input/web_gesture_event.h",
   "+third_party/blink/public/common/input/web_input_event.h",
+  "+third_party/blink/public/mojom/mediastream/media_stream.mojom.h",
 ]
 
 specific_include_rules = {
diff --git a/components/guest_view/browser/guest_view_base.cc b/components/guest_view/browser/guest_view_base.cc
index 7ee7d44..24875b0 100644
--- a/components/guest_view/browser/guest_view_base.cc
+++ b/components/guest_view/browser/guest_view_base.cc
@@ -30,6 +30,7 @@
 #include "content/public/common/content_features.h"
 #include "third_party/blink/public/common/input/web_gesture_event.h"
 #include "third_party/blink/public/common/page/page_zoom.h"
+#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
 
 using content::WebContents;
 
@@ -720,6 +721,21 @@
 
 void GuestViewBase::GuestClose() {}
 
+void GuestViewBase::GuestRequestMediaAccessPermission(
+    const content::MediaStreamRequest& request,
+    content::MediaResponseCallback callback) {
+  std::move(callback).Run(blink::mojom::StreamDevicesSet(),
+                          blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED,
+                          std::unique_ptr<content::MediaStreamUI>());
+}
+
+bool GuestViewBase::GuestCheckMediaAccessPermission(
+    content::RenderFrameHost* render_frame_host,
+    const url::Origin& security_origin,
+    blink::mojom::MediaStreamType type) {
+  return false;
+}
+
 void GuestViewBase::LoadProgressChanged(double progress) {
   if (base::FeatureList::IsEnabled(features::kGuestViewMPArch)) {
     // The load state of the embedder does not affect the load state of the
diff --git a/components/guest_view/browser/guest_view_base.h b/components/guest_view/browser/guest_view_base.h
index 6dc30e6..78fb222 100644
--- a/components/guest_view/browser/guest_view_base.h
+++ b/components/guest_view/browser/guest_view_base.h
@@ -468,6 +468,13 @@
                     base::OnceCallback<void(content::NavigationHandle&)>
                         navigation_handle_callback) override;
   void GuestClose() override;
+  void GuestRequestMediaAccessPermission(
+      const content::MediaStreamRequest& request,
+      content::MediaResponseCallback callback) override;
+  bool GuestCheckMediaAccessPermission(
+      content::RenderFrameHost* render_frame_host,
+      const url::Origin& security_origin,
+      blink::mojom::MediaStreamType type) override;
 
   // WebContentsDelegate implementation.
   void ActivateContents(content::WebContents* contents) final;
diff --git a/components/input/render_input_router_client.h b/components/input/render_input_router_client.h
index 02443a72..9c240dc 100644
--- a/components/input/render_input_router_client.h
+++ b/components/input/render_input_router_client.h
@@ -49,13 +49,14 @@
   // Initiate stylus handwriting.
   virtual void OnStartStylusWriting() = 0;
   // Update which editable element has focus for stylus writing. When
-  // `focus_rect_in_widget` is provided, sets focus and caret position based on
-  // a hit test performed with that rect. Otherwise fallback to the Element
-  // CurrentTouchDownElement() and use default focus caret position. Caret
-  // position is only updated if the target element doesn't already have focus.
+  // `focus_widget_rect_in_dips` is provided, sets focus and caret position
+  // based on a hit test performed with that rect. Otherwise fallback to the
+  // Element CurrentTouchDownElement() and use default focus caret position.
+  // Caret position is only updated if the target element doesn't already have
+  // focus.
   virtual void UpdateElementFocusForStylusWriting(
 #if BUILDFLAG(IS_WIN)
-      const gfx::Rect& focus_rect_in_widget
+      const gfx::Rect& focus_widget_rect_in_dips
 #endif  // BUILDFLAG(IS_WIN)
       ) = 0;
 };
diff --git a/components/omnibox/browser/mock_autocomplete_provider_client.cc b/components/omnibox/browser/mock_autocomplete_provider_client.cc
index 8c52c50..059882d 100644
--- a/components/omnibox/browser/mock_autocomplete_provider_client.cc
+++ b/components/omnibox/browser/mock_autocomplete_provider_client.cc
@@ -20,7 +20,8 @@
   document_suggestions_service_ = std::make_unique<DocumentSuggestionsService>(
       /*identity_manager=*/nullptr, GetURLLoaderFactory());
   remote_suggestions_service_ = std::make_unique<RemoteSuggestionsService>(
-      document_suggestions_service_.get(), GetURLLoaderFactory());
+      document_suggestions_service_.get(),
+      /*search_aggregator_suggestions_service=*/nullptr, GetURLLoaderFactory());
   omnibox_triggered_feature_service_ =
       std::make_unique<OmniboxTriggeredFeatureService>();
   provider_state_service_ = std::make_unique<ProviderStateService>();
diff --git a/components/omnibox/browser/remote_suggestions_service.cc b/components/omnibox/browser/remote_suggestions_service.cc
index 0ddcdfc..0a40b7c 100644
--- a/components/omnibox/browser/remote_suggestions_service.cc
+++ b/components/omnibox/browser/remote_suggestions_service.cc
@@ -12,6 +12,7 @@
 #include "components/lens/proto/server/lens_overlay_response.pb.h"
 #include "components/omnibox/browser/base_search_provider.h"
 #include "components/omnibox/browser/document_suggestions_service.h"
+#include "components/omnibox/browser/search_aggregator_suggestions_service.h"
 #include "components/search/search.h"
 #include "components/search_engines/template_url_service.h"
 #include "components/variations/net/variations_http_headers.h"
@@ -115,8 +116,11 @@
 
 RemoteSuggestionsService::RemoteSuggestionsService(
     DocumentSuggestionsService* document_suggestions_service,
+    SearchAggregatorSuggestionsService* search_aggregator_suggestions_service,
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
     : document_suggestions_service_(document_suggestions_service),
+      search_aggregator_suggestions_service_(
+          search_aggregator_suggestions_service),
       url_loader_factory_(url_loader_factory) {
   DCHECK(url_loader_factory);
 }
@@ -341,6 +345,32 @@
   }
 }
 
+void RemoteSuggestionsService::CreateSearchAggregatorSuggestionsRequest(
+    const GURL& suggest_url,
+    const std::string& request_body,
+    StartCallback start_callback,
+    CompletionCallback completion_callback) {
+  if (!search_aggregator_suggestions_service_) {
+    return;
+  }
+
+  // Create a unique identifier for the request.
+  const base::UnguessableToken request_id = base::UnguessableToken::Create();
+
+  search_aggregator_suggestions_service_
+      ->CreateSearchAggregatorSuggestionsRequest(
+          suggest_url, request_body,
+          base::BindOnce(&RemoteSuggestionsService::OnRequestCreated,
+                         weak_ptr_factory_.GetWeakPtr(), request_id),
+          base::BindOnce(&RemoteSuggestionsService::OnRequestStartedAsync,
+                         weak_ptr_factory_.GetWeakPtr(), request_id,
+                         RemoteRequestType::kEnterpriseSearchAggregatorSuggest,
+                         std::move(start_callback)),
+          base::BindOnce(&RemoteSuggestionsService::OnRequestCompleted,
+                         weak_ptr_factory_.GetWeakPtr(), request_id,
+                         std::move(completion_callback)));
+}
+
 std::unique_ptr<network::SimpleURLLoader>
 RemoteSuggestionsService::StartDeletionRequest(
     const std::string& deletion_url,
diff --git a/components/omnibox/browser/remote_suggestions_service.h b/components/omnibox/browser/remote_suggestions_service.h
index d34393d..b5ea7ada 100644
--- a/components/omnibox/browser/remote_suggestions_service.h
+++ b/components/omnibox/browser/remote_suggestions_service.h
@@ -16,11 +16,13 @@
 #include "base/unguessable_token.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/omnibox/browser/autocomplete_input.h"
+#include "components/omnibox/browser/search_aggregator_suggestions_service.h"
 #include "components/search_engines/template_url.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "url/gurl.h"
 
 class DocumentSuggestionsService;
+class SearchAggregatorSuggestionsService;
 
 namespace network {
 class SharedURLLoaderFactory;
@@ -46,7 +48,9 @@
   kDocumentSuggest = 5,
   // Suggestion deletion requests.
   kDeletion = 6,
-  kMaxValue = kDeletion,
+  // Enterprise Search Aggregator suggestion requests.
+  kEnterpriseSearchAggregatorSuggest = 7,
+  kMaxValue = kEnterpriseSearchAggregatorSuggest,
 };
 
 // The event types recorded by the providers for remote suggestions. Each event
@@ -135,6 +139,7 @@
 
   RemoteSuggestionsService(
       DocumentSuggestionsService* document_suggestions_service,
+      SearchAggregatorSuggestionsService* search_aggregator_suggestions_service,
       scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
   ~RemoteSuggestionsService() override;
   RemoteSuggestionsService(const RemoteSuggestionsService&) = delete;
@@ -192,6 +197,15 @@
   // request.
   void StopCreatingDocumentSuggestionsRequest();
 
+  // Creates and starts an enterprise search aggregator suggestion request using
+  //  `suggest_url` and `response_body` asynchronously after obtaining an OAuth2
+  //  token for signed-in enterprise users.
+  void CreateSearchAggregatorSuggestionsRequest(
+      const GURL& suggest_url,
+      const std::string& request_body,
+      StartCallback start_callback,
+      CompletionCallback completion_callback);
+
   // Creates and returns a loader to delete personalized suggestions.
   //
   // `deletion_url` must be a valid URL.
@@ -238,6 +252,10 @@
   // May be nullptr in OTR profiles. Otherwise guaranteed to outlive this due to
   // the factories' dependency.
   raw_ptr<DocumentSuggestionsService> document_suggestions_service_;
+  // May be nullptr in OTR profiles. Otherwise guaranteed to outlive this due to
+  // the factories' dependency.
+  raw_ptr<SearchAggregatorSuggestionsService>
+      search_aggregator_suggestions_service_;
   scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
   // Observers being notified of request start and completion events.
   base::ObserverList<Observer> observers_;
diff --git a/components/omnibox/browser/remote_suggestions_service_unittest.cc b/components/omnibox/browser/remote_suggestions_service_unittest.cc
index ed57888..60e0f83 100644
--- a/components/omnibox/browser/remote_suggestions_service_unittest.cc
+++ b/components/omnibox/browser/remote_suggestions_service_unittest.cc
@@ -134,8 +134,10 @@
         resource_request = request;
       }));
 
-  RemoteSuggestionsService service(/*document_suggestions_service_=*/nullptr,
-                                   GetUrlLoaderFactory());
+  RemoteSuggestionsService service(
+      /*document_suggestions_service_=*/nullptr,
+      /*search_aggregator_suggestions_service_*/ nullptr,
+      GetUrlLoaderFactory());
 
   TemplateURLRef::SearchTermsArgs search_terms_args;
   search_terms_args.current_page_url = "https://www.google.com/";
@@ -162,8 +164,10 @@
         resource_request = request;
       }));
 
-  RemoteSuggestionsService service(/*document_suggestions_service_=*/nullptr,
-                                   GetUrlLoaderFactory());
+  RemoteSuggestionsService service(
+      /*document_suggestions_service_=*/nullptr,
+      /*search_aggregator_suggestions_service_*/ nullptr,
+      GetUrlLoaderFactory());
 
   TemplateURLRef::SearchTermsArgs search_terms_args;
   search_terms_args.current_page_url = "https://www.google.com/";
@@ -189,8 +193,10 @@
         resource_request = request;
       }));
 
-  RemoteSuggestionsService service(/*document_suggestions_service_=*/nullptr,
-                                   GetUrlLoaderFactory());
+  RemoteSuggestionsService service(
+      /*document_suggestions_service_=*/nullptr,
+      /*search_aggregator_suggestions_service_*/ nullptr,
+      GetUrlLoaderFactory());
   auto loader = service.StartDeletionRequest(
       "https://google.com/complete/delete",
       /*is_off_the_record=*/false, base::DoNothing());
@@ -209,8 +215,10 @@
         resource_request = request;
       }));
 
-  RemoteSuggestionsService service(/*document_suggestions_service_=*/nullptr,
-                                   GetUrlLoaderFactory());
+  RemoteSuggestionsService service(
+      /*document_suggestions_service_=*/nullptr,
+      /*search_aggregator_suggestions_service_*/ nullptr,
+      GetUrlLoaderFactory());
 
   TemplateURLRef::SearchTermsArgs search_terms_args;
   search_terms_args.current_page_url = "https://www.google.com/";
@@ -241,8 +249,10 @@
       template_url_service().Add(
           std::make_unique<TemplateURL>(template_url_data)));
 
-  RemoteSuggestionsService service(/*document_suggestions_service_=*/nullptr,
-                                   GetUrlLoaderFactory());
+  RemoteSuggestionsService service(
+      /*document_suggestions_service_=*/nullptr,
+      /*search_aggregator_suggestions_service_*/ nullptr,
+      GetUrlLoaderFactory());
   TestObserver observer(&service);
   auto loader = service.StartZeroPrefixSuggestionsRequest(
       RemoteRequestType::kZeroSuggest, /*is_off_the_record=*/false,
@@ -288,8 +298,10 @@
       template_url_service().Add(
           std::make_unique<TemplateURL>(template_url_data)));
 
-  RemoteSuggestionsService service(/*document_suggestions_service_=*/nullptr,
-                                   GetUrlLoaderFactory());
+  RemoteSuggestionsService service(
+      /*document_suggestions_service_=*/nullptr,
+      /*search_aggregator_suggestions_service_*/ nullptr,
+      GetUrlLoaderFactory());
 
   // Set up a delegate that will be replaced.
   MockDelegate delegate1(&service);
diff --git a/components/omnibox/browser/search_aggregator_suggestions_service.cc b/components/omnibox/browser/search_aggregator_suggestions_service.cc
index faf38fe..79f06c9 100644
--- a/components/omnibox/browser/search_aggregator_suggestions_service.cc
+++ b/components/omnibox/browser/search_aggregator_suggestions_service.cc
@@ -5,21 +5,18 @@
 #include "components/omnibox/browser/search_aggregator_suggestions_service.h"
 
 #include <memory>
+#include <string>
 #include <utility>
 
 #include "base/functional/bind.h"
-#include "base/i18n/rtl.h"
-#include "base/json/json_writer.h"
-#include "base/metrics/field_trial_params.h"
-#include "base/strings/stringprintf.h"
-#include "base/values.h"
-#include "components/omnibox/browser/document_provider.h"
 #include "components/variations/net/variations_http_headers.h"
 #include "net/base/load_flags.h"
+#include "net/cookies/site_for_cookies.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 #include "services/network/public/cpp/simple_url_loader.h"
+#include "url/gurl.h"
 
 SearchAggregatorSuggestionsService::SearchAggregatorSuggestionsService(
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
diff --git a/components/omnibox/browser/search_aggregator_suggestions_service.h b/components/omnibox/browser/search_aggregator_suggestions_service.h
index 39071b6..974e99d5 100644
--- a/components/omnibox/browser/search_aggregator_suggestions_service.h
+++ b/components/omnibox/browser/search_aggregator_suggestions_service.h
@@ -8,15 +8,20 @@
 #include <memory>
 #include <string>
 
+#include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
-#include "base/scoped_observation.h"
 #include "components/keyed_service/core/keyed_service.h"
-#include "services/network/public/cpp/resource_request.h"
-#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/network/public/cpp/simple_url_loader.h"
 #include "url/gurl.h"
 
+namespace network {
+struct ResourceRequest;
+class SharedURLLoaderFactory;
+class SimpleURLLoader;
+}  // namespace network
+
 // A service to fetch suggestions from the search aggregator endpoint URL.
 class SearchAggregatorSuggestionsService : public KeyedService {
  public:
diff --git a/components/omnibox/browser/search_aggregator_suggestions_service_unittest.cc b/components/omnibox/browser/search_aggregator_suggestions_service_unittest.cc
index f1e1249..d00865b 100644
--- a/components/omnibox/browser/search_aggregator_suggestions_service_unittest.cc
+++ b/components/omnibox/browser/search_aggregator_suggestions_service_unittest.cc
@@ -76,7 +76,6 @@
         shared_url_loader_factory_(
             base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                 &test_url_loader_factory_)),
-        identity_test_env_(&test_url_loader_factory_, &prefs_),
         search_aggregator_suggestions_service_(
             new SearchAggregatorSuggestionsService(
                 shared_url_loader_factory_)) {
diff --git a/components/optimization_guide/content/browser/page_content_proto_provider.cc b/components/optimization_guide/content/browser/page_content_proto_provider.cc
index 48734e5c..f0a02542 100644
--- a/components/optimization_guide/content/browser/page_content_proto_provider.cc
+++ b/components/optimization_guide/content/browser/page_content_proto_provider.cc
@@ -12,7 +12,6 @@
 #include "mojo/public/cpp/bindings/callback_helpers.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "services/service_manager/public/cpp/interface_provider.h"
-#include "third_party/blink/public/mojom/content_extraction/ai_page_content.mojom.h"
 
 namespace optimization_guide {
 
@@ -82,7 +81,16 @@
 
 }  // namespace
 
+blink::mojom::AIPageContentOptionsPtr DefaultAIPageContentOptions() {
+  auto request = blink::mojom::AIPageContentOptions::New();
+  request->include_geometry = true;
+  request->on_critical_path = true;
+
+  return request;
+}
+
 void GetAIPageContent(content::WebContents* web_contents,
+                      blink::mojom::AIPageContentOptionsPtr request,
                       OnAIPageContentDone done_callback) {
   DCHECK(web_contents);
   DCHECK(web_contents->GetPrimaryMainFrame());
@@ -108,11 +116,14 @@
         rfh->GetRemoteInterfaces()->GetInterface(
             agent.BindNewPipeAndPassReceiver());
         auto* agent_ptr = agent.get();
-        agent_ptr->GetAIPageContent(mojo::WrapCallbackWithDefaultInvokeIfNotRun(
-            base::BindOnce(&OnGotAIPageContentForFrame,
-                           rfh->GetGlobalFrameToken(), std::move(agent),
-                           page_content_map.get(), concurrent.CreateClosure()),
-            nullptr));
+        agent_ptr->GetAIPageContent(
+            request.Clone(),
+            mojo::WrapCallbackWithDefaultInvokeIfNotRun(
+                base::BindOnce(&OnGotAIPageContentForFrame,
+                               rfh->GetGlobalFrameToken(), std::move(agent),
+                               page_content_map.get(),
+                               concurrent.CreateClosure()),
+                nullptr));
       });
 
   std::move(concurrent)
diff --git a/components/optimization_guide/content/browser/page_content_proto_provider.h b/components/optimization_guide/content/browser/page_content_proto_provider.h
index 7e3120d8..a03db290 100644
--- a/components/optimization_guide/content/browser/page_content_proto_provider.h
+++ b/components/optimization_guide/content/browser/page_content_proto_provider.h
@@ -7,18 +7,21 @@
 
 #include "base/functional/callback.h"
 #include "components/optimization_guide/proto/features/model_prototyping.pb.h"
+#include "third_party/blink/public/mojom/content_extraction/ai_page_content.mojom.h"
 
 namespace content {
 class WebContents;
 }
 
 namespace optimization_guide {
+blink::mojom::AIPageContentOptionsPtr DefaultAIPageContentOptions();
 
 // Provides AIPageContent representation for the primary page displayed in a
 // WebContents.
 using OnAIPageContentDone = base::OnceCallback<void(
     std::optional<optimization_guide::proto::AnnotatedPageContent>)>;
 void GetAIPageContent(content::WebContents* web_contents,
+                      blink::mojom::AIPageContentOptionsPtr request,
                       OnAIPageContentDone done_callback);
 
 }  // namespace optimization_guide
diff --git a/components/optimization_guide/content/browser/page_content_proto_provider_browsertest.cc b/components/optimization_guide/content/browser/page_content_proto_provider_browsertest.cc
index 1b4d221..425c472e 100644
--- a/components/optimization_guide/content/browser/page_content_proto_provider_browsertest.cc
+++ b/components/optimization_guide/content/browser/page_content_proto_provider_browsertest.cc
@@ -105,10 +105,11 @@
 
   const proto::AnnotatedPageContent& page_content() { return *page_content_; }
 
-  void LoadData() {
+  void LoadData(blink::mojom::AIPageContentOptionsPtr request =
+                    DefaultAIPageContentOptions()) {
     base::RunLoop run_loop;
     GetAIPageContent(
-        web_contents(),
+        web_contents(), std::move(request),
         base::BindOnce(&PageContentProtoProviderBrowserTest::SetPageContent,
                        base::Unretained(this), run_loop.QuitClosure()));
     run_loop.Run();
@@ -163,6 +164,34 @@
 }
 
 IN_PROC_BROWSER_TEST_F(PageContentProtoProviderBrowserTest,
+                       AIPageContentNoGeometry) {
+  LoadPage(https_server()->GetURL("/simple.html"),
+           /* with_page_content = */ false);
+
+  auto request = blink::mojom::AIPageContentOptions::New();
+  request->include_geometry = false;
+  LoadData(std::move(request));
+
+  EXPECT_EQ(page_content().root_node().children_nodes().size(), 1);
+  AssertHasText(page_content().root_node(), "Non empty simple page\n\n");
+  EXPECT_FALSE(page_content().root_node().content_attributes().has_geometry());
+}
+
+IN_PROC_BROWSER_TEST_F(PageContentProtoProviderBrowserTest,
+                       AIPageContentNoCriticalPath) {
+  LoadPage(https_server()->GetURL("/simple.html"),
+           /* with_page_content = */ false);
+
+  auto request = blink::mojom::AIPageContentOptions::New();
+  request->on_critical_path = false;
+  LoadData(std::move(request));
+
+  EXPECT_EQ(page_content().root_node().children_nodes().size(), 1);
+  AssertHasText(page_content().root_node(), "Non empty simple page\n\n");
+  EXPECT_TRUE(page_content().root_node().content_attributes().has_geometry());
+}
+
+IN_PROC_BROWSER_TEST_F(PageContentProtoProviderBrowserTest,
                        AIPageContentImageDataURL) {
   LoadPage(https_server()->GetURL("a.com", "/data_image.html"));
 
diff --git a/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc b/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc
index f4d7b48..dc46da9 100644
--- a/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc
+++ b/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc
@@ -15,9 +15,12 @@
 #include "base/logging.h"
 #include "base/memory/ref_counted_memory.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/nix/xdg_util.h"
 #include "base/no_destructor.h"
+#include "base/notreached.h"
 #include "base/rand_util.h"
+#include "base/strings/string_util.h"
 #include "components/dbus/thread_linux/dbus_thread_linux.h"
 #include "components/os_crypt/async/common/algorithm.mojom.h"
 #include "crypto/encryptor.h"
@@ -29,6 +32,11 @@
 
 namespace {
 
+constexpr char kUmaInitStatus[] =
+    "OSCrypt.FreedesktopSecretKeyProvider.InitStatus";
+constexpr char kUmaErrorDetail[] =
+    "OSCrypt.FreedesktopSecretKeyProvider.$1.ErrorDetail";
+
 // These constants are duplicated from the sync backend.
 constexpr char kEncryptionTag[] = "v11";
 constexpr char kSalt[] = "saltysalt";
@@ -36,11 +44,14 @@
 constexpr size_t kEncryptionIterations = 1;
 
 template <typename ReplyArgs>
-void CallMethod(dbus::ObjectProxy* object_proxy,
-                const std::string& interface_name,
-                const std::string& method_name,
-                const DbusType& arguments,
-                base::OnceCallback<void(std::optional<ReplyArgs>)> callback) {
+void CallMethod(
+    dbus::ObjectProxy* object_proxy,
+    const std::string& interface_name,
+    const std::string& method_name,
+    const DbusType& arguments,
+    base::OnceCallback<void(
+        base::expected<ReplyArgs, FreedesktopSecretKeyProvider::ErrorDetail>)>
+        callback) {
   dbus::MethodCall method_call(interface_name, method_name);
   dbus::MessageWriter writer(&method_call);
   arguments.Write(&writer);
@@ -48,10 +59,15 @@
       &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
       base::BindOnce(
           [](const std::string& interface_name, const std::string& method_name,
-             base::OnceCallback<void(std::optional<ReplyArgs>)> callback,
+             base::OnceCallback<void(
+                 base::expected<ReplyArgs,
+                                FreedesktopSecretKeyProvider::ErrorDetail>)>
+                 callback,
              dbus::Response* response) {
+            using ErrorDetail = FreedesktopSecretKeyProvider::ErrorDetail;
             if (!response) {
-              std::move(callback).Run(std::nullopt);
+              std::move(callback).Run(
+                  base::unexpected(ErrorDetail::kNoResponse));
               return;
             }
             dbus::MessageReader reader(response);
@@ -61,7 +77,8 @@
                          << method_name << ": expected type "
                          << ReplyArgs::GetSignature() << " but got type "
                          << response->GetSignature();
-              std::move(callback).Run(std::nullopt);
+              std::move(callback).Run(
+                  base::unexpected(ErrorDetail::kInvalidReplyFormat));
               return;
             }
             std::move(callback).Run(std::move(reply));
@@ -77,6 +94,35 @@
   return base::MakeRefCounted<dbus::Bus>(options);
 }
 
+const char* InitStatusToString(
+    FreedesktopSecretKeyProvider::InitStatus status) {
+  switch (status) {
+    case FreedesktopSecretKeyProvider::InitStatus::kSuccess:
+      return "Success";
+    case FreedesktopSecretKeyProvider::InitStatus::kCreateCollectionFailed:
+      return "CreateCollectionFailed";
+    case FreedesktopSecretKeyProvider::InitStatus::kCreateItemFailed:
+      return "CreateItemFailed";
+    case FreedesktopSecretKeyProvider::InitStatus::kEmptySecret:
+      return "EmptySecret";
+    case FreedesktopSecretKeyProvider::InitStatus::kGetSecretFailed:
+      return "GetSecretFailed";
+    case FreedesktopSecretKeyProvider::InitStatus::kGnomeKeyringDeadlock:
+      return "GnomeKeyringDeadlock";
+    case FreedesktopSecretKeyProvider::InitStatus::kNoService:
+      return "NoService";
+    case FreedesktopSecretKeyProvider::InitStatus::kReadAliasFailed:
+      return "ReadAliasFailed";
+    case FreedesktopSecretKeyProvider::InitStatus::kSearchItemsFailed:
+      return "SearchItemsFailed";
+    case FreedesktopSecretKeyProvider::InitStatus::kSessionFailure:
+      return "SessionFailure";
+    case FreedesktopSecretKeyProvider::InitStatus::kUnlockFailed:
+      return "UnlockFailed";
+  }
+  NOTREACHED();
+}
+
 }  // namespace
 
 FreedesktopSecretKeyProvider::FreedesktopSecretKeyProvider(
@@ -123,7 +169,7 @@
     std::optional<bool> service_started) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!service_started.value_or(false)) {
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kNoService, ErrorDetail::kNone);
     return;
   }
 
@@ -136,22 +182,76 @@
 }
 
 void FreedesktopSecretKeyProvider::OnReadAliasDefault(
-    std::optional<DbusObjectPath> collection_path) {
+    base::expected<DbusObjectPath, ErrorDetail> collection_path) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!collection_path.has_value()) {
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kReadAliasFailed, collection_path.error());
     return;
   }
   if (collection_path->value().value() != "/") {
     default_collection_proxy_ =
         bus_->GetObjectProxy(kSecretServiceName, collection_path->value());
-    OpenSession();
+    CallMethod(default_collection_proxy_, kPropertiesInterface, kMethodGet,
+               MakeDbusParameters(DbusString(kSecretCollectionInterface),
+                                  DbusString(kLabelProperty)),
+               base::BindOnce(
+                   &FreedesktopSecretKeyProvider::OnGetCollectionLabelResponse,
+                   weak_ptr_factory_.GetWeakPtr()));
   } else {
     NOTIMPLEMENTED();
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kCreateCollectionFailed, ErrorDetail::kNone);
   }
 }
 
+void FreedesktopSecretKeyProvider::OnGetCollectionLabelResponse(
+    base::expected<DbusVariant, ErrorDetail> variant) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (!variant.has_value()) {
+    LOG(ERROR) << "Get(Label) failed.";
+    FinalizeFailure(InitStatus::kGnomeKeyringDeadlock, variant.error());
+    return;
+  }
+
+  const DbusString* label_variant = variant->GetAs<DbusString>();
+  if (!label_variant) {
+    LOG(ERROR) << "Label property missing or invalid.";
+    FinalizeFailure(InitStatus::kGnomeKeyringDeadlock,
+                    ErrorDetail::kInvalidVariantFormat);
+    return;
+  }
+
+  // Label property read successfully
+  UnlockDefaultCollection();
+}
+
+void FreedesktopSecretKeyProvider::UnlockDefaultCollection() {
+  auto* service_proxy = bus_->GetObjectProxy(
+      kSecretServiceName, dbus::ObjectPath(kSecretServicePath));
+
+  auto objects =
+      MakeDbusArray(DbusObjectPath(default_collection_proxy_->object_path()));
+  CallMethod(service_proxy, kSecretServiceInterface, kMethodUnlock, objects,
+             base::BindOnce(&FreedesktopSecretKeyProvider::OnUnlock,
+                            weak_ptr_factory_.GetWeakPtr()));
+}
+
+void FreedesktopSecretKeyProvider::OnUnlock(
+    base::expected<DbusParameters<DbusArray<DbusObjectPath>, DbusObjectPath>,
+                   ErrorDetail> unlocked_collection) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (!unlocked_collection.has_value()) {
+    FinalizeFailure(InitStatus::kUnlockFailed, unlocked_collection.error());
+    return;
+  }
+  const auto& [collection_paths, _] = unlocked_collection->value();
+  if (collection_paths.value().empty()) {
+    FinalizeFailure(InitStatus::kUnlockFailed, ErrorDetail::kEmptyObjectPaths);
+    return;
+  }
+  // Unlocked now
+  OpenSession();
+}
+
 void FreedesktopSecretKeyProvider::OpenSession() {
   auto* service_proxy = bus_->GetObjectProxy(
       kSecretServiceName, dbus::ObjectPath(kSecretServicePath));
@@ -163,10 +263,11 @@
 }
 
 void FreedesktopSecretKeyProvider::OnOpenSession(
-    std::optional<DbusParameters<DbusVariant, DbusObjectPath>> session_reply) {
+    base::expected<DbusParameters<DbusVariant, DbusObjectPath>, ErrorDetail>
+        session_reply) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!session_reply.has_value()) {
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kSessionFailure, session_reply.error());
     return;
   }
   const auto& [_, result] = session_reply->value();
@@ -183,16 +284,16 @@
 }
 
 void FreedesktopSecretKeyProvider::OnSearchItems(
-    std::optional<DbusArray<DbusObjectPath>> results) {
+    base::expected<DbusArray<DbusObjectPath>, ErrorDetail> results) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!results.has_value()) {
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kSearchItemsFailed, results.error());
     return;
   }
 
   if (results->value().empty()) {
     NOTIMPLEMENTED();
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kCreateItemFailed, ErrorDetail::kNone);
     return;
   }
 
@@ -205,10 +306,10 @@
 }
 
 void FreedesktopSecretKeyProvider::OnGetSecret(
-    std::optional<DbusSecret> secret_reply) {
+    base::expected<DbusSecret, ErrorDetail> secret_reply) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!secret_reply.has_value()) {
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kGetSecretFailed, secret_reply.error());
     return;
   }
 
@@ -216,12 +317,13 @@
       secret_reply->value();
   const auto& secret_bytes = value.value();
   if (!secret_bytes) {
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kGetSecretFailed,
+                    ErrorDetail::kInvalidVariantFormat);
     return;
   }
   if (secret_bytes->size() == 0) {
     LOG(ERROR) << "GetSecret returned an empty secret.";
-    FinalizeFailure();
+    FinalizeFailure(InitStatus::kEmptySecret, ErrorDetail::kNone);
     return;
   }
 
@@ -242,24 +344,42 @@
 
 void FreedesktopSecretKeyProvider::FinalizeSuccess(Encryptor::Key key) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  RecordInitStatus(InitStatus::kSuccess, ErrorDetail::kNone);
   std::move(key_callback_).Run(kEncryptionTag, std::move(key));
   CloseSession();
 }
 
-void FreedesktopSecretKeyProvider::FinalizeFailure() {
+void FreedesktopSecretKeyProvider::FinalizeFailure(InitStatus status,
+                                                   ErrorDetail detail) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!key_callback_) {
     return;
   }
+  RecordInitStatus(status, detail);
   std::move(key_callback_).Run(std::string(), std::nullopt);
   CloseSession();
 }
 
+void FreedesktopSecretKeyProvider::RecordInitStatus(InitStatus status,
+                                                    ErrorDetail detail) {
+  // Log the high-level InitStatus.
+  base::UmaHistogramEnumeration(kUmaInitStatus, status);
+
+  // If there was an error, also log the error detail.
+  if (status != InitStatus::kSuccess) {
+    auto histogram_name = base::ReplaceStringPlaceholders(
+        kUmaErrorDetail, std::vector<std::string>{InitStatusToString(status)},
+        nullptr);
+    base::UmaHistogramEnumeration(histogram_name, detail);
+  }
+}
+
 void FreedesktopSecretKeyProvider::CloseSession() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (session_opened_) {
     CallMethod(session_proxy_, kSecretSessionInterface, kMethodClose,
-               DbusVoid(), base::BindOnce([](std::optional<DbusVoid>) {}));
+               DbusVoid(),
+               base::BindOnce([](base::expected<DbusVoid, ErrorDetail>) {}));
   }
 }
 
diff --git a/components/os_crypt/async/browser/freedesktop_secret_key_provider.h b/components/os_crypt/async/browser/freedesktop_secret_key_provider.h
index aad3ebc..da2c899 100644
--- a/components/os_crypt/async/browser/freedesktop_secret_key_provider.h
+++ b/components/os_crypt/async/browser/freedesktop_secret_key_provider.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <memory>
-#include <optional>
 #include <string>
 #include <vector>
 
@@ -16,6 +15,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/sequence_checker.h"
+#include "base/types/expected.h"
 #include "build/branding_buildflags.h"
 #include "components/dbus/properties/types.h"
 #include "components/dbus/utils/check_for_service_and_start.h"
@@ -33,6 +33,35 @@
 // which can then be used to encrypt confidential data.
 class FreedesktopSecretKeyProvider : public KeyProvider {
  public:
+  enum class InitStatus {
+    // These values are persisted to logs. Do not renumber or reuse.
+    kSuccess = 0,
+    kCreateCollectionFailed = 1,
+    kCreateItemFailed = 2,
+    kEmptySecret = 3,
+    kGetSecretFailed = 4,
+    kGnomeKeyringDeadlock = 5,
+    kNoService = 6,
+    kReadAliasFailed = 7,
+    kSearchItemsFailed = 8,
+    kSessionFailure = 9,
+    kUnlockFailed = 10,
+    kMaxValue = kUnlockFailed,
+  };
+
+  // Supplements InitStatus in case of errors.
+  enum class ErrorDetail {
+    // These values are persisted to logs. Do not renumber or reuse.
+    kNone = 0,
+    kDestructedBeforeComplete = 1,
+    kEmptyObjectPaths = 2,
+    kInvalidReplyFormat = 3,
+    kInvalidSignalFormat = 4,
+    kInvalidVariantFormat = 5,
+    kNoResponse = 6,
+    kMaxValue = kNoResponse,
+  };
+
   FreedesktopSecretKeyProvider(bool use_for_encryption,
                                const std::string& product_name,
                                scoped_refptr<dbus::Bus> bus);
@@ -65,6 +94,7 @@
   static constexpr char kMethodReadAlias[] = "ReadAlias";
   static constexpr char kMethodGetSecret[] = "GetSecret";
   static constexpr char kMethodOpenSession[] = "OpenSession";
+  static constexpr char kMethodUnlock[] = "Unlock";
   static constexpr char kMethodClose[] = "Close";
   static constexpr char kMethodSearchItems[] = "SearchItems";
   static constexpr char kPropertiesInterface[] =
@@ -99,16 +129,25 @@
 #endif
 
   void OnServiceStarted(std::optional<bool> service_started);
-  void OnReadAliasDefault(std::optional<DbusObjectPath> collection_path);
-  void OnOpenSession(
-      std::optional<DbusParameters<DbusVariant, DbusObjectPath>> session_reply);
-  void OnSearchItems(std::optional<DbusArray<DbusObjectPath>> results);
-  void OnGetSecret(std::optional<DbusSecret> secret_reply);
+  void OnReadAliasDefault(
+      base::expected<DbusObjectPath, ErrorDetail> collection_path);
+  void OnGetCollectionLabelResponse(
+      base::expected<DbusVariant, ErrorDetail> variant);
+  void OnUnlock(
+      base::expected<DbusParameters<DbusArray<DbusObjectPath>, DbusObjectPath>,
+                     ErrorDetail> unlocked_collection);
+  void OnOpenSession(base::expected<DbusParameters<DbusVariant, DbusObjectPath>,
+                                    ErrorDetail> session_reply);
+  void OnSearchItems(
+      base::expected<DbusArray<DbusObjectPath>, ErrorDetail> results);
+  void OnGetSecret(base::expected<DbusSecret, ErrorDetail> secret_reply);
 
+  void UnlockDefaultCollection();
   void OpenSession();
   void DeriveKeyFromSecret(base::span<const uint8_t> secret);
   void FinalizeSuccess(Encryptor::Key key);
-  void FinalizeFailure();
+  void FinalizeFailure(InitStatus status, ErrorDetail detail);
+  void RecordInitStatus(InitStatus status, ErrorDetail detail);
   void CloseSession();
 
   raw_ptr<dbus::ObjectProxy> default_collection_proxy_ = nullptr;
diff --git a/components/os_crypt/async/browser/freedesktop_secret_key_provider_unittest.cc b/components/os_crypt/async/browser/freedesktop_secret_key_provider_unittest.cc
index 0568d22..ad03527 100644
--- a/components/os_crypt/async/browser/freedesktop_secret_key_provider_unittest.cc
+++ b/components/os_crypt/async/browser/freedesktop_secret_key_provider_unittest.cc
@@ -161,6 +161,30 @@
            _))
       .WillOnce(RespondWith(DbusObjectPath(dbus::ObjectPath(kCollectionPath))));
 
+  // Get(Label)
+  EXPECT_CALL(
+      *mock_collection_proxy,
+      Call(FreedesktopSecretKeyProvider::kPropertiesInterface,
+           FreedesktopSecretKeyProvider::kMethodGet,
+           MatchArgs(MakeDbusParameters(
+               DbusString(
+                   FreedesktopSecretKeyProvider::kSecretCollectionInterface),
+               DbusString(FreedesktopSecretKeyProvider::kLabelProperty))),
+           _))
+      .WillOnce(RespondWith(MakeDbusVariant(
+          DbusString(FreedesktopSecretKeyProvider::kDefaultCollectionLabel))));
+
+  // Unlock(default_collection)
+  EXPECT_CALL(*mock_service_proxy,
+              Call(FreedesktopSecretKeyProvider::kSecretServiceInterface,
+                   FreedesktopSecretKeyProvider::kMethodUnlock,
+                   MatchArgs(MakeDbusArray(
+                       DbusObjectPath(dbus::ObjectPath(kCollectionPath)))),
+                   _))
+      .WillOnce(RespondWith(MakeDbusParameters(
+          MakeDbusArray(DbusObjectPath(dbus::ObjectPath(kCollectionPath))),
+          DbusObjectPath(dbus::ObjectPath("/")))));
+
   // OpenSession
   EXPECT_CALL(
       *mock_service_proxy,
diff --git a/components/os_crypt/async/browser/secret_portal_key_provider.cc b/components/os_crypt/async/browser/secret_portal_key_provider.cc
index 075a4b7f..818f4e3 100644
--- a/components/os_crypt/async/browser/secret_portal_key_provider.cc
+++ b/components/os_crypt/async/browser/secret_portal_key_provider.cc
@@ -132,15 +132,8 @@
       bus_, secret_proxy, kInterfaceSecret, kMethodRetrieveSecret,
       DbusUnixFd(std::move(write_fd)), std::move(options),
       base::BindOnce(&SecretPortalKeyProvider::OnRetrieveSecret,
-                     weak_ptr_factory_.GetWeakPtr()));
-
-  // Read the secret from the pipe.  This must happen asynchronously because the
-  // file may not become readable until the keyring is unlocked by typing a
-  // password.
-  read_watcher_ = base::FileDescriptorWatcher::WatchReadable(
-      read_fd_.get(),
-      base::BindRepeating(&SecretPortalKeyProvider::OnFdReadable,
-                          weak_ptr_factory_.GetWeakPtr()));
+                     weak_ptr_factory_.GetWeakPtr()),
+      GetSecretServiceName());
 }
 
 void SecretPortalKeyProvider::OnSignalConnected(
@@ -178,6 +171,14 @@
     }
   }
 
+  // Read the secret from the pipe.  This must happen asynchronously because the
+  // file may not become readable until the keyring is unlocked by typing a
+  // password.
+  read_watcher_ = base::FileDescriptorWatcher::WatchReadable(
+      read_fd_.get(),
+      base::BindRepeating(&SecretPortalKeyProvider::OnFdReadable,
+                          weak_ptr_factory_.GetWeakPtr()));
+
   // Though it is documented in the spec, xdg-desktop-portal does not currently
   // implement returning a token.
   auto* token = results->GetAs<DbusString>("token");
diff --git a/components/os_crypt/async/browser/test_secret_portal.cc b/components/os_crypt/async/browser/test_secret_portal.cc
index c545b9b..6433e4ef 100644
--- a/components/os_crypt/async/browser/test_secret_portal.cc
+++ b/components/os_crypt/async/browser/test_secret_portal.cc
@@ -46,7 +46,7 @@
   base::ScopedFD write_fd;
   EXPECT_TRUE(reader.PopFileDescriptor(&write_fd));
 
-  base::WriteFileDescriptor(write_fd.get(), "secret");
+  EXPECT_TRUE(base::WriteFileDescriptor(write_fd.get(), "secret"));
   write_fd.reset();
 
   DbusDictionary options;
diff --git a/components/os_crypt/sync/os_crypt_posix.cc b/components/os_crypt/sync/os_crypt_posix.cc
index 10030a2..69dcfd7 100644
--- a/components/os_crypt/sync/os_crypt_posix.cc
+++ b/components/os_crypt/sync/os_crypt_posix.cc
@@ -13,49 +13,28 @@
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "components/os_crypt/sync/os_crypt_metrics.h"
-#include "crypto/encryptor.h"
-#include "crypto/symmetric_key.h"
+#include "crypto/aes_cbc.h"
 
 namespace {
 
-// Salt for Symmetric key derivation.
-constexpr char kSalt[] = "saltysalt";
+// clang-format off
+// PBKDF2-HMAC-SHA1(1 iteration, key = "peanuts", salt = "saltysalt")
+constexpr auto kV10Key = std::to_array<uint8_t>({
+    0xfd, 0x62, 0x1f, 0xe5, 0xa2, 0xb4, 0x02, 0x53,
+    0x9d, 0xfa, 0x14, 0x7c, 0xa9, 0x27, 0x27, 0x78,
+});
 
-// Key size required for 128 bit AES.
-constexpr size_t kDerivedKeySizeInBits = 128;
-
-// Constant for Symmetric key derivation.
-constexpr size_t kEncryptionIterations = 1;
-
-// Size of initialization vector for AES 128-bit.
-constexpr size_t kIVBlockSizeAES128 = 16;
+constexpr std::array<uint8_t, crypto::aes_cbc::kBlockSize> kIv{
+    ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
+    ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
+};
+// clang-format on
 
 // Prefix for cypher text returned by obfuscation version.  We prefix the
 // cyphertext with this string so that future data migration can detect
 // this and migrate to full encryption without data loss.
 constexpr char kObfuscationPrefix[] = "v10";
 
-// Generates a newly allocated SymmetricKey object based a hard-coded password.
-// Ownership of the key is passed to the caller.  Returns NULL key if a key
-// generation error occurs.
-crypto::SymmetricKey* GetEncryptionKey() {
-  // We currently "obfuscate" by encrypting and decrypting with hard-coded
-  // password.  We need to improve this password situation by moving a secure
-  // password into a system-level key store.
-  // http://crbug.com/25404 and http://crbug.com/49115
-  const std::string password = "peanuts";
-  const std::string salt(kSalt);
-
-  // Create an encryption key from our password and salt.
-  std::unique_ptr<crypto::SymmetricKey> encryption_key(
-      crypto::SymmetricKey::DeriveKeyFromPasswordUsingPbkdf2(
-          crypto::SymmetricKey::AES, password, salt, kEncryptionIterations,
-          kDerivedKeySizeInBits));
-  DCHECK(encryption_key.get());
-
-  return encryption_key.release();
-}
-
 }  // namespace
 
 bool OSCrypt::EncryptString16(const std::u16string& plaintext,
@@ -73,52 +52,35 @@
   return true;
 }
 
+// This is an obfuscation layer that does not provide any genuine
+// confidentiality. It is used on OSes that already provide protection for data
+// at rest some other way (Android, CrOS, and Fuchsia) or where there's no
+// implementation available of platform secret store integration in Chromium
+// (FreeBSD, others).
 bool OSCrypt::EncryptString(const std::string& plaintext,
                             std::string* ciphertext) {
-  // This currently "obfuscates" by encrypting with hard-coded password.
-  // We need to improve this password situation by moving a secure password
-  // into a system-level key store.
-  // http://crbug.com/25404 and http://crbug.com/49115
-
   if (plaintext.empty()) {
     *ciphertext = std::string();
     return true;
   }
 
-  std::unique_ptr<crypto::SymmetricKey> encryption_key(GetEncryptionKey());
-  if (!encryption_key.get())
-    return false;
+  *ciphertext = kObfuscationPrefix;
+  ciphertext->append(base::as_string_view(
+      crypto::aes_cbc::Encrypt(kV10Key, kIv, base::as_byte_span(plaintext))));
 
-  const std::string iv(kIVBlockSizeAES128, ' ');
-  crypto::Encryptor encryptor;
-  if (!encryptor.Init(encryption_key.get(), crypto::Encryptor::CBC, iv))
-    return false;
-
-  if (!encryptor.Encrypt(plaintext, ciphertext))
-    return false;
-
-  // Prefix the cypher text with version information.
-  ciphertext->insert(0, kObfuscationPrefix);
   return true;
 }
 
 bool OSCrypt::DecryptString(const std::string& ciphertext,
                             std::string* plaintext) {
-  // This currently "obfuscates" by encrypting with hard-coded password.
-  // We need to improve this password situation by moving a secure password
-  // into a system-level key store.
-  // http://crbug.com/25404 and http://crbug.com/49115
-
   if (ciphertext.empty()) {
     *plaintext = std::string();
     return true;
   }
 
-  // Check that the incoming cyphertext was indeed encrypted with the expected
-  // version.  If the prefix is not found then we'll assume we're dealing with
-  // old data saved as clear text and we'll return it directly.
-  // Credit card numbers are current legacy data, so false match with prefix
-  // won't happen.
+  // The incoming ciphertext was either obfuscated by OSCrypt::EncryptString()
+  // as above, or is regular unobfuscated plaintext if it was imported from an
+  // old version. Check for the obfuscation prefix to detect obfuscated text.
   const bool no_prefix_found = !base::StartsWith(ciphertext, kObfuscationPrefix,
                                                  base::CompareCase::SENSITIVE);
 
@@ -135,19 +97,14 @@
   const std::string raw_ciphertext =
       ciphertext.substr(strlen(kObfuscationPrefix));
 
-  std::unique_ptr<crypto::SymmetricKey> encryption_key(GetEncryptionKey());
-  if (!encryption_key.get())
-    return false;
+  std::optional<std::vector<uint8_t>> maybe_plain = crypto::aes_cbc::Decrypt(
+      kV10Key, kIv, base::as_byte_span(raw_ciphertext));
 
-  const std::string iv(kIVBlockSizeAES128, ' ');
-  crypto::Encryptor encryptor;
-  if (!encryptor.Init(encryption_key.get(), crypto::Encryptor::CBC, iv))
-    return false;
+  if (maybe_plain) {
+    plaintext->assign(base::as_string_view(*maybe_plain));
+  }
 
-  if (!encryptor.Decrypt(raw_ciphertext, plaintext))
-    return false;
-
-  return true;
+  return maybe_plain.has_value();
 }
 
 // static
diff --git a/components/page_image_service/image_service_impl_unittest.cc b/components/page_image_service/image_service_impl_unittest.cc
index c6f37ef..15a8dfb 100644
--- a/components/page_image_service/image_service_impl_unittest.cc
+++ b/components/page_image_service/image_service_impl_unittest.cc
@@ -83,6 +83,7 @@
 
     remote_suggestions_service_ = std::make_unique<RemoteSuggestionsService>(
         /*document_suggestions_service=*/nullptr,
+        /*search_aggregator_suggestions_service=*/nullptr,
         test_url_loader_factory_.GetSafeWeakWrapper());
     test_opt_guide_ =
         std::make_unique<optimization_guide::ImageServiceTestOptGuide>();
@@ -481,6 +482,7 @@
 
     remote_suggestions_service_ = std::make_unique<RemoteSuggestionsService>(
         /*document_suggestions_service=*/nullptr,
+        /*search_aggregator_suggestions_service=*/nullptr,
         test_url_loader_factory_.GetSafeWeakWrapper());
     test_opt_guide_ =
         std::make_unique<optimization_guide::ImageServiceTestOptGuide>();
diff --git a/components/password_manager/core/browser/password_manual_fallback_flow.cc b/components/password_manager/core/browser/password_manual_fallback_flow.cc
index 374681fc..9c0e26d 100644
--- a/components/password_manager/core/browser/password_manual_fallback_flow.cc
+++ b/components/password_manager/core/browser/password_manual_fallback_flow.cc
@@ -13,6 +13,7 @@
 #include "base/metrics/histogram_functions.h"
 #include "base/ranges/algorithm.h"
 #include "base/strings/utf_string_conversions.h"
+#include "components/autofill/core/browser/foundations/autofill_client.h"
 #include "components/autofill/core/browser/suggestions/suggestion.h"
 #include "components/autofill/core/browser/ui/popup_open_enums.h"
 #include "components/autofill/core/common/aliases.h"
diff --git a/components/performance_manager/resource_attribution/frame_context_unittest.cc b/components/performance_manager/resource_attribution/frame_context_unittest.cc
index 415a885..a4d3fb7 100644
--- a/components/performance_manager/resource_attribution/frame_context_unittest.cc
+++ b/components/performance_manager/resource_attribution/frame_context_unittest.cc
@@ -12,7 +12,6 @@
 #include "components/performance_manager/public/performance_manager.h"
 #include "components/performance_manager/resource_attribution/performance_manager_aliases.h"
 #include "components/performance_manager/test_support/performance_manager_test_harness.h"
-#include "components/performance_manager/test_support/run_in_graph.h"
 #include "content/public/browser/global_routing_id.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
@@ -49,18 +48,15 @@
   base::WeakPtr<FrameNode> frame_node = frame_context->GetWeakFrameNode();
   base::WeakPtr<FrameNode> frame_node_from_pm =
       PerformanceManager::GetFrameNodeForRenderFrameHost(rfh);
-  performance_manager::RunInGraph([&] {
-    ASSERT_TRUE(frame_node);
-    ASSERT_TRUE(frame_node_from_pm);
-    EXPECT_EQ(frame_node.get(), frame_node_from_pm.get());
+  ASSERT_TRUE(frame_node);
+  ASSERT_TRUE(frame_node_from_pm);
+  EXPECT_EQ(frame_node.get(), frame_node_from_pm.get());
 
-    EXPECT_EQ(frame_node.get(), frame_context->GetFrameNode());
-    EXPECT_EQ(frame_context.value(), frame_node->GetResourceContext());
-    EXPECT_EQ(frame_context.value(),
-              FrameContext::FromFrameNode(frame_node.get()));
-    EXPECT_EQ(frame_context.value(),
-              FrameContext::FromWeakFrameNode(frame_node));
-  });
+  EXPECT_EQ(frame_node.get(), frame_context->GetFrameNode());
+  EXPECT_EQ(frame_context.value(), frame_node->GetResourceContext());
+  EXPECT_EQ(frame_context.value(),
+            FrameContext::FromFrameNode(frame_node.get()));
+  EXPECT_EQ(frame_context.value(), FrameContext::FromWeakFrameNode(frame_node));
 
   // Make sure a second frame gets a different context.
   std::unique_ptr<content::WebContents> web_contents2 = CreateTestWebContents();
@@ -80,11 +76,9 @@
   EXPECT_EQ(nullptr, frame_context->GetRenderFrameHost());
   EXPECT_EQ(rfh_id, frame_context->GetRenderFrameHostId());
 
-  performance_manager::RunInGraph([&] {
-    EXPECT_FALSE(frame_node);
-    EXPECT_EQ(nullptr, frame_context->GetFrameNode());
-    EXPECT_EQ(std::nullopt, FrameContext::FromWeakFrameNode(frame_node));
-  });
+  EXPECT_FALSE(frame_node);
+  EXPECT_EQ(nullptr, frame_context->GetFrameNode());
+  EXPECT_EQ(std::nullopt, FrameContext::FromWeakFrameNode(frame_node));
 }
 
 TEST_F(ResourceAttrFrameContextNoPMTest, FrameContextWithoutPM) {
@@ -101,10 +95,9 @@
   ASSERT_TRUE(rfh);
 
   // Verify that PM didn't see the frame.
-  performance_manager::RunInGraph(
-      [node = PerformanceManager::GetFrameNodeForRenderFrameHost(rfh)] {
-        EXPECT_FALSE(node);
-      });
+  base::WeakPtr<FrameNode> node =
+      PerformanceManager::GetFrameNodeForRenderFrameHost(rfh);
+  EXPECT_FALSE(node);
 
   // FromRenderFrameHost() should return nullopt, not a context that's missing
   // PM info.
diff --git a/components/performance_manager/resource_attribution/memory_measurement_provider_browsertest.cc b/components/performance_manager/resource_attribution/memory_measurement_provider_browsertest.cc
index 2ca3030..6a0a8c4c 100644
--- a/components/performance_manager/resource_attribution/memory_measurement_provider_browsertest.cc
+++ b/components/performance_manager/resource_attribution/memory_measurement_provider_browsertest.cc
@@ -18,7 +18,6 @@
 #include "components/performance_manager/resource_attribution/performance_manager_aliases.h"
 #include "components/performance_manager/test_support/performance_manager_browsertest_harness.h"
 #include "components/performance_manager/test_support/resource_attribution/gtest_util.h"
-#include "components/performance_manager/test_support/run_in_graph.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
 #include "content/public/browser/web_contents.h"
@@ -59,7 +58,7 @@
   void TearDownOnMainThread() override {
     // Delete MemoryMeasurementProvider before tearing down the graph to avoid
     // dangling pointers.
-    performance_manager::RunInGraph([&] { memory_provider_.reset(); });
+    memory_provider_.reset();
     Super::TearDownOnMainThread();
   }
 
@@ -69,10 +68,7 @@
     base::test::TestFuture<QueryResultMap> results_future;
     base::OnceCallback<void(QueryResultMap)> results_callback =
         results_future.GetSequenceBoundCallback();
-    performance_manager::RunInGraph([&](base::OnceClosure quit_closure) {
-      memory_provider_->RequestMemorySummary(
-          std::move(results_callback).Then(std::move(quit_closure)));
-    });
+    memory_provider_->RequestMemorySummary(std::move(results_callback));
     return results_future.Take();
   }
 
diff --git a/components/performance_manager/resource_attribution/page_context_unittest.cc b/components/performance_manager/resource_attribution/page_context_unittest.cc
index 9d584ff..485b7b7 100644
--- a/components/performance_manager/resource_attribution/page_context_unittest.cc
+++ b/components/performance_manager/resource_attribution/page_context_unittest.cc
@@ -12,7 +12,6 @@
 #include "components/performance_manager/public/performance_manager.h"
 #include "components/performance_manager/resource_attribution/performance_manager_aliases.h"
 #include "components/performance_manager/test_support/performance_manager_test_harness.h"
-#include "components/performance_manager/test_support/run_in_graph.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/navigation_simulator.h"
@@ -39,16 +38,14 @@
   base::WeakPtr<PageNode> page_node = page_context->GetWeakPageNode();
   base::WeakPtr<PageNode> page_node_from_pm =
       PerformanceManager::GetPrimaryPageNodeForWebContents(web_contents.get());
-  performance_manager::RunInGraph([&] {
-    ASSERT_TRUE(page_node);
-    ASSERT_TRUE(page_node_from_pm);
-    EXPECT_EQ(page_node.get(), page_node_from_pm.get());
+  ASSERT_TRUE(page_node);
+  ASSERT_TRUE(page_node_from_pm);
+  EXPECT_EQ(page_node.get(), page_node_from_pm.get());
 
-    EXPECT_EQ(page_node.get(), page_context->GetPageNode());
-    EXPECT_EQ(page_context.value(), page_node->GetResourceContext());
-    EXPECT_EQ(page_context.value(), PageContext::FromPageNode(page_node.get()));
-    EXPECT_EQ(page_context.value(), PageContext::FromWeakPageNode(page_node));
-  });
+  EXPECT_EQ(page_node.get(), page_context->GetPageNode());
+  EXPECT_EQ(page_context.value(), page_node->GetResourceContext());
+  EXPECT_EQ(page_context.value(), PageContext::FromPageNode(page_node.get()));
+  EXPECT_EQ(page_context.value(), PageContext::FromWeakPageNode(page_node));
 
   // Navigating the page should not change the PageContext since it corresponds
   // to the WebContents, not the document.
@@ -71,11 +68,9 @@
   web_contents.reset();
 
   EXPECT_EQ(nullptr, page_context->GetWebContents());
-  performance_manager::RunInGraph([&] {
-    EXPECT_FALSE(page_node);
-    EXPECT_EQ(nullptr, page_context->GetPageNode());
-    EXPECT_EQ(std::nullopt, PageContext::FromWeakPageNode(page_node));
-  });
+  EXPECT_FALSE(page_node);
+  EXPECT_EQ(nullptr, page_context->GetPageNode());
+  EXPECT_EQ(std::nullopt, PageContext::FromWeakPageNode(page_node));
 
   // The unique id of a PageContext isn't exposed so can't be tested directly.
   // But after deleting a page, its PageContexts should still compare equal,
@@ -97,9 +92,9 @@
   std::unique_ptr<content::WebContents> web_contents = CreateTestWebContents();
 
   // Verify that PM didn't see the page.
-  performance_manager::RunInGraph(
-      [node = PerformanceManager::GetPrimaryPageNodeForWebContents(
-           web_contents.get())] { EXPECT_FALSE(node); });
+  base::WeakPtr<PageNode> node =
+      PerformanceManager::GetPrimaryPageNodeForWebContents(web_contents.get());
+  EXPECT_FALSE(node);
 
   EXPECT_FALSE(PageContext::FromWebContents(web_contents.get()));
 }
diff --git a/components/performance_manager/resource_attribution/process_context_unittest.cc b/components/performance_manager/resource_attribution/process_context_unittest.cc
index f4c12d3e..e5ffc97 100644
--- a/components/performance_manager/resource_attribution/process_context_unittest.cc
+++ b/components/performance_manager/resource_attribution/process_context_unittest.cc
@@ -13,7 +13,6 @@
 #include "components/performance_manager/public/render_process_host_id.h"
 #include "components/performance_manager/resource_attribution/performance_manager_aliases.h"
 #include "components/performance_manager/test_support/performance_manager_test_harness.h"
-#include "components/performance_manager/test_support/run_in_graph.h"
 #include "components/performance_manager/test_support/test_browser_child_process.h"
 #include "content/public/browser/browser_child_process_host.h"
 #include "content/public/browser/render_frame_host.h"
@@ -55,27 +54,24 @@
       process_context->GetWeakProcessNode();
   base::WeakPtr<ProcessNode> process_node_from_pm =
       PerformanceManager::GetProcessNodeForBrowserProcess();
-  performance_manager::RunInGraph([&] {
-    ASSERT_TRUE(process_node);
-    ASSERT_TRUE(process_node_from_pm);
-    EXPECT_EQ(process_node.get(), process_node_from_pm.get());
 
-    EXPECT_EQ(process_node.get(), process_context->GetProcessNode());
-    EXPECT_EQ(process_context.value(), process_node->GetResourceContext());
-    EXPECT_EQ(process_context.value(),
-              ProcessContext::FromProcessNode(process_node.get()));
-    EXPECT_EQ(process_context.value(),
-              ProcessContext::FromWeakProcessNode(process_node));
-  });
+  ASSERT_TRUE(process_node);
+  ASSERT_TRUE(process_node_from_pm);
+  EXPECT_EQ(process_node.get(), process_node_from_pm.get());
+
+  EXPECT_EQ(process_node.get(), process_context->GetProcessNode());
+  EXPECT_EQ(process_context.value(), process_node->GetResourceContext());
+  EXPECT_EQ(process_context.value(),
+            ProcessContext::FromProcessNode(process_node.get()));
+  EXPECT_EQ(process_context.value(),
+            ProcessContext::FromWeakProcessNode(process_node));
 
   performance_manager::DeleteBrowserProcessNodeForTesting();
 
   EXPECT_TRUE(process_context->IsBrowserProcessContext());
-  performance_manager::RunInGraph([&] {
-    EXPECT_FALSE(process_node);
-    EXPECT_EQ(nullptr, process_context->GetProcessNode());
-    EXPECT_EQ(std::nullopt, ProcessContext::FromWeakProcessNode(process_node));
-  });
+  EXPECT_FALSE(process_node);
+  EXPECT_EQ(nullptr, process_context->GetProcessNode());
+  EXPECT_EQ(std::nullopt, ProcessContext::FromWeakProcessNode(process_node));
 }
 
 TEST_F(ResourceAttrProcessContextTest, RenderProcessContext) {
@@ -106,18 +102,17 @@
       process_context->GetWeakProcessNode();
   base::WeakPtr<ProcessNode> process_node_from_pm =
       PerformanceManager::GetProcessNodeForRenderProcessHost(rph);
-  performance_manager::RunInGraph([&] {
-    ASSERT_TRUE(process_node);
-    ASSERT_TRUE(process_node_from_pm);
-    EXPECT_EQ(process_node.get(), process_node_from_pm.get());
 
-    EXPECT_EQ(process_node.get(), process_context->GetProcessNode());
-    EXPECT_EQ(process_context.value(), process_node->GetResourceContext());
-    EXPECT_EQ(process_context.value(),
-              ProcessContext::FromProcessNode(process_node.get()));
-    EXPECT_EQ(process_context.value(),
-              ProcessContext::FromWeakProcessNode(process_node));
-  });
+  ASSERT_TRUE(process_node);
+  ASSERT_TRUE(process_node_from_pm);
+  EXPECT_EQ(process_node.get(), process_node_from_pm.get());
+
+  EXPECT_EQ(process_node.get(), process_context->GetProcessNode());
+  EXPECT_EQ(process_context.value(), process_node->GetResourceContext());
+  EXPECT_EQ(process_context.value(),
+            ProcessContext::FromProcessNode(process_node.get()));
+  EXPECT_EQ(process_context.value(),
+            ProcessContext::FromWeakProcessNode(process_node));
 
   // Make sure a second process gets a different context.
   std::unique_ptr<content::WebContents> web_contents2 = CreateTestWebContents();
@@ -140,11 +135,9 @@
   EXPECT_EQ(nullptr, process_context->GetRenderProcessHost());
   EXPECT_EQ(rph_id, process_context->GetRenderProcessHostId());
 
-  performance_manager::RunInGraph([&] {
-    EXPECT_FALSE(process_node);
-    EXPECT_EQ(nullptr, process_context->GetProcessNode());
-    EXPECT_EQ(std::nullopt, ProcessContext::FromWeakProcessNode(process_node));
-  });
+  EXPECT_FALSE(process_node);
+  EXPECT_EQ(nullptr, process_context->GetProcessNode());
+  EXPECT_EQ(std::nullopt, ProcessContext::FromWeakProcessNode(process_node));
 }
 
 TEST_F(ResourceAttrProcessContextTest, BrowserChildProcessContext) {
@@ -171,18 +164,17 @@
   base::WeakPtr<ProcessNode> process_node_from_pm =
       PerformanceManager::GetProcessNodeForBrowserChildProcessHost(
           utility_process->host());
-  performance_manager::RunInGraph([&] {
-    ASSERT_TRUE(process_node);
-    ASSERT_TRUE(process_node_from_pm);
-    EXPECT_EQ(process_node.get(), process_node_from_pm.get());
 
-    EXPECT_EQ(process_node.get(), process_context->GetProcessNode());
-    EXPECT_EQ(process_context.value(), process_node->GetResourceContext());
-    EXPECT_EQ(process_context.value(),
-              ProcessContext::FromProcessNode(process_node.get()));
-    EXPECT_EQ(process_context.value(),
-              ProcessContext::FromWeakProcessNode(process_node));
-  });
+  ASSERT_TRUE(process_node);
+  ASSERT_TRUE(process_node_from_pm);
+  EXPECT_EQ(process_node.get(), process_node_from_pm.get());
+
+  EXPECT_EQ(process_node.get(), process_context->GetProcessNode());
+  EXPECT_EQ(process_context.value(), process_node->GetResourceContext());
+  EXPECT_EQ(process_context.value(),
+            ProcessContext::FromProcessNode(process_node.get()));
+  EXPECT_EQ(process_context.value(),
+            ProcessContext::FromWeakProcessNode(process_node));
 
   // Make sure a second process gets a different context.
   TestBrowserChildProcess gpu_process(content::PROCESS_TYPE_GPU);
@@ -199,11 +191,9 @@
   EXPECT_EQ(nullptr, process_context->GetBrowserChildProcessHost());
   EXPECT_EQ(utility_id, process_context->GetBrowserChildProcessHostId());
 
-  performance_manager::RunInGraph([&] {
-    EXPECT_FALSE(process_node);
-    EXPECT_EQ(nullptr, process_context->GetProcessNode());
-    EXPECT_EQ(std::nullopt, ProcessContext::FromWeakProcessNode(process_node));
-  });
+  EXPECT_FALSE(process_node);
+  EXPECT_EQ(nullptr, process_context->GetProcessNode());
+  EXPECT_EQ(std::nullopt, ProcessContext::FromWeakProcessNode(process_node));
 }
 
 TEST_F(ResourceAttrProcessContextNoPMTest, ProcessContextWithoutPM) {
diff --git a/components/performance_manager/resource_attribution/worker_context_unittest.cc b/components/performance_manager/resource_attribution/worker_context_unittest.cc
index 6034c11..9da69fc 100644
--- a/components/performance_manager/resource_attribution/worker_context_unittest.cc
+++ b/components/performance_manager/resource_attribution/worker_context_unittest.cc
@@ -18,7 +18,6 @@
 #include "components/performance_manager/public/performance_manager.h"
 #include "components/performance_manager/resource_attribution/performance_manager_aliases.h"
 #include "components/performance_manager/test_support/performance_manager_test_harness.h"
-#include "components/performance_manager/test_support/run_in_graph.h"
 #include "components/performance_manager/worker_watcher.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/global_routing_id.h"
@@ -73,19 +72,18 @@
   };
 
   // Validate that the right worker nodes were created, save a pointer to one.
-  base::WeakPtr<WorkerNode> worker_node;
-  performance_manager::RunInGraph([&](Graph* graph) {
-    bool found_worker = false;
+  base::WeakPtr<WorkerNode> worker_node = [&]() -> base::WeakPtr<WorkerNode> {
+    Graph* graph = PerformanceManager::GetGraph();
     for (const WorkerNode* node : graph->GetAllWorkerNodes()) {
       EXPECT_THAT(node->GetWorkerToken(),
                   ::testing::AnyOf(worker_token, worker_token2));
       if (node->GetWorkerToken() == worker_token) {
-        worker_node = WorkerNodeImpl::FromNode(node)->GetWeakPtr();
-        found_worker = true;
+        return WorkerNodeImpl::FromNode(node)->GetWeakPtr();
       }
     }
-    EXPECT_TRUE(found_worker);
-  });
+    return nullptr;
+  }();
+  ASSERT_TRUE(worker_node);
 
   std::optional<WorkerContext> worker_context =
       WorkerContext::FromWorkerToken(worker_token);
@@ -94,18 +92,16 @@
 
   base::WeakPtr<WorkerNode> worker_node_from_context =
       worker_context->GetWeakWorkerNode();
-  performance_manager::RunInGraph([&] {
-    ASSERT_TRUE(worker_node);
-    ASSERT_TRUE(worker_node_from_context);
-    EXPECT_EQ(worker_node.get(), worker_node_from_context.get());
+  ASSERT_TRUE(worker_node);
+  ASSERT_TRUE(worker_node_from_context);
+  EXPECT_EQ(worker_node.get(), worker_node_from_context.get());
 
-    EXPECT_EQ(worker_node.get(), worker_context->GetWorkerNode());
-    EXPECT_EQ(worker_context.value(), worker_node->GetResourceContext());
-    EXPECT_EQ(worker_context.value(),
-              WorkerContext::FromWorkerNode(worker_node.get()));
-    EXPECT_EQ(worker_context.value(),
-              WorkerContext::FromWeakWorkerNode(worker_node));
-  });
+  EXPECT_EQ(worker_node.get(), worker_context->GetWorkerNode());
+  EXPECT_EQ(worker_context.value(), worker_node->GetResourceContext());
+  EXPECT_EQ(worker_context.value(),
+            WorkerContext::FromWorkerNode(worker_node.get()));
+  EXPECT_EQ(worker_context.value(),
+            WorkerContext::FromWeakWorkerNode(worker_node));
 
   // Make sure a second worker gets a different context.
   std::optional<WorkerContext> worker_context2 =
@@ -118,12 +114,12 @@
   // Context still returns worker token, but it no longer matches any worker.
   EXPECT_EQ(worker_token, worker_context->GetWorkerToken());
   EXPECT_FALSE(WorkerContext::FromWorkerToken(worker_token).has_value());
-  performance_manager::RunInGraph([&](Graph* graph) {
-    EXPECT_EQ(graph->GetAllWorkerNodes().size(), 0u);
-    EXPECT_FALSE(worker_node);
-    EXPECT_EQ(nullptr, worker_context->GetWorkerNode());
-    EXPECT_EQ(std::nullopt, WorkerContext::FromWeakWorkerNode(worker_node));
-  });
+
+  Graph* graph = PerformanceManager::GetGraph();
+  EXPECT_EQ(graph->GetAllWorkerNodes().size(), 0u);
+  EXPECT_FALSE(worker_node);
+  EXPECT_EQ(nullptr, worker_context->GetWorkerNode());
+  EXPECT_EQ(std::nullopt, WorkerContext::FromWeakWorkerNode(worker_node));
 }
 
 TEST_F(ResourceAttrWorkerContextNoPMTest, WorkerContextWithoutPM) {
diff --git a/components/performance_manager/scenarios/performance_scenarios_unittest.cc b/components/performance_manager/scenarios/performance_scenarios_unittest.cc
index 2937d0ba..7d24a46 100644
--- a/components/performance_manager/scenarios/performance_scenarios_unittest.cc
+++ b/components/performance_manager/scenarios/performance_scenarios_unittest.cc
@@ -14,7 +14,6 @@
 #include "components/performance_manager/public/graph/process_node.h"
 #include "components/performance_manager/public/performance_manager.h"
 #include "components/performance_manager/test_support/performance_manager_test_harness.h"
-#include "components/performance_manager/test_support/run_in_graph.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/mock_render_process_host.h"
 #include "content/public/test/navigation_simulator.h"
@@ -78,15 +77,11 @@
     }
     base::WeakPtr<ProcessNode> process_node =
         PerformanceManager::GetProcessNodeForRenderProcessHost(process());
-    base::ReadOnlySharedMemoryRegion process_region;
-    RunInGraph([&] {
-      ASSERT_TRUE(process_node);
-      // GetPerformanceScenarioRegionForProcess() creates writable shared memory
-      // for that process' state if it doesn't already exist.
-      process_region =
-          GetSharedScenarioRegionForProcessNode(process_node.get());
-    });
-    return process_region;
+    EXPECT_TRUE(process_node);
+
+    // GetPerformanceScenarioRegionForProcess() creates writable shared memory
+    // for that process' state if it doesn't already exist.
+    return GetSharedScenarioRegionForProcessNode(process_node.get());
   }
 };
 
@@ -140,77 +135,9 @@
   EXPECT_TRUE(process_region.IsValid());
   SetLoadingScenarioForProcess(LoadingScenario::kVisiblePageLoading, process());
 
-  // SetLoadingScenarioForProcess posts to the PM thread. Wait until the message
-  // is received before reading.
-  RunInGraph([] {
-    EXPECT_EQ(GetLoadingScenario(ScenarioScope::kCurrentProcess)
-                  ->load(std::memory_order_relaxed),
-              LoadingScenario::kNoPageLoading);
-  });
-
-  // Map in the read-only view of `process_region`. Normally this would be done
-  // in the renderer process as the "current process" state. The state should
-  // now become visible.
-  blink::performance_scenarios::ScopedReadOnlyScenarioMemory
-      process_shared_memory(ScenarioScope::kCurrentProcess,
-                            std::move(process_region));
-  EXPECT_EQ(GetLoadingScenario(ScenarioScope::kCurrentProcess)
-                ->load(std::memory_order_relaxed),
-            LoadingScenario::kVisiblePageLoading);
-
-  observer_list->RemoveObserver(&mock_observer);
-}
-
-// TODO(crbug.com/382551028): Test is flaky on Android.
-#if BUILDFLAG(IS_ANDROID)
-#define MAYBE_SetFromPMSequence DISABLED_SetFromPMSequence
-#else
-#define MAYBE_SetFromPMSequence SetFromPMSequence
-#endif
-TEST_F(PerformanceScenariosTest, MAYBE_SetFromPMSequence) {
-  base::WeakPtr<ProcessNode> process_node =
-      PerformanceManager::GetProcessNodeForRenderProcessHost(process());
-
-  StrictMockPerformanceScenarioObserver mock_observer;
-  EXPECT_CALL(mock_observer,
-              OnLoadingScenarioChanged(ScenarioScope::kGlobal,
-                                       LoadingScenario::kNoPageLoading,
-                                       LoadingScenario::kFocusedPageLoading))
-      .WillOnce(base::test::RunOnceClosure(task_environment()->QuitClosure()));
-
-  // Create writable shared memory for the global state. This maps a read-only
-  // view of the memory in as well.
-  ScopedGlobalScenarioMemory global_shared_memory;
-  auto observer_list =
-      PerformanceScenarioObserverList::GetForScope(ScenarioScope::kGlobal);
-  ASSERT_TRUE(observer_list);
-  observer_list->AddObserver(&mock_observer);
-
-  // Create writable shared memory for a render process state. Since this is
-  // called in the browser process and the state is for a different process, it
-  // doesn't map in a read-only view.
-  base::ReadOnlySharedMemoryRegion process_region = main_process_region();
-  EXPECT_TRUE(process_region.IsValid());
-
-  // Set the loading scenario from the PM sequence.
-  RunInGraph([&] {
-    ASSERT_TRUE(process_node);
-    SetLoadingScenarioForProcessNode(LoadingScenario::kVisiblePageLoading,
-                                     process_node.get());
-    SetGlobalLoadingScenario(LoadingScenario::kFocusedPageLoading);
-  });
-
   EXPECT_EQ(GetLoadingScenario(ScenarioScope::kCurrentProcess)
                 ->load(std::memory_order_relaxed),
             LoadingScenario::kNoPageLoading);
-  EXPECT_EQ(GetLoadingScenario(ScenarioScope::kGlobal)
-                ->load(std::memory_order_relaxed),
-            LoadingScenario::kFocusedPageLoading);
-
-  // PerformanceScenarioObserverList is an ObserverListThreadSafe that posts
-  // a message to notify. Need to wait for the message.
-  task_environment()->RunUntilQuit();
-  ::testing::Mock::VerifyAndClearExpectations(&mock_observer);
 
   // Map in the read-only view of `process_region`. Normally this would be done
   // in the renderer process as the "current process" state. The state should
diff --git a/components/privacy_sandbox/masked_domain_list/masked_domain_list.proto b/components/privacy_sandbox/masked_domain_list/masked_domain_list.proto
index 0a83d8e..323592b 100644
--- a/components/privacy_sandbox/masked_domain_list/masked_domain_list.proto
+++ b/components/privacy_sandbox/masked_domain_list/masked_domain_list.proto
@@ -10,7 +10,7 @@
 
   enum Experiment {
     EXPERIMENT_UNKNOWN = 0;
-    EXPERIMENT_AFP = 1;
+    EXPERIMENT_EXTERNAL_REGULAR = 1;
   }
 
   optional string domain = 1;
diff --git a/components/safe_browsing/core/browser/realtime/url_lookup_service_base.cc b/components/safe_browsing/core/browser/realtime/url_lookup_service_base.cc
index 71544971..25e2fc976 100644
--- a/components/safe_browsing/core/browser/realtime/url_lookup_service_base.cc
+++ b/components/safe_browsing/core/browser/realtime/url_lookup_service_base.cc
@@ -659,10 +659,6 @@
       referrer_chain_provider_->IdentifyReferrerChainByEventURL(
           SanitizeURL(url), tab_id, GetReferrerUserGestureLimit(),
           request->mutable_referrer_chain());
-
-      RecordBooleanWithAndWithoutSuffix(
-          "SafeBrowsing.RT.EventUrlReferrerChainFetchSucceeded",
-          GetMetricSuffix(), !request->referrer_chain().empty());
     }
     SanitizeReferrerChainEntries(request->mutable_referrer_chain(),
                                  GetMinAllowedTimestampForReferrerChains(),
diff --git a/components/safe_browsing/core/browser/realtime/url_lookup_service_unittest.cc b/components/safe_browsing/core/browser/realtime/url_lookup_service_unittest.cc
index fc5ad88..e4c25fb 100644
--- a/components/safe_browsing/core/browser/realtime/url_lookup_service_unittest.cc
+++ b/components/safe_browsing/core/browser/realtime/url_lookup_service_unittest.cc
@@ -1380,11 +1380,6 @@
                             /*referring_app_info=*/std::nullopt);
   task_environment_.RunUntilIdle();
   EXPECT_TRUE(request_validated);
-
-  histogram_tester.ExpectUniqueSample(
-      "SafeBrowsing.RT.EventUrlReferrerChainFetchSucceeded",
-      /*sample=*/true,
-      /*expected_bucket_count=*/1);
 }
 
 TEST_F(RealTimeUrlLookupServiceTest, TestShutdown_CallbackNotPostedOnShutdown) {
diff --git a/components/safe_browsing/core/common/OWNERS b/components/safe_browsing/core/common/OWNERS
index bc50e25..5e84e2a 100644
--- a/components/safe_browsing/core/common/OWNERS
+++ b/components/safe_browsing/core/common/OWNERS
@@ -5,4 +5,3 @@
 
 per-file features.cc=file://components/safe_browsing/core/common/SAFE_BROWSING_FEATURE_OWNERS
 per-file features.h=file://components/safe_browsing/core/common/SAFE_BROWSING_FEATURE_OWNERS
-per-file features_unittest.cc=file://components/safe_browsing/core/common/SAFE_BROWSING_FEATURE_OWNERS
diff --git a/components/safe_browsing/core/common/SAFE_BROWSING_FEATURE_OWNERS b/components/safe_browsing/core/common/SAFE_BROWSING_FEATURE_OWNERS
index 2fc81adf..ad873e95 100644
--- a/components/safe_browsing/core/common/SAFE_BROWSING_FEATURE_OWNERS
+++ b/components/safe_browsing/core/common/SAFE_BROWSING_FEATURE_OWNERS
@@ -1,5 +1,9 @@
+# For adding/removing Features in features.{h,cc}
 anunoy@chromium.org
 chlily@chromium.org
+drubery@chromium.org
 jacastro@chromium.org
 kristianm@chromium.org
-nwokedi@chromium.org
\ No newline at end of file
+nwokedi@chromium.org
+thefrog@chromium.org
+xinghuilu@chromium.org
diff --git a/components/saved_tab_groups/public/tab_group_sync_service.h b/components/saved_tab_groups/public/tab_group_sync_service.h
index b6bd4fdd4..303c987 100644
--- a/components/saved_tab_groups/public/tab_group_sync_service.h
+++ b/components/saved_tab_groups/public/tab_group_sync_service.h
@@ -19,7 +19,7 @@
 #include "components/saved_tab_groups/proto/url_restriction.pb.h"
 #include "components/saved_tab_groups/public/saved_tab_group.h"
 #include "components/saved_tab_groups/public/types.h"
-#include "components/sync/model/data_type_sync_bridge.h"
+#include "components/sync/model/data_type_controller_delegate.h"
 #include "components/tab_groups/tab_group_id.h"
 #include "components/tab_groups/tab_group_visual_data.h"
 #include "ui/gfx/range/range.h"
diff --git a/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h b/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
index d5d171e..4478f88 100644
--- a/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
+++ b/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
@@ -8,6 +8,7 @@
 #include "components/saved_tab_groups/delegate/tab_group_sync_delegate.h"
 #include "components/saved_tab_groups/public/tab_group_sync_service.h"
 #include "components/saved_tab_groups/public/types.h"
+#include "components/sync/model/data_type_sync_bridge.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
 namespace tab_groups {
diff --git a/components/user_education/common/feature_promo/feature_promo_controller.h b/components/user_education/common/feature_promo/feature_promo_controller.h
index caeac8d..075e0b5 100644
--- a/components/user_education/common/feature_promo/feature_promo_controller.h
+++ b/components/user_education/common/feature_promo/feature_promo_controller.h
@@ -40,7 +40,7 @@
 }  // namespace ui
 
 // Declaring these in the global namespace for testing purposes.
-class BrowserFeaturePromoController20Test;
+class BrowserFeaturePromoController20TestBase;
 class FeaturePromoLifecycleUiTest;
 
 namespace user_education {
@@ -270,7 +270,7 @@
   }
 
  protected:
-  friend BrowserFeaturePromoController20Test;
+  friend BrowserFeaturePromoController20TestBase;
   friend FeaturePromoLifecycleUiTest;
 
   struct ShowPromoBubbleParams {
diff --git a/components/user_education/common/user_education_features.cc b/components/user_education/common/user_education_features.cc
index bd5d9de..861c964 100644
--- a/components/user_education/common/user_education_features.cc
+++ b/components/user_education/common/user_education_features.cc
@@ -68,6 +68,10 @@
 inline constexpr char kLowPriorityTimeout[] = "low_priority_timeout";
 inline constexpr base::TimeDelta kDefaultLowPriorityTimeout = base::Seconds(30);
 
+inline constexpr char kIdleTimeBeforeHeavyweight[] = "idle_before_heavyweight";
+inline constexpr base::TimeDelta kDefaultIdleTimeBeforeHeavyweight =
+    base::Seconds(5);
+
 inline constexpr char kPollingInterval[] = "polling_interval";
 inline constexpr base::TimeDelta kDefaultPollingInterval =
     base::Milliseconds(500);
@@ -199,6 +203,12 @@
       kDefaultLowPriorityTimeout);
 }
 
+base::TimeDelta GetIdleTimeBeforeHeavyweightPromo() {
+  return base::GetFieldTrialParamByFeatureAsTimeDelta(
+      kUserEducationExperienceVersion2Point5, kIdleTimeBeforeHeavyweight,
+      kDefaultIdleTimeBeforeHeavyweight);
+}
+
 base::TimeDelta GetPromoControllerPollingInterval() {
   return base::GetFieldTrialParamByFeatureAsTimeDelta(
       kUserEducationExperienceVersion2Point5, kPollingInterval,
diff --git a/components/user_education/common/user_education_features.h b/components/user_education/common/user_education_features.h
index 221fd65..13aa9972 100644
--- a/components/user_education/common/user_education_features.h
+++ b/components/user_education/common/user_education_features.h
@@ -86,6 +86,10 @@
 extern base::TimeDelta GetMediumPriorityTimeout();
 extern base::TimeDelta GetLowPriorityTimeout();
 
+// Returns how long the user must stop sending input before a heavyweight promo
+// can be shown.
+extern base::TimeDelta GetIdleTimeBeforeHeavyweightPromo();
+
 // Returns the polling interval for the promo controller for User Education 2.5.
 extern base::TimeDelta GetPromoControllerPollingInterval();
 
diff --git a/components/viz/service/display/overlay_processor_ozone.cc b/components/viz/service/display/overlay_processor_ozone.cc
index 223d7a1..407f1c6 100644
--- a/components/viz/service/display/overlay_processor_ozone.cc
+++ b/components/viz/service/display/overlay_processor_ozone.cc
@@ -481,6 +481,10 @@
     has_independent_cursor_plane_ =
         hardware_capabilities.has_independent_cursor_plane;
 
+    UMA_HISTOGRAM_COUNTS_100(
+        "Compositing.Display.OverlayProcessorOzone.MaxPlanesSupported",
+        hardware_capabilities.num_overlay_capable_planes);
+
     DCHECK(overlay_candidates_);
     overlay_candidates_->SetSupportedBufferFormats(
         std::move(hardware_capabilities.supported_buffer_formats));
diff --git a/components/viz/service/display/overlay_processor_using_strategy.cc b/components/viz/service/display/overlay_processor_using_strategy.cc
index 6fc88283..dcc169a 100644
--- a/components/viz/service/display/overlay_processor_using_strategy.cc
+++ b/components/viz/service/display/overlay_processor_using_strategy.cc
@@ -76,6 +76,9 @@
   kMaxValue = kNoDrmRejected
 };
 
+constexpr char kShouldAttemptMultipleOverlaysHistogramName[] =
+    "Compositing.Display.OverlayProcessorUsingStrategy."
+    "ShouldAttemptMultipleOverlays";
 constexpr char kNumOverlaysPromotedHistogramName[] =
     "Compositing.Display.OverlayProcessorUsingStrategy.NumOverlaysPromoted";
 constexpr char kNumOverlaysAttemptedHistogramName[] =
@@ -918,6 +921,8 @@
 bool OverlayProcessorUsingStrategy::ShouldAttemptMultipleOverlays(
     const std::vector<OverlayProposedCandidate>& sorted_candidates) {
   if (max_overlays_config_ <= 1) {
+    UMA_HISTOGRAM_ENUMERATION(kShouldAttemptMultipleOverlaysHistogramName,
+                              AttemptingMultipleOverlays::kNoFeatureDisabled);
     return false;
   }
 
@@ -926,6 +931,8 @@
     // different scale factors. This becomes complicated when using multiple
     // overlays at once so we won't attempt multiple in that case.
     if (proposed.candidate.requires_overlay) {
+      UMA_HISTOGRAM_ENUMERATION(kShouldAttemptMultipleOverlaysHistogramName,
+                                AttemptingMultipleOverlays::kNoRequiredOverlay);
       return false;
     }
     // Using multiple overlays only makes sense with SingleOnTop and Underlay
@@ -933,10 +940,15 @@
     OverlayStrategy type = proposed.strategy->GetUMAEnum();
     if (type != OverlayStrategy::kSingleOnTop &&
         type != OverlayStrategy::kUnderlay) {
+      UMA_HISTOGRAM_ENUMERATION(
+          kShouldAttemptMultipleOverlaysHistogramName,
+          AttemptingMultipleOverlays::kNoUnsupportedStrategy);
       return false;
     }
   }
 
+  UMA_HISTOGRAM_ENUMERATION(kShouldAttemptMultipleOverlaysHistogramName,
+                            AttemptingMultipleOverlays::kYes);
   return true;
 }
 
diff --git a/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc b/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
index 687fd392..252bba7 100644
--- a/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
+++ b/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
@@ -531,8 +531,6 @@
         unmatched_field = "navigation_id";
       } else if (context_origin() != other.context_origin()) {
         unmatched_field = "context_origin";
-      } else if (last_input_event() != other.last_input_event()) {
-        unmatched_field = "last_input_event";
       } else if (is_within_fenced_frame() != other.is_within_fenced_frame()) {
         unmatched_field = "is_within_fenced_frame";
       } else if (render_frame_id() != other.render_frame_id()) {
@@ -1019,7 +1017,9 @@
   std::vector<attribution_reporting::OsRegistrationItem> Buffer(
       std::vector<attribution_reporting::OsRegistrationItem> items,
       const RegistrationContext& registration_context) {
-    // Only navigation-tied OS registrations should be buffered.
+    // Only navigation-tied OS registrations should be buffered. The last input
+    // event for the first registration of the navigation is used for all
+    // subsequent registrations for the corresponding navigation.
     CHECK(registration_context.navigation_id().has_value());
     CHECK_EQ(registration_context.navigation_id().value(), navigation_id_);
     if (!context_.has_value()) {
diff --git a/content/browser/attribution_reporting/attribution_suitable_context.cc b/content/browser/attribution_reporting/attribution_suitable_context.cc
index ab58718..3c88b373 100644
--- a/content/browser/attribution_reporting/attribution_suitable_context.cc
+++ b/content/browser/attribution_reporting/attribution_suitable_context.cc
@@ -130,7 +130,9 @@
   const auto tie = [](const AttributionSuitableContext& c) {
     // We don't check the `attribution_data_host_manager_` property since we'd
     // consider two contexts equal even if the manager is no longer available.
-    return std::make_tuple(c.context_origin(), c.last_input_event(),
+    // We also don't check the `last_input_event_` property which is time
+    // sensitive.
+    return std::make_tuple(c.context_origin(),
                            c.is_nested_within_fenced_frame(),
                            c.last_navigation_id(), c.root_render_frame_id());
   };
diff --git a/content/browser/devtools/protocol/target_handler.cc b/content/browser/devtools/protocol/target_handler.cc
index 02677b3..0631d42 100644
--- a/content/browser/devtools/protocol/target_handler.cc
+++ b/content/browser/devtools/protocol/target_handler.cc
@@ -1191,6 +1191,7 @@
     std::optional<int> top,
     std::optional<int> width,
     std::optional<int> height,
+    std::optional<std::string> window_state,
     std::optional<std::string> context_id,
     std::optional<bool> enable_begin_frame_control,
     std::optional<bool> new_window,
diff --git a/content/browser/devtools/protocol/target_handler.h b/content/browser/devtools/protocol/target_handler.h
index 6715e2f4..5521f28 100644
--- a/content/browser/devtools/protocol/target_handler.h
+++ b/content/browser/devtools/protocol/target_handler.h
@@ -119,6 +119,7 @@
                         std::optional<int> top,
                         std::optional<int> width,
                         std::optional<int> height,
+                        std::optional<std::string> window_state,
                         std::optional<std::string> context_id,
                         std::optional<bool> enable_begin_frame_control,
                         std::optional<bool> new_window,
diff --git a/content/browser/fenced_frame/PERMISSIONS_POLICIES.md b/content/browser/fenced_frame/PERMISSIONS_POLICIES.md
index c82a5ed..d2186f0 100644
--- a/content/browser/fenced_frame/PERMISSIONS_POLICIES.md
+++ b/content/browser/fenced_frame/PERMISSIONS_POLICIES.md
@@ -332,13 +332,21 @@
 kClientHintUAPlatformVersion, kClientHintPrefersColorScheme,
 kClientHintUABitness, kClientHintViewportHeight, kClientHintUAFullVersionList,
 kClientHintUAWoW64, kClientHintSaveData, kClientHintPrefersReducedMotion,
-kClientHintUAFormFactor*
+kClientHintUAFormFactors*
 
 This allows a fenced frame to learn about the device it’s on, but no information
 will flow back to the embedder. Data is only sent at navigation time, as the
 client hints live in the HTTP request headers. This can be used for
 fingerprinting at navigation time (before network cutoff). **Note that this will
 require its own separate effort to enable**.
+### 🖐️ User-Agent Client Hints getHighEntropyValues(): fingerprinting risk
+*Feature: kClientHintUAHighEntropyValues*
+
+By default, the `navigator.userAgentData.getHighEntropyValues()` JS API can be
+called by embedded frames and may pose a fingerprinting risk, and unlike other
+Client Hints features, is unrelated to navigation. kClientHintUAHighEntropyValues
+can be used to limit which origins can collect high-entropy User-Agent
+client hints.
 
 ### ❌ Credentials Get: usability issues
 *Feature: kPublicKeyCredentialsGet, kOTPCredentials, kIdentityCredentialsGet*
diff --git a/content/browser/media/media_browsertest.cc b/content/browser/media/media_browsertest.cc
index 39d6dd7..56e01cb 100644
--- a/content/browser/media/media_browsertest.cc
+++ b/content/browser/media/media_browsertest.cc
@@ -357,26 +357,26 @@
 #if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
 // HEVC video stream with 8-bit 422 range extension profile
 IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearMp4Hevc8bit422) {
-  MaybePlayVideo("hev1.4.10.L93.9d.8",
-                 "bear-1280x720-hevc-8bit-422-no-audio.mp4");
+  MaybePlayVideo("hvc1.4.10.L93.9D.08",
+                 "quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4");
 }
 
 // HEVC video stream with 8-bit 444 range extension profile
 IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearMp4Hevc8bit444) {
-  MaybePlayVideo("hev1.4.10.L93.9e.8",
-                 "bear-1280x720-hevc-8bit-444-no-audio.mp4");
+  MaybePlayVideo("hvc1.4.10.L93.9E.08",
+                 "quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4");
 }
 
 // HEVC video stream with 10-bit 422 range extension profile
 IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearMp4Hevc10bit422) {
-  MaybePlayVideo("hev1.4.10.L93.9d.8",
-                 "bear-1280x720-hevc-10bit-422-no-audio.mp4");
+  MaybePlayVideo("hvc1.4.10.L93.9D.08",
+                 "quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4");
 }
 
 // HEVC video stream with 10-bit 444 range extension profile
 IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearMp4Hevc10bit444) {
-  MaybePlayVideo("hev1.4.10.L93.9c.8",
-                 "bear-1280x720-hevc-10bit-444-no-audio.mp4");
+  MaybePlayVideo("hvc1.4.10.L93.9C.08",
+                 "quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4");
 }
 
 // HEVC video stream with 8-bit main profile
diff --git a/content/browser/preloading/prefetch/prefetch_response_reader.cc b/content/browser/preloading/prefetch/prefetch_response_reader.cc
index e722678..8d0fe26 100644
--- a/content/browser/preloading/prefetch/prefetch_response_reader.cc
+++ b/content/browser/preloading/prefetch/prefetch_response_reader.cc
@@ -611,8 +611,8 @@
     auto& indices = cookie_indices_.emplace();
     indices.cookie_names = *head.parsed_headers->cookie_indices;
     base::ranges::sort(indices.cookie_names);
-    indices.cookie_names.erase(base::ranges::unique(indices.cookie_names),
-                               indices.cookie_names.end());
+    auto repeated = std::ranges::unique(indices.cookie_names);
+    indices.cookie_names.erase(repeated.begin(), repeated.end());
     indices.cookie_names.shrink_to_fit();
     indices.expected_hash =
         net::HashCookieIndices(indices.cookie_names, head.request_cookies);
diff --git a/content/browser/private_aggregation/private_aggregation_budgeter.cc b/content/browser/private_aggregation/private_aggregation_budgeter.cc
index e1666eea..e64ce10 100644
--- a/content/browser/private_aggregation/private_aggregation_budgeter.cc
+++ b/content/browser/private_aggregation/private_aggregation_budgeter.cc
@@ -115,14 +115,13 @@
       PrivateAggregationBudgeter::kLargerScopeValues.budget_scope_duration
           .InMicroseconds() -
       kWindowDuration;
-  const auto minmax = base::ranges::minmax(
+  const auto minmax = std::ranges::minmax(
       *budget_entries, /*comp=*/{},
       &proto::PrivateAggregationBudgetEntry::entry_start_timestamp);
 
   CHECK_EQ(kMaximumWindowStartDifference,
            current_window_start - earliest_window_in_larger_scope_start);
-  if (minmax.second.entry_start_timestamp() -
-          minmax.first.entry_start_timestamp() >
+  if (minmax.max.entry_start_timestamp() - minmax.min.entry_start_timestamp() >
       kMaximumWindowStartDifference) {
     RecordBudgetValidity(ValidityStatus::kSpansMoreThanADay);
     return;
@@ -147,14 +146,14 @@
     google::protobuf::RepeatedPtrField<proto::PrivateAggregationBudgetEntry>*
         budget_entries,
     const int64_t earliest_window_in_larger_scope_start) {
-  auto new_end = base::ranges::remove_if(
+  auto to_remove = std::ranges::remove_if(
       *budget_entries, [&earliest_window_in_larger_scope_start](
                            const proto::PrivateAggregationBudgetEntry& elem) {
         return elem.entry_start_timestamp() <
                earliest_window_in_larger_scope_start;
       });
-  bool was_modified = new_end != budget_entries->end();
-  budget_entries->erase(new_end, budget_entries->end());
+  bool was_modified = !to_remove.empty();
+  budget_entries->erase(to_remove.begin(), to_remove.end());
   return was_modified;
 }
 
@@ -163,14 +162,14 @@
     google::protobuf::RepeatedPtrField<proto::ReportingOrigin>*
         reporting_origins,
     const int64_t earliest_window_in_larger_scope_start) {
-  auto new_end = base::ranges::remove_if(
+  auto to_remove = std::ranges::remove_if(
       *reporting_origins, [&earliest_window_in_larger_scope_start](
                               const proto::ReportingOrigin& elem) {
         return elem.last_used_timestamp() <
                earliest_window_in_larger_scope_start;
       });
-  bool was_modified = new_end != reporting_origins->end();
-  reporting_origins->erase(new_end, reporting_origins->end());
+  bool was_modified = !to_remove.empty();
+  reporting_origins->erase(to_remove.begin(), to_remove.end());
   return was_modified;
 }
 
@@ -631,13 +630,13 @@
           budget_entries = GetBudgetEntries(api, budgets);
       CHECK(budget_entries);
 
-      auto new_end = base::ranges::remove_if(
+      auto to_remove = std::ranges::remove_if(
           *budget_entries,
           [=](const proto::PrivateAggregationBudgetEntry& elem) {
             return elem.entry_start_timestamp() >= serialized_delete_begin &&
                    elem.entry_start_timestamp() <= serialized_delete_end;
           });
-      budget_entries->erase(new_end, budget_entries->end());
+      budget_entries->erase(to_remove.begin(), to_remove.end());
 
       CleanUpStaleBudgetEntries(budget_entries,
                                 earliest_window_in_larger_scope_start);
diff --git a/content/browser/renderer_host/input/stylus_handwriting_callback_sink_win.cc b/content/browser/renderer_host/input/stylus_handwriting_callback_sink_win.cc
index 94b69cf..360116a 100644
--- a/content/browser/renderer_host/input/stylus_handwriting_callback_sink_win.cc
+++ b/content/browser/renderer_host/input/stylus_handwriting_callback_sink_win.cc
@@ -52,7 +52,8 @@
 
   handwriting_callback_.Run(
       display::win::ScreenWin::ScreenToDIPRect(window, gfx::Rect(rect)),
-      gfx::Size(distance_threshold.cx, distance_threshold.cy));
+      display::win::ScreenWin::ScreenToDIPSize(
+          window, gfx::Size(distance_threshold.cx, distance_threshold.cy)));
 
   // Check that we have no pending callback.
   DCHECK(!pending_target_args_);
diff --git a/content/browser/renderer_host/media/media_stream_ui_proxy.cc b/content/browser/renderer_host/media/media_stream_ui_proxy.cc
index 1370e91..57901cacc3 100644
--- a/content/browser/renderer_host/media/media_stream_ui_proxy.cc
+++ b/content/browser/renderer_host/media/media_stream_ui_proxy.cc
@@ -159,8 +159,11 @@
     return;
   }
 
+  RenderFrameHostImpl* host = RenderFrameHostImpl::FromID(
+      request->render_process_id, request->render_frame_id);
+
   render_delegate->RequestMediaAccessPermission(
-      *request,
+      host, *request,
       base::BindOnce(&Core::ProcessAccessRequestResponse, weak_this_,
                      request->render_process_id, request->render_frame_id));
 }
diff --git a/content/browser/renderer_host/media/media_stream_ui_proxy_unittest.cc b/content/browser/renderer_host/media/media_stream_ui_proxy_unittest.cc
index 52a077b..7e9c23b 100644
--- a/content/browser/renderer_host/media/media_stream_ui_proxy_unittest.cc
+++ b/content/browser/renderer_host/media/media_stream_ui_proxy_unittest.cc
@@ -38,7 +38,8 @@
 namespace {
 class MockRenderFrameHostDelegate : public RenderFrameHostDelegate {
  public:
-  void RequestMediaAccessPermission(const MediaStreamRequest& request,
+  void RequestMediaAccessPermission(RenderFrameHostImpl* render_frame_host,
+                                    const MediaStreamRequest& request,
                                     MediaResponseCallback callback) override {
     return RequestMediaAccessPermission(request, &callback);
   }
@@ -632,7 +633,8 @@
  private:
   class TestRFHDelegate : public RenderFrameHostDelegate {
    public:
-    void RequestMediaAccessPermission(const MediaStreamRequest& request,
+    void RequestMediaAccessPermission(RenderFrameHostImpl* render_frame_host,
+                                      const MediaStreamRequest& request,
                                       MediaResponseCallback callback) override {
       blink::mojom::StreamDevicesSet stream_devices_set;
       stream_devices_set.stream_devices.emplace_back(
diff --git a/content/browser/renderer_host/media/video_capture_manager.cc b/content/browser/renderer_host/media/video_capture_manager.cc
index 06681956..59aebe3 100644
--- a/content/browser/renderer_host/media/video_capture_manager.cc
+++ b/content/browser/renderer_host/media/video_capture_manager.cc
@@ -930,7 +930,8 @@
 bool VideoCaptureManager::IsControllerPointerValid(
     const VideoCaptureController* controller) const {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  return base::Contains(controllers_, controller);
+  return base::Contains(controllers_, controller,
+                        &scoped_refptr<VideoCaptureController>::get);
 }
 
 scoped_refptr<VideoCaptureController>
diff --git a/content/browser/renderer_host/render_frame_host_delegate.cc b/content/browser/renderer_host/render_frame_host_delegate.cc
index 16055c4..00d0f6b 100644
--- a/content/browser/renderer_host/render_frame_host_delegate.cc
+++ b/content/browser/renderer_host/render_frame_host_delegate.cc
@@ -49,6 +49,7 @@
 }
 
 void RenderFrameHostDelegate::RequestMediaAccessPermission(
+    RenderFrameHostImpl* render_frame_host,
     const MediaStreamRequest& request,
     MediaResponseCallback callback) {
   LOG(ERROR) << "RenderFrameHostDelegate::RequestMediaAccessPermission: "
diff --git a/content/browser/renderer_host/render_frame_host_delegate.h b/content/browser/renderer_host/render_frame_host_delegate.h
index d9b10a46..d7c59ef 100644
--- a/content/browser/renderer_host/render_frame_host_delegate.h
+++ b/content/browser/renderer_host/render_frame_host_delegate.h
@@ -289,8 +289,10 @@
   // The render frame has requested access to media devices listed in
   // |request|, and the client should grant or deny that permission by
   // calling |callback|.
-  virtual void RequestMediaAccessPermission(const MediaStreamRequest& request,
-                                            MediaResponseCallback callback);
+  virtual void RequestMediaAccessPermission(
+      RenderFrameHostImpl* render_frame_host,
+      const MediaStreamRequest& request,
+      MediaResponseCallback callback);
 
   // Called when a renderer requests to select an audio output device.
   // |request| contains parameters for audio output device selection.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 429e12a..ac8f86a0 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -3978,11 +3978,10 @@
     if (previous_rfh) {
       // When migrating a frame to a new/different render process, use the frame
       // size we already have from the existing RenderFrameHost.
-      if (params->widget_params->visual_properties.new_size.IsZero()) {
-        float dsf = rwh->GetScreenInfo().device_scale_factor;
-        params->widget_params->visual_properties.new_size =
-            gfx::ScaleToRoundedSize(
-                previous_rfh->GetFrameSize().value_or(gfx::Size()), 1.f / dsf);
+      if (params->widget_params->visual_properties.new_size_device_px
+              .IsZero()) {
+        params->widget_params->visual_properties.new_size_device_px =
+            previous_rfh->GetFrameSize().value_or(gfx::Size());
       }
 
       params->widget_params->reuse_compositor =
diff --git a/content/browser/renderer_host/render_widget_host_browsertest.cc b/content/browser/renderer_host/render_widget_host_browsertest.cc
index c147ddf..3e45ef21 100644
--- a/content/browser/renderer_host/render_widget_host_browsertest.cc
+++ b/content/browser/renderer_host/render_widget_host_browsertest.cc
@@ -912,9 +912,12 @@
   WaitForVisualPropertiesAck();
   EXPECT_EQ(base::NumberToString(offset) + "px",
             EvalJs(shell(), "getComputedStyle(video).width").ExtractString());
-  EXPECT_EQ(
+  // Rounding of GetVisibleViewportSize in the presence of a non-integer
+  // devicePixelRatio device can make this off by one vs the video height.
+  EXPECT_NEAR(
       root_view_size.height(),
-      EvalJs(shell(), "parseInt(getComputedStyle(video).height)").ExtractInt());
+      EvalJs(shell(), "parseInt(getComputedStyle(video).height)").ExtractInt(),
+      1);
 
   emulated_display_feature.orientation =
       DisplayFeature::Orientation::kHorizontal;
@@ -925,21 +928,26 @@
   WaitForVisualPropertiesAck();
   EXPECT_EQ(base::NumberToString(offset) + "px",
             EvalJs(shell(), "getComputedStyle(video).height").ExtractString());
-  EXPECT_EQ(
+  EXPECT_NEAR(
       root_view_size.width(),
-      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt());
+      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt(),
+      1);
 
   // No display feature/viewport segments are set, the video should go
   // fullscreen.
   view()->SetDisplayFeatureForTesting(nullptr);
   host()->SynchronizeVisualProperties();
   WaitForVisualPropertiesAck();
-  EXPECT_EQ(
+  // Rounding of GetVisibleViewportSize in the presence of a non-integer
+  // devicePixelRatio device can make this off by one vs the video height.
+  EXPECT_NEAR(
       root_view_size.height(),
-      EvalJs(shell(), "parseInt(getComputedStyle(video).height)").ExtractInt());
-  EXPECT_EQ(
+      EvalJs(shell(), "parseInt(getComputedStyle(video).height)").ExtractInt(),
+      1);
+  EXPECT_NEAR(
       root_view_size.width(),
-      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt());
+      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt(),
+      1);
 
   constexpr char kExitFullscreenScript[] = R"JS(
     document.exitFullscreen().then(() => {
@@ -956,9 +964,10 @@
   WaitForVisualPropertiesAck();
   EXPECT_EQ(base::NumberToString(offset) + "px",
             EvalJs(shell(), "getComputedStyle(video).height").ExtractString());
-  EXPECT_EQ(
+  EXPECT_NEAR(
       root_view_size.width(),
-      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt());
+      EvalJs(shell(), "parseInt(getComputedStyle(video).width)").ExtractInt(),
+      1);
 }
 
 // Tests that the renderer receives the root widget's viewport segments and
diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc
index dde8e1e..d0135cf 100644
--- a/content/browser/renderer_host/render_widget_host_impl.cc
+++ b/content/browser/renderer_host/render_widget_host_impl.cc
@@ -1051,7 +1051,8 @@
   visual_properties.min_size_for_auto_resize = min_size_for_auto_resize_;
   visual_properties.max_size_for_auto_resize = max_size_for_auto_resize_;
 
-  visual_properties.new_size = view_->GetRequestedRendererSize();
+  visual_properties.new_size_device_px =
+      view_->GetRequestedRendererSizeDevicePx();
 
   // This widget is for a frame that is the main frame of the outermost frame
   // tree. That makes it the top-most frame. OR this is a non-frame widget.
@@ -1265,9 +1266,9 @@
     blink_widget_->UpdateVisualProperties(*visual_properties);
   }
 
-  bool width_changed =
-      !old_visual_properties_ || old_visual_properties_->new_size.width() !=
-                                     visual_properties->new_size.width();
+  bool width_changed = !old_visual_properties_ ||
+                       old_visual_properties_->new_size_device_px.width() !=
+                           visual_properties->new_size_device_px.width();
 
   // WidgetBase::UpdateSurfaceAndScreenInfo uses similar logic to detect
   // orientation changes on the display currently showing the widget.
@@ -2816,7 +2817,8 @@
            old_visual_properties.max_size_for_auto_resize !=
                new_visual_properties.max_size_for_auto_resize)) ||
          (!old_visual_properties.auto_resize_enabled &&
-          (old_visual_properties.new_size != new_visual_properties.new_size ||
+          (old_visual_properties.new_size_device_px !=
+               new_visual_properties.new_size_device_px ||
            (old_visual_properties.compositor_viewport_pixel_rect.IsEmpty() &&
             !new_visual_properties.compositor_viewport_pixel_rect.IsEmpty())));
 }
@@ -2832,7 +2834,7 @@
   bool is_acking_applicable =
       g_check_for_pending_visual_properties_ack &&
       !new_visual_properties.auto_resize_enabled &&
-      !new_visual_properties.new_size.IsEmpty() &&
+      !new_visual_properties.new_size_device_px.IsEmpty() &&
       !new_visual_properties.compositor_viewport_pixel_rect.IsEmpty() &&
       new_visual_properties.local_surface_id;
 
@@ -3026,7 +3028,7 @@
 
 void RenderWidgetHostImpl::UpdateElementFocusForStylusWriting(
 #if BUILDFLAG(IS_WIN)
-    const gfx::Rect& focus_rect_in_widget
+    const gfx::Rect& focus_widget_rect_in_dips
 #endif  // BUILDFLAG(IS_WIN)
 ) {
   if (blink_frame_widget_) {
@@ -3035,7 +3037,7 @@
         weak_factory_.GetWeakPtr());
     blink_frame_widget_->OnStartStylusWriting(
 #if BUILDFLAG(IS_WIN)
-        focus_rect_in_widget,
+        focus_widget_rect_in_dips,
 #endif  // BUILDFLAG(IS_WIN)
         std::move(callback));
   }
@@ -3049,7 +3051,7 @@
 #if BUILDFLAG(IS_WIN)
   if (focus_result && focus_result->proximate_bounds) {
     if (focus_result->proximate_bounds->range.length() !=
-        focus_result->proximate_bounds->bounds.size()) {
+        focus_result->proximate_bounds->widget_bounds_in_dips.size()) {
       mojo::ReportBadMessage("mismatched range and bounds length received");
       return;
     }
diff --git a/content/browser/renderer_host/render_widget_host_impl.h b/content/browser/renderer_host/render_widget_host_impl.h
index 5307d55..69da8efc0 100644
--- a/content/browser/renderer_host/render_widget_host_impl.h
+++ b/content/browser/renderer_host/render_widget_host_impl.h
@@ -818,7 +818,7 @@
   void OnStartStylusWriting() override;
   void UpdateElementFocusForStylusWriting(
 #if BUILDFLAG(IS_WIN)
-      const gfx::Rect& focus_rect_in_widget
+      const gfx::Rect& focus_widget_rect_in_dips
 #endif  // BUILDFLAG(IS_WIN)
       ) override;
   bool IsAutoscrollInProgress() override;
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index d5d80035..4740713 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -970,7 +970,8 @@
   view_->SetMockCompositorViewportPixelSize(gfx::Size());
   host_->SynchronizeVisualProperties();
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(original_size.size(), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(original_size.size(),
+            host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(1u, widget_.ReceivedVisualProperties().size());
 
@@ -981,7 +982,8 @@
   view_->ClearMockCompositorViewportPixelSize();
   host_->SynchronizeVisualProperties();
   EXPECT_TRUE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(original_size.size(), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(original_size.size(),
+            host_->old_visual_properties_->new_size_device_px);
   cc::RenderFrameMetadata metadata;
   metadata.viewport_size_in_pixels = original_size.size();
   metadata.local_surface_id = std::nullopt;
@@ -1021,7 +1023,8 @@
   static_cast<RenderFrameMetadataProvider::Observer&>(*host_)
       .OnLocalSurfaceIdChanged(metadata);
   EXPECT_TRUE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(third_size.size(), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(third_size.size(),
+            host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(1u, widget_.ReceivedVisualProperties().size());
 
@@ -1033,7 +1036,8 @@
   static_cast<RenderFrameMetadataProvider::Observer&>(*host_)
       .OnLocalSurfaceIdChanged(metadata);
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(third_size.size(), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(third_size.size(),
+            host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(0u, widget_.ReceivedVisualProperties().size());
 
@@ -1046,7 +1050,7 @@
   view_->SetBounds(gfx::Rect());
   host_->SynchronizeVisualProperties();
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(gfx::Size(), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(gfx::Size(), host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(1u, widget_.ReceivedVisualProperties().size());
 
@@ -1056,7 +1060,8 @@
   view_->SetBounds(gfx::Rect(0, 0, 0, 30));
   host_->SynchronizeVisualProperties();
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(gfx::Size(0, 30), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(gfx::Size(0, 30),
+            host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(1u, widget_.ReceivedVisualProperties().size());
 
@@ -1065,7 +1070,8 @@
   // Set the same size again. It should not be sent again.
   host_->SynchronizeVisualProperties();
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(gfx::Size(0, 30), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(gfx::Size(0, 30),
+            host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(0u, widget_.ReceivedVisualProperties().size());
 
@@ -1075,7 +1081,8 @@
   view_->SetBounds(gfx::Rect(0, 0, 0, 31));
   host_->SynchronizeVisualProperties();
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(gfx::Size(0, 31), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(gfx::Size(0, 31),
+            host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(1u, widget_.ReceivedVisualProperties().size());
 
@@ -1087,7 +1094,8 @@
   view_->InvalidateLocalSurfaceId();
   host_->SynchronizeVisualProperties();
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
-  EXPECT_EQ(gfx::Size(25, 25), host_->old_visual_properties_->new_size);
+  EXPECT_EQ(gfx::Size(25, 25),
+            host_->old_visual_properties_->new_size_device_px);
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(1u, widget_.ReceivedVisualProperties().size());
 }
@@ -1146,6 +1154,31 @@
   EXPECT_FALSE(host_->visual_properties_ack_pending_);
 }
 
+// Test that the reported new_size includes the scale factor.
+TEST_F(RenderWidgetHostTest, NewSizeIncludesScaleFactor) {
+  display::ScreenInfo screen_info;
+  screen_info.rect = gfx::Rect(0, 0, 800, 600);
+  screen_info.available_rect = gfx::Rect(0, 0, 800, 600);
+  screen_info.orientation_type =
+      display::mojom::ScreenOrientation::kPortraitPrimary;
+  screen_info.device_scale_factor = 2.f;
+
+  ClearVisualProperties();
+  view_->SetScreenInfo(screen_info);
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(0u, widget_.ReceivedVisualProperties().size());
+  gfx::Rect original_size(0, 0, 101, 100);
+  view_->SetBounds(original_size);
+  EXPECT_TRUE(host_->SynchronizeVisualProperties());
+  // blink::mojom::Widget::UpdateVisualProperties sent to the renderer.
+  base::RunLoop().RunUntilIdle();
+  ASSERT_EQ(1u, widget_.ReceivedVisualProperties().size());
+  auto new_size = widget_.ReceivedVisualProperties()[0].new_size_device_px;
+  EXPECT_EQ(202, new_size.width());
+  EXPECT_EQ(200, new_size.height());
+  EXPECT_TRUE(host_->visual_properties_ack_pending_);
+}
+
 // Ensure VisualProperties continues reporting the size of the current screen,
 // not the viewport, when the frame is fullscreen. See crbug.com/1367416.
 TEST_F(RenderWidgetHostTest, ScreenSizeInFullscreen) {
@@ -1171,7 +1204,7 @@
   blink::VisualProperties props = widget_.ReceivedVisualProperties().at(0);
   EXPECT_EQ(kScreenBounds, props.screen_infos.current().rect);
   EXPECT_EQ(kScreenBounds, props.screen_infos.current().available_rect);
-  EXPECT_EQ(kViewBounds.size(), props.new_size);
+  EXPECT_EQ(kViewBounds.size(), props.new_size_device_px);
 
   // Enter fullscreen and do another VisualProperties sync.
   delegate_->set_is_fullscreen(true);
@@ -1182,7 +1215,7 @@
   props = widget_.ReceivedVisualProperties().at(1);
   EXPECT_EQ(kScreenBounds, props.screen_infos.current().rect);
   EXPECT_EQ(kScreenBounds, props.screen_infos.current().available_rect);
-  EXPECT_EQ(kViewBounds.size(), props.new_size);
+  EXPECT_EQ(kViewBounds.size(), props.new_size_device_px);
 
   // Exit fullscreen and do another VisualProperties sync.
   delegate_->set_is_fullscreen(false);
@@ -1193,7 +1226,7 @@
   props = widget_.ReceivedVisualProperties().at(2);
   EXPECT_EQ(kScreenBounds, props.screen_infos.current().rect);
   EXPECT_EQ(kScreenBounds, props.screen_infos.current().available_rect);
-  EXPECT_EQ(kViewBounds.size(), props.new_size);
+  EXPECT_EQ(kViewBounds.size(), props.new_size_device_px);
 }
 
 TEST_F(RenderWidgetHostTest, RootViewportSegments) {
@@ -2149,7 +2182,7 @@
       compositor_viewport_pixel_rect.size());
 
   blink::VisualProperties visual_properties = host_->GetVisualProperties();
-  EXPECT_EQ(bounds.size(), visual_properties.new_size);
+  EXPECT_EQ(bounds.size(), visual_properties.new_size_device_px);
   EXPECT_EQ(compositor_viewport_pixel_rect,
             visual_properties.compositor_viewport_pixel_rect);
 }
@@ -2240,7 +2273,7 @@
   // SynchronizeVisualProperties calls should not result in new IPC (unless the
   // size has actually changed).
   EXPECT_FALSE(host_->SynchronizeVisualProperties());
-  EXPECT_EQ(initial_size_, host_->old_visual_properties_->new_size);
+  EXPECT_EQ(initial_size_, host_->old_visual_properties_->new_size_device_px);
   EXPECT_TRUE(host_->visual_properties_ack_pending_);
 }
 
@@ -2254,7 +2287,7 @@
   {
     // Size sent to the renderer.
     EXPECT_EQ(gfx::Size(100, 100),
-              widget_.ReceivedVisualProperties().at(0).new_size);
+              widget_.ReceivedVisualProperties().at(0).new_size_device_px);
   }
   // An ack is pending, throttling further updates.
   EXPECT_TRUE(host_->visual_properties_ack_pending_);
diff --git a/content/browser/renderer_host/render_widget_host_view_android.cc b/content/browser/renderer_host/render_widget_host_view_android.cc
index d72c1f4f..dd0c7c97 100644
--- a/content/browser/renderer_host/render_widget_host_view_android.cc
+++ b/content/browser/renderer_host/render_widget_host_view_android.cc
@@ -353,7 +353,7 @@
 
 void RenderWidgetHostViewAndroid::ScreenStateChangeHandler::
     BeginScreenStateChange() {
-  current_screen_state_.visible_viewport_size = rwhva_->view_.GetSize();
+  current_screen_state_.visible_viewport_size = rwhva_->view_.GetSizeDIPs();
   current_screen_state_.physical_backing_size =
       rwhva_->view_.GetPhysicalBackingSize();
   auto screen_info = rwhva_->GetScreenInfo();
@@ -1185,11 +1185,24 @@
   if (!view_.parent())
     return default_bounds_dip_;
 
-  gfx::Size size(view_.GetSize());
-
+  gfx::Size size(view_.GetSizeDIPs());
   return gfx::Rect(size);
 }
 
+gfx::Size RenderWidgetHostViewAndroid::GetRequestedRendererSizeDevicePx() {
+  if (!view_.parent()) {
+    if (default_bounds_dip_.IsEmpty()) {
+      return gfx::Size();
+    }
+
+    const float scale_factor = GetDeviceScaleFactor();
+    return gfx::Size(default_bounds_dip_.width() * scale_factor,
+                     default_bounds_dip_.height() * scale_factor);
+  }
+
+  return view_.GetSizeDevicePx();
+}
+
 gfx::Size RenderWidgetHostViewAndroid::GetVisibleViewportSize() {
   int pinned_bottom_adjust_dps =
       std::max(0, (int)(view_.GetViewportInsetBottom() / view_.GetDipScale()));
@@ -2471,8 +2484,8 @@
     // TODO(yusufo) : Get rid of the below conditions and have a better handling
     // for resizing after crbug.com/628302 is handled.
     bool is_size_initialized = !will_build_tree ||
-                               view_.GetSize().width() != 0 ||
-                               view_.GetSize().height() != 0;
+                               view_.GetSizeDIPs().width() != 0 ||
+                               view_.GetSizeDIPs().height() != 0;
     if (has_view_tree || is_size_initialized)
       resize = true;
     has_view_tree = will_build_tree;
@@ -2596,7 +2609,8 @@
 }
 
 void RenderWidgetHostViewAndroid::OnSizeChanged() {
-  screen_state_change_handler_.OnVisibleViewportSizeChanged(view_.GetSize());
+  screen_state_change_handler_.OnVisibleViewportSizeChanged(
+      view_.GetSizeDIPs());
   // The display feature depends on the view size so we need to recompute it.
   ComputeDisplayFeature();
 }
@@ -2946,7 +2960,7 @@
   }
 
   display_feature_ = std::nullopt;
-  gfx::Size view_size(view_.GetSize());
+  gfx::Size view_size(view_.GetSizeDIPs());
   // On some devices like the Galaxy Fold the display feature has a size of
   // 0 (width or height depending on the orientation). IsEmpty() will fail here.
   if (display_feature_bounds_.size().IsZero() || view_size.IsEmpty()) {
diff --git a/content/browser/renderer_host/render_widget_host_view_android.h b/content/browser/renderer_host/render_widget_host_view_android.h
index ef7bbda..f74e5e4 100644
--- a/content/browser/renderer_host/render_widget_host_view_android.h
+++ b/content/browser/renderer_host/render_widget_host_view_android.h
@@ -155,6 +155,7 @@
   void Hide() override;
   bool IsShowing() override;
   gfx::Rect GetViewBounds() override;
+  gfx::Size GetRequestedRendererSizeDevicePx() override;
   gfx::Size GetVisibleViewportSize() override;
   void SetInsets(const gfx::Insets& insets) override;
   gfx::Size GetCompositorViewportPixelSize() override;
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc
index 0668684..2f95024 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -754,8 +754,6 @@
   CHECK(GetBackgroundColor());
 
   SkColor color = *GetBackgroundColor();
-  bool opaque = SkColorGetA(color) == SK_AlphaOPAQUE;
-  window_->layer()->SetFillsBoundsOpaquely(opaque);
   window_->layer()->SetColor(color);
 }
 
@@ -1498,6 +1496,16 @@
                    base::ClampSub(end.y(), origin.y()));
 }
 
+gfx::Point RenderWidgetHostViewAura::ConvertPointFromScreen(
+    const gfx::Point& screen_point_in_dips) const {
+  gfx::Point result = screen_point_in_dips;
+  if (window_->GetRootWindow() &&
+      aura::client::GetScreenPositionClient(window_->GetRootWindow())) {
+    wm::ConvertPointFromScreen(window_, &result);
+  }
+  return result;
+}
+
 gfx::Rect RenderWidgetHostViewAura::ConvertRectFromScreen(
     const gfx::Rect& rect) const {
   gfx::Rect result = rect;
@@ -1557,7 +1565,7 @@
   std::optional<gfx::Rect> result;
   for (size_t i = range.start(); i < range.end(); ++i) {
     const gfx::Rect& rect_for_index =
-        proximate->bounds[i - proximate->range.start()];
+        proximate->widget_bounds_in_dips[i - proximate->range.start()];
     if (result.has_value()) {
       result->UnionEvenIfEmpty(rect_for_index);
     } else {
@@ -1572,7 +1580,7 @@
 
 std::optional<size_t>
 RenderWidgetHostViewAura::GetProximateCharacterIndexFromPoint(
-    const gfx::Point& point,
+    const gfx::Point& screen_point_in_dips,
     ui::IndexFromPointFlags flags) const {
   if (!text_input_manager_ || !text_input_manager_->GetActiveWidget()) {
     return std::nullopt;
@@ -1593,19 +1601,18 @@
   bool any_contain_point = false;
   size_t nearest_index = 0U;
   int64_t nearest_distance_sq = std::numeric_limits<int64_t>::max();
-  const HWND host_hwnd = GetHostWindowHWND();
 
-  for (size_t i = 0; i < proximate->bounds.size(); ++i) {
-    const gfx::Rect bounds_in_screen_coord =
-        display::win::ScreenWin::DIPToScreenRect(
-            host_hwnd, ConvertRectToScreen(proximate->bounds[i]));
+  const gfx::Point widget_point_in_dips =
+      ConvertPointFromScreen(screen_point_in_dips);
+  for (size_t i = 0; i < proximate->widget_bounds_in_dips.size(); ++i) {
+    const gfx::Rect& bounds = proximate->widget_bounds_in_dips[i];
     if (!any_contain_point) {
-      any_contain_point = bounds_in_screen_coord.Contains(point);
+      any_contain_point = bounds.Contains(widget_point_in_dips);
     }
     // When kNearestToContainedPoint is included, this can't early return
     // because we need to check to see if there's a character that's closer to
     // the point. kNearestToUncontainedPoint only applies when a character
-    // doesn't contain `point`, so this can early return when
+    // doesn't contain `widget_point_in_dips`, so this can early return when
     // kNearestToContainedPoint isn't included.
     if (any_contain_point && !nearest_to_contained_point) {
       return proximate->range.start() + i;
@@ -1613,12 +1620,13 @@
 
     // When either flag is provided, we need to perform distance checks in case
     // either of them apply. Ideally this wouldn't need to iterate over all
-    // characters to determine whether one of them contains `point`, but the
-    // current implementation lacks any form of acceleration structures or
-    // such as spatial partitioning which could make this faster. There's no
-    // guarantee that character indices will be laid out spatially contiguously,
-    // so it's also not possible to reliably perform a any sort of binary search
-    // based on the character bounds. For example, nested `float: right;` text.
+    // characters to determine whether one of them contains
+    // `widget_point_in_dips`, but the current implementation lacks any form of
+    // acceleration structures or such as spatial partitioning which could make
+    // this faster. There's no guarantee that character indices will be laid out
+    // spatially contiguously, so it's also not possible to reliably perform a
+    // any sort of binary search based on the character bounds. For example,
+    // nested `float: right;` text.
     if (flags != ui::IndexFromPointFlags::kNone) {
       // For kNearestToContainedPoint, it's unclear from the API documentation
       // whether this expects the "nearest" to only consider characters on the
@@ -1629,7 +1637,7 @@
       // lines it's possible that a character offset for an adjacent line of
       // text may be picked.
       const int64_t distance_sq =
-          (bounds_in_screen_coord.left_center() - point).LengthSquared();
+          (bounds.left_center() - widget_point_in_dips).LengthSquared();
       if (distance_sq < nearest_distance_sq) {
         nearest_index = proximate->range.start() + i;
         nearest_distance_sq = distance_sq;
@@ -2390,12 +2398,12 @@
 }
 
 void RenderWidgetHostViewAura::OnFocusHandwritingTarget(
-    const gfx::Rect& rect_in_screen,
-    const gfx::Size& distance_tolerance) {
-  // TODO(crbug.com/355578906): Consider `distance_tolerance`.
+    const gfx::Rect& focus_screen_rect_in_dips,
+    const gfx::Size& tolerance_screen_distance_in_dips) {
+  // TODO(crbug.com/355578906): Consider `tolerance_screen_distance_in_dips`.
   if (host()) {
     host()->UpdateElementFocusForStylusWriting(
-        ConvertRectFromScreen(rect_in_screen));
+        ConvertRectFromScreen(focus_screen_rect_in_dips));
   }
 }
 #endif  // BUILDFLAG(IS_WIN)
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.h b/content/browser/renderer_host/render_widget_host_view_aura.h
index 564a870..5410c9d 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.h
+++ b/content/browser/renderer_host/render_widget_host_view_aura.h
@@ -246,7 +246,7 @@
   std::optional<gfx::Rect> GetProximateCharacterBounds(
       const gfx::Range& range) const override;
   std::optional<size_t> GetProximateCharacterIndexFromPoint(
-      const gfx::Point& point,
+      const gfx::Point& screen_point_in_dips,
       ui::IndexFromPointFlags flags) const override;
 #endif  // BUILDFLAG(IS_WIN)
   bool GetCompositionCharacterBounds(size_t index,
@@ -661,6 +661,10 @@
   // parent.
   void ApplyEventObserverForPopupExit(const ui::LocatedEvent& event);
 
+  // Converts |screen_point| from screen coordinate to window coordinate.
+  gfx::Point ConvertPointFromScreen(
+      const gfx::Point& screen_point_in_dips) const;
+
   // Converts |rect| from screen coordinate to window coordinate.
   gfx::Rect ConvertRectFromScreen(const gfx::Rect& rect) const;
 
@@ -719,8 +723,9 @@
 
   // Invoked on Shell Handwriting API request to update the element's focus
   // based on the provided rect and the distance tolerance.
-  void OnFocusHandwritingTarget(const gfx::Rect& rect_in_screen,
-                                const gfx::Size& distance_tolerance);
+  void OnFocusHandwritingTarget(
+      const gfx::Rect& focus_screen_rect_in_dips,
+      const gfx::Size& tolerance_screen_distance_in_dips);
 #endif  // BUILDFLAG(IS_WIN)
 
   raw_ptr<aura::Window> window_;
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
index bdb00be..d695762a 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
@@ -2616,7 +2616,7 @@
 
   view_->SetSize(gfx::Size(100, 100));
 
-  // Physical pixel size.
+  // Device pixel size.
   EXPECT_EQ(gfx::Size(100, 100), view_->GetCompositorViewportPixelSize());
   // Update to the renderer.
   base::RunLoop().RunUntilIdle();
@@ -2624,9 +2624,9 @@
   {
     blink::VisualProperties visual_properties =
         widget_host_->visual_properties().at(0);
-    // DIP size.
-    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size);
-    // Physical pixel size.
+    // Device pixel size.
+    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size_device_px);
+    // Device pixel size.
     EXPECT_EQ(gfx::Size(100, 100),
               visual_properties.compositor_viewport_pixel_rect.size());
   }
@@ -2641,11 +2641,11 @@
   sink_->ClearMessages();
   widget_host_->ClearVisualProperties();
 
-  // Device scale factor changes to 2, so the physical pixel sizes should
+  // Device scale factor changes to 2, so the device pixel sizes should
   // change, while the DIP sizes do not.
 
   aura_test_helper_->GetTestScreen()->SetDeviceScaleFactor(2.0f);
-  // Physical pixel size.
+  // Device pixel size.
   EXPECT_EQ(gfx::Size(200, 200), view_->GetCompositorViewportPixelSize());
   // Update to the renderer.
   base::RunLoop().RunUntilIdle();
@@ -2653,9 +2653,9 @@
   {
     blink::VisualProperties visual_properties =
         widget_host_->visual_properties().at(0);
-    // DIP size.
-    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size);
-    // Physical pixel size.
+    // Device pixel size.
+    EXPECT_EQ(gfx::Size(200, 200), visual_properties.new_size_device_px);
+    // Device pixel size.
     EXPECT_EQ(gfx::Size(200, 200),
               visual_properties.compositor_viewport_pixel_rect.size());
   }
@@ -2671,7 +2671,7 @@
 
   aura_test_helper_->GetTestScreen()->SetDeviceScaleFactor(1.0f);
 
-  // Physical pixel size.
+  // Device pixel size.
   EXPECT_EQ(gfx::Size(100, 100), view_->GetCompositorViewportPixelSize());
   // Update to the renderer.
   base::RunLoop().RunUntilIdle();
@@ -2680,8 +2680,8 @@
     blink::VisualProperties visual_properties =
         widget_host_->visual_properties().at(0);
     // DIP size.
-    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size);
-    // Physical pixel size.
+    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size_device_px);
+    // Device pixel size.
     EXPECT_EQ(gfx::Size(100, 100),
               visual_properties.compositor_viewport_pixel_rect.size());
   }
@@ -2780,7 +2780,7 @@
   EXPECT_EQ(1u, widget_host_->visual_properties().size());
   const auto& received_property = widget_host_->visual_properties()[0];
   EXPECT_EQ(false, received_property.auto_resize_enabled);
-  EXPECT_EQ(size_after_disabling, received_property.new_size);
+  EXPECT_EQ(size_after_disabling, received_property.new_size_device_px);
 }
 
 // This test verifies that in AutoResize mode a new
@@ -2837,7 +2837,7 @@
     // Auto-resizve limits sent to the renderer.
     EXPECT_EQ(gfx::Size(50, 50), visual_properties.min_size_for_auto_resize);
     EXPECT_EQ(gfx::Size(100, 100), visual_properties.max_size_for_auto_resize);
-    EXPECT_EQ(gfx::Size(120, 120), visual_properties.new_size);
+    EXPECT_EQ(gfx::Size(120, 120), visual_properties.new_size_device_px);
     EXPECT_EQ(1, visual_properties.screen_infos.current().device_scale_factor);
     // A newly generated LocalSurfaceId is sent.
     EXPECT_TRUE(visual_properties.local_surface_id.has_value());
@@ -3098,7 +3098,7 @@
     blink::VisualProperties visual_properties =
         widget_host_->visual_properties().at(0);
     // Empty size is sent.
-    EXPECT_EQ(gfx::Size(), visual_properties.new_size);
+    EXPECT_EQ(gfx::Size(), visual_properties.new_size_device_px);
     // A LocalSurfaceId is sent too.
     ASSERT_TRUE(visual_properties.local_surface_id.has_value());
     EXPECT_TRUE(visual_properties.local_surface_id->is_valid());
@@ -3175,7 +3175,7 @@
   EXPECT_TRUE(widget_host_->visual_properties_ack_pending_for_testing());
   base::RunLoop().RunUntilIdle();
   ASSERT_EQ(1u, widget_host_->visual_properties().size());
-  EXPECT_EQ(size2, widget_host_->visual_properties().at(0).new_size);
+  EXPECT_EQ(size2, widget_host_->visual_properties().at(0).new_size_device_px);
   // Render should send back RenderFrameMetadata with new size.
   {
     cc::RenderFrameMetadata metadata;
@@ -3433,7 +3433,7 @@
   {
     blink::VisualProperties visual_properties =
         widget_host_->visual_properties().at(0);
-    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size);
+    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size_device_px);
     EXPECT_EQ(gfx::Size(100, 100), visual_properties.visible_viewport_size);
   }
 
@@ -3455,7 +3455,7 @@
   {
     blink::VisualProperties visual_properties =
         widget_host_->visual_properties().at(0);
-    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size);
+    EXPECT_EQ(gfx::Size(100, 100), visual_properties.new_size_device_px);
     EXPECT_EQ(gfx::Size(100, 60), visual_properties.visible_viewport_size);
   }
 }
diff --git a/content/browser/renderer_host/render_widget_host_view_base.cc b/content/browser/renderer_host/render_widget_host_view_base.cc
index 2fb637bb..51f8361 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.cc
+++ b/content/browser/renderer_host/render_widget_host_view_base.cc
@@ -140,6 +140,11 @@
   return GetViewBounds().size();
 }
 
+gfx::Size RenderWidgetHostViewBase::GetRequestedRendererSizeDevicePx() {
+  return gfx::ScaleToCeiledSize(GetRequestedRendererSize(),
+                                GetDeviceScaleFactor());
+}
+
 uint32_t RenderWidgetHostViewBase::GetCaptureSequenceNumber() const {
   // TODO(vmpstr): Implement this for overrides other than aura and child frame.
   NOTIMPLEMENTED_LOG_ONCE();
@@ -564,7 +569,7 @@
 void RenderWidgetHostViewBase::ResetGestureDetection() {}
 
 float RenderWidgetHostViewBase::GetDeviceScaleFactor() const {
-  return screen_infos_.current().device_scale_factor;
+  return GetScreenInfos().current().device_scale_factor;
 }
 
 base::WeakPtr<input::RenderWidgetHostViewInput>
diff --git a/content/browser/renderer_host/render_widget_host_view_base.h b/content/browser/renderer_host/render_widget_host_view_base.h
index 8377a897..2a2edc60 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.h
+++ b/content/browser/renderer_host/render_widget_host_view_base.h
@@ -236,6 +236,7 @@
   // The requested size of the renderer. May differ from GetViewBounds().size()
   // when the view requires additional throttling.
   virtual gfx::Size GetRequestedRendererSize();
+  virtual gfx::Size GetRequestedRendererSizeDevicePx();
 
   // Returns the current capture sequence number.
   virtual uint32_t GetCaptureSequenceNumber() const;
diff --git a/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc b/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
index 2c733a7..da43859 100644
--- a/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
@@ -324,7 +324,8 @@
 
     EXPECT_EQ(compositor_viewport_pixel_rect,
               sent_visual_properties.compositor_viewport_pixel_rect);
-    EXPECT_EQ(rect_in_local_root.size(), sent_visual_properties.new_size);
+    EXPECT_EQ(rect_in_local_root.size(),
+              sent_visual_properties.new_size_device_px);
     EXPECT_EQ(local_surface_id, sent_visual_properties.local_surface_id);
     EXPECT_EQ(123u, sent_visual_properties.capture_sequence_number);
     EXPECT_EQ(1u, sent_visual_properties.root_widget_viewport_segments.size());
diff --git a/content/browser/renderer_host/scroll_into_view_browsertest.cc b/content/browser/renderer_host/scroll_into_view_browsertest.cc
index 51ab2bf..b9bb46d 100644
--- a/content/browser/renderer_host/scroll_into_view_browsertest.cc
+++ b/content/browser/renderer_host/scroll_into_view_browsertest.cc
@@ -796,7 +796,7 @@
   RunTest();
 
   // width=device-width must prevent the zooming behavior.
-  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
+  EXPECT_LE(kMobileMinimumScale, GetVisualViewport().scale);
 }
 
 // Similar to above, an input in a touch-action region that disables pinch-zoom
diff --git a/content/browser/site_per_process_browsertest.cc b/content/browser/site_per_process_browsertest.cc
index 47afc760..54f24878 100644
--- a/content/browser/site_per_process_browsertest.cc
+++ b/content/browser/site_per_process_browsertest.cc
@@ -9427,7 +9427,7 @@
   // Avoid having the root try to handle the following event.
   root_view->set_event_handler(nullptr);
 
-  auto size = root_view->GetSize();
+  auto size = root_view->GetSizeDIPs();
   float x = size.width() / 2;
   float y = size.height() / 2;
   ui::MotionEventAndroid::Pointer pointer0(0, x, y, 0, 0, 0, 0, 0);
diff --git a/content/browser/web_contents/file_chooser_impl.cc b/content/browser/web_contents/file_chooser_impl.cc
index 64a4910..479cce8 100644
--- a/content/browser/web_contents/file_chooser_impl.cc
+++ b/content/browser/web_contents/file_chooser_impl.cc
@@ -29,7 +29,7 @@
     std::vector<blink::mojom::FileChooserFileInfoPtr> files,
     base::FilePath base_dir) {
   DCHECK(!base_dir.empty());
-  auto new_end = base::ranges::remove_if(
+  auto to_remove = std::ranges::remove_if(
       files,
       [&base_dir](const base::FilePath& file_path) {
         if (base::IsLink(file_path))
@@ -42,7 +42,7 @@
         return false;
       },
       [](const auto& file) { return file->get_native_file()->file_path; });
-  files.erase(new_end, files.end());
+  files.erase(to_remove.begin(), to_remove.end());
   return files;
 }
 
diff --git a/content/browser/web_contents/web_contents_android.cc b/content/browser/web_contents/web_contents_android.cc
index fc486e03..2776494c 100644
--- a/content/browser/web_contents/web_contents_android.cc
+++ b/content/browser/web_contents/web_contents_android.cc
@@ -826,11 +826,11 @@
 }
 
 int WebContentsAndroid::GetWidth(JNIEnv* env) {
-  return web_contents_->GetNativeView()->GetSize().width();
+  return web_contents_->GetNativeView()->GetSizeDIPs().width();
 }
 
 int WebContentsAndroid::GetHeight(JNIEnv* env) {
-  return web_contents_->GetNativeView()->GetSize().height();
+  return web_contents_->GetNativeView()->GetSizeDIPs().height();
 }
 
 ScopedJavaLocalRef<jobject> WebContentsAndroid::GetOrCreateEventForwarder(
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 84e5d2e..27c675d 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -5385,6 +5385,7 @@
 }
 
 void WebContentsImpl::RequestMediaAccessPermission(
+    RenderFrameHostImpl* render_frame_host,
     const MediaStreamRequest& request,
     MediaResponseCallback callback) {
   OPTIONAL_TRACE_EVENT2("content",
@@ -5392,14 +5393,22 @@
                         "render_process_id", request.render_process_id,
                         "render_frame_id", request.render_frame_id);
 
-  if (delegate_) {
+  if (GuestPageHolderImpl* guest =
+          render_frame_host
+              ? GuestPageHolderImpl::FromRenderFrameHost(*render_frame_host)
+              : nullptr) {
+    if (auto* delegate = guest->delegate()) {
+      delegate->GuestRequestMediaAccessPermission(request, std::move(callback));
+      return;
+    }
+  } else if (delegate_) {
     delegate_->RequestMediaAccessPermission(this, request, std::move(callback));
-  } else {
-    std::move(callback).Run(
-        blink::mojom::StreamDevicesSet(),
-        blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN,
-        std::unique_ptr<MediaStreamUI>());
+    return;
   }
+  std::move(callback).Run(
+      blink::mojom::StreamDevicesSet(),
+      blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN,
+      std::unique_ptr<MediaStreamUI>());
 }
 
 void WebContentsImpl::ProcessSelectAudioOutput(
@@ -5426,6 +5435,15 @@
 
   DCHECK(type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE ||
          type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
+
+  if (auto* guest =
+          GuestPageHolderImpl::FromRenderFrameHost(*render_frame_host)) {
+    if (auto* delegate = guest->delegate()) {
+      return delegate->GuestCheckMediaAccessPermission(render_frame_host,
+                                                       security_origin, type);
+    }
+    return false;
+  }
   return delegate_ && delegate_->CheckMediaAccessPermission(
                           render_frame_host, security_origin, type);
 }
@@ -10200,7 +10218,7 @@
   return window->bounds().size();
 #elif BUILDFLAG(IS_ANDROID)
   ui::ViewAndroid* view_android = GetNativeView();
-  return view_android->GetSize();
+  return view_android->GetSizeDIPs();
 #elif BUILDFLAG(IS_IOS)
   // TODO(crbug.com/40254930): Implement me.
   NOTREACHED();
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index a53ffb2..2e46f66 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -959,7 +959,8 @@
       RenderFrameHostImpl* frame_host,
       mojo::PendingAssociatedReceiver<media::mojom::MediaPlayerHost> receiver)
       override;
-  void RequestMediaAccessPermission(const MediaStreamRequest& request,
+  void RequestMediaAccessPermission(RenderFrameHostImpl* render_frame_host,
+                                    const MediaStreamRequest& request,
                                     MediaResponseCallback callback) override;
 
   void ProcessSelectAudioOutput(const SelectAudioOutputRequest& request,
diff --git a/content/browser/web_contents/web_contents_impl_unittest.cc b/content/browser/web_contents/web_contents_impl_unittest.cc
index 1c1b4e68b..6b87463 100644
--- a/content/browser/web_contents/web_contents_impl_unittest.cc
+++ b/content/browser/web_contents/web_contents_impl_unittest.cc
@@ -3383,7 +3383,7 @@
       /*captured_surface_control_active=*/false);
   bool callback_run = false;
   contents()->RequestMediaAccessPermission(
-      dummy_request,
+      contents()->GetPrimaryMainFrame(), dummy_request,
       base::BindLambdaForTesting(
           [&callback_run](
               const blink::mojom::StreamDevicesSet& stream_devices_set,
diff --git a/content/browser/web_contents/web_contents_view_android.cc b/content/browser/web_contents/web_contents_view_android.cc
index b088103..37b434da 100644
--- a/content/browser/web_contents/web_contents_view_android.cc
+++ b/content/browser/web_contents/web_contents_view_android.cc
@@ -248,7 +248,7 @@
 }
 
 gfx::Rect WebContentsViewAndroid::GetViewBounds() const {
-  return gfx::Rect(view_.GetSize());
+  return gfx::Rect(view_.GetSizeDIPs());
 }
 
 void WebContentsViewAndroid::CreateView(gfx::NativeView context) {}
diff --git a/content/public/browser/guest_page_holder.h b/content/public/browser/guest_page_holder.h
index eb6f7f7..ca2ae22 100644
--- a/content/public/browser/guest_page_holder.h
+++ b/content/public/browser/guest_page_holder.h
@@ -12,6 +12,7 @@
 #include "base/process/kill.h"
 #include "base/supports_user_data.h"
 #include "content/common/content_export.h"
+#include "content/public/browser/media_stream_request.h"
 #include "ui/base/window_open_disposition.h"
 #include "url/gurl.h"
 
@@ -101,6 +102,19 @@
     // Close the current window.
     virtual void GuestClose() = 0;
 
+    // Asks permission to use the camera and/or microphone.
+    // See `WebContentsDelegate::RequestMediaAccessPermission`
+    virtual void GuestRequestMediaAccessPermission(
+        const MediaStreamRequest& request,
+        MediaResponseCallback callback) = 0;
+
+    // Checks if we have permission to access the microphone or camera.
+    // See `WebContentsDelegate::CheckMediaAccessPermission`
+    virtual bool GuestCheckMediaAccessPermission(
+        RenderFrameHost* render_frame_host,
+        const url::Origin& security_origin,
+        blink::mojom::MediaStreamType type) = 0;
+
     // TODO(40202416): Guest implementations need to be informed of several
     // other events that they currently get through primary main frame specific
     // WebContentsObserver methods (e.g.
diff --git a/content/public/test/fake_frame_widget.h b/content/public/test/fake_frame_widget.h
index 429be72..dbb7395 100644
--- a/content/public/test/fake_frame_widget.h
+++ b/content/public/test/fake_frame_widget.h
@@ -61,7 +61,7 @@
   void DragSourceSystemDragEnded() override {}
   void OnStartStylusWriting(
 #if BUILDFLAG(IS_WIN)
-      const gfx::Rect& focus_rect_in_widget,
+      const gfx::Rect& focus_widget_rect_in_dips,
 #endif  // BUILDFLAG(IS_WIN)
       OnStartStylusWritingCallback callback) override {
   }
diff --git a/content/public/test/render_view_test.cc b/content/public/test/render_view_test.cc
index 16f4458a..85ce29e 100644
--- a/content/public/test/render_view_test.cc
+++ b/content/public/test/render_view_test.cc
@@ -771,7 +771,7 @@
 void RenderViewTest::Resize(gfx::Size new_size, bool is_fullscreen_granted) {
   blink::VisualProperties visual_properties;
   visual_properties.screen_infos = display::ScreenInfos(display::ScreenInfo());
-  visual_properties.new_size = new_size;
+  visual_properties.new_size_device_px = new_size;
   visual_properties.compositor_viewport_pixel_rect = gfx::Rect(new_size);
   visual_properties.is_fullscreen_granted = is_fullscreen_granted;
   visual_properties.display_mode = blink::mojom::DisplayMode::kBrowser;
@@ -880,7 +880,7 @@
   initial_visual_properties.screen_infos =
       display::ScreenInfos(display::ScreenInfo());
   // Ensure the view has some size so tests involving scrolling bounds work.
-  initial_visual_properties.new_size = gfx::Size(400, 300);
+  initial_visual_properties.new_size_device_px = gfx::Size(400, 300);
   initial_visual_properties.visible_viewport_size = gfx::Size(400, 300);
   return initial_visual_properties;
 }
diff --git a/content/renderer/render_frame_impl_browsertest.cc b/content/renderer/render_frame_impl_browsertest.cc
index d0d72072..0531485d 100644
--- a/content/renderer/render_frame_impl_browsertest.cc
+++ b/content/renderer/render_frame_impl_browsertest.cc
@@ -122,7 +122,7 @@
     mojom::CreateFrameWidgetParamsPtr widget_params =
         mojom::CreateFrameWidgetParams::New();
     widget_params->routing_id = kSubframeWidgetRouteId;
-    widget_params->visual_properties.new_size = gfx::Size(100, 100);
+    widget_params->visual_properties.new_size_device_px = gfx::Size(100, 100);
     widget_params->visual_properties.screen_infos =
         display::ScreenInfos(display::ScreenInfo());
 
@@ -286,7 +286,7 @@
   visual_properties.screen_infos = display::ScreenInfos(display::ScreenInfo());
   gfx::Size widget_size(400, 200);
   gfx::Size visible_size(350, 170);
-  visual_properties.new_size = widget_size;
+  visual_properties.new_size_device_px = widget_size;
   visual_properties.compositor_viewport_pixel_rect = gfx::Rect(widget_size);
   visual_properties.visible_viewport_size = visible_size;
 
diff --git a/content/renderer/render_view_browsertest.cc b/content/renderer/render_view_browsertest.cc
index 17bbb44..140ec647 100644
--- a/content/renderer/render_view_browsertest.cc
+++ b/content/renderer/render_view_browsertest.cc
@@ -548,9 +548,11 @@
     visual_properties.screen_infos =
         display::ScreenInfos(display::ScreenInfo());
     visual_properties.screen_infos.mutable_current().device_scale_factor = dsf;
-    visual_properties.new_size = gfx::Size(100, 100);
+    visual_properties.new_size_device_px =
+        gfx::ScaleToCeiledSize(gfx::Size(100, 100), dsf);
     visual_properties.compositor_viewport_pixel_rect = gfx::Rect(200, 200);
-    visual_properties.visible_viewport_size = visual_properties.new_size;
+    visual_properties.visible_viewport_size =
+        visual_properties.new_size_device_px;
     visual_properties.auto_resize_enabled = web_view_->AutoResizeMode();
     visual_properties.min_size_for_auto_resize = min_size_for_autoresize_;
     visual_properties.max_size_for_auto_resize = max_size_for_autoresize_;
diff --git a/content/renderer/render_widget_browsertest.cc b/content/renderer/render_widget_browsertest.cc
index 4874ea3f..89b9fde 100644
--- a/content/renderer/render_widget_browsertest.cc
+++ b/content/renderer/render_widget_browsertest.cc
@@ -68,7 +68,7 @@
  protected:
   blink::VisualProperties InitialVisualProperties() override {
     blink::VisualProperties initial_visual_properties;
-    initial_visual_properties.new_size = initial_size_;
+    initial_visual_properties.new_size_device_px = initial_size_;
     initial_visual_properties.compositor_viewport_pixel_rect =
         gfx::Rect(initial_size_);
     initial_visual_properties.local_surface_id =
diff --git a/content/services/auction_worklet/public/cpp/auction_worklet_features.cc b/content/services/auction_worklet/public/cpp/auction_worklet_features.cc
index 06ffb53..95343fd8 100644
--- a/content/services/auction_worklet/public/cpp/auction_worklet_features.cc
+++ b/content/services/auction_worklet/public/cpp/auction_worklet_features.cc
@@ -19,7 +19,7 @@
 
 BASE_FEATURE(kFledgeAuctionDownloaderStaleWhileRevalidate,
              "FledgeAuctionDownloaderStaleWhileRevalidate",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 BASE_FEATURE(kFledgeEagerJSCompilation,
              "FledgeEagerJSCompilation",
diff --git a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
index 302f41c..2fe85c5 100644
--- a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
@@ -326,12 +326,14 @@
 crbug.com/1268144 [ fuchsia fuchsia-board-qemu-x64 renderer-skia-vulkan ] Pixel_BackgroundImage [ Skip ]
 crbug.com/1268144 [ fuchsia fuchsia-board-qemu-x64 renderer-skia-vulkan ] Pixel_SolidColorBackground [ Skip ]
 
-# Nexus 5x consistently fail for View transition capture on webview.
-crbug.com/349071740 [ android-webview-instrumentation android-nexus-5x ] Pixel_ViewTransitionsCapture [ Failure ]
-
 # Flaky timeout on Linux
 crbug.com/364179193 [ linux ] Pixel_Video_Media_Stream_Incompatible_Stride [ Skip ]
 
+crbug.com/1345777 [ android ] Pixel_VideoStreamFromWebGLCanvas [ Skip ]
+
+crbug.com/1456360 [ chromeos chromeos-board-amd64-generic ] Pixel_WebGLCopyImage [ Skip ]
+crbug.com/1456360 [ chromeos chromeos-board-amd64-generic ] Pixel_WebGLSadCanvas [ Skip ]
+
 ###############################
 # Permanent Slow Expectations #
 ###############################
@@ -379,6 +381,9 @@
 ###################
 # Non-"Skip" expectations go here to suppress regular flakes/failures.
 
+# Nexus 5x consistently fail for View transition capture on webview.
+crbug.com/349071740 [ android-webview-instrumentation android-nexus-5x ] Pixel_ViewTransitionsCapture [ Failure ]
+
 crbug.com/624256 [ android ] Pixel_SolidColorBackground [ Failure ]
 
 # Mark all webview tests as RetryOnFailure due to Nexus 5x driver bug.
@@ -390,7 +395,6 @@
 # investigate later. Filed a bug to track the debug.
 crbug.com/984734 [ android android-webview-instrumentation android-nexus-5x ] Pixel_Video_Context_Loss_VP9 [ Failure ]
 
-
 # Times out / fails on Pixel 4s in webview.
 crbug.com/1176918 [ android android-pixel-4 android-webview-instrumentation renderer-skia-gl ] Pixel_Video_Context_Loss_VP9 [ Failure ]
 
@@ -409,25 +413,14 @@
 crbug.com/1290953 [ android android-pixel-4 android-webview-instrumentation ] Pixel_CanvasLowLatency2DDrawImage [ Failure ]
 crbug.com/1290953 [ android android-pixel-4 android-webview-instrumentation ] Pixel_CanvasLowLatency2DImageData [ Failure ]
 
-crbug.com/1345777 [ android ] Pixel_VideoStreamFromWebGLCanvas [ Skip ]
-
 crbug.com/40241616 [ android android-sm-a137f ] Pixel_CanvasLowLatencyWebGLAlphaFalse [ Failure ]
 crbug.com/1372141 [ android android-sm-a235m ] Pixel_CanvasLowLatencyWebGLAlphaFalse [ Failure ]
 
 crbug.com/1376927 [ win10 target-cpu-32 nvidia-0x2184 ] Pixel_MediaFoundationClearDirectComposition [ Failure ]
 
-
-
-
-crbug.com/1456360 [ chromeos chromeos-board-amd64-generic ] Pixel_WebGLCopyImage [ Skip ]
-crbug.com/1456360 [ chromeos chromeos-board-amd64-generic ] Pixel_WebGLSadCanvas [ Skip ]
-
-
-
 crbug.com/1474611 [ mac nvidia angle-opengl graphite-disabled ] Pixel_WebGLGreenTriangle_NonChromiumImage_NoAA_NoAlpha [ Failure ]
 crbug.com/1477318 [ mac ] Pixel_WebGLPreservedAfterTabSwitch [ RetryOnFailure ]
 
-
 crbug.com/354748635 [ mac amd-0x7340 angle-opengl ] Pixel_OffscreenCanvasAccelerated2D [ RetryOnFailure ]
 crbug.com/354748635 [ mac amd-0x7340 angle-opengl ] Pixel_OffscreenCanvasAccelerated2DWorker [ Failure ]
 
@@ -438,7 +431,6 @@
 crbug.com/1372149 [ android android-sm-a235m no-passthrough renderer-skia-vulkan ] Pixel_WebGLFloat [ Failure ]
 crbug.com/40241618 [ android android-sm-s911u1 no-passthrough renderer-skia-vulkan ] Pixel_WebGLFloat [ Failure ]
 
-
 # Flaky failures vulkan queue submission, causing dchecks on CQ
 # finder:disable-narrowing
 crbug.com/40937765 [ android android-pixel-6 renderer-skia-vulkan ] Pixel_Video_Context_Loss_VP9 [ RetryOnFailure ]
@@ -447,12 +439,9 @@
 crbug.com/40937765 [ android android-pixel-6 renderer-skia-vulkan ] Pixel_Video_MP4_Rounded_Corner [ RetryOnFailure ]
 # finder:enable-narrowing
 
-
-
 # ChromeOS failures
 crbug.com/345146895 [ chromeos cros-chrome chromeos-board-amd64-generic renderer-skia-gl ] Pixel_RenderPasses [ Failure ]
 
-
 # Failures on fuchsia
 crbug.com/40935289 [ fuchsia fuchsia-board-qemu-x64 ] Pixel_WebGLFloat [ Failure ]
 crbug.com/40935289 [ fuchsia no-asan web-engine-shell ] Pixel_Video_AV1 [ Failure ]
@@ -477,6 +466,8 @@
 # Flaky hang on Mac AMD GL ASAN
 crbug.com/40277254 [ mac amd angle-opengl asan ] Pixel_WebGPUDisplayP3 [ Failure ]
 
+# Flaky timeout on Mac Retina Debug (AMD) pixel_skia_gold_metal_passthrough_graphite_test
+crbug.com/389654097 [ mac amd debug angle-metal graphite-enabled ] Pixel_CSSFilterEffects [ Failure ]
 
 #######################################################################
 # Automated Entries After This Point - Do Not Manually Add Below Here #
diff --git a/extensions/browser/api/app_window/app_window_api.cc b/extensions/browser/api/app_window/app_window_api.cc
index b3d9f2b..cbc89be 100644
--- a/extensions/browser/api/app_window/app_window_api.cc
+++ b/extensions/browser/api/app_window/app_window_api.cc
@@ -29,7 +29,6 @@
 #include "extensions/browser/extensions_browser_client.h"
 #include "extensions/common/api/app_runtime.h"
 #include "extensions/common/api/app_window.h"
-#include "extensions/common/features/simple_feature.h"
 #include "extensions/common/image_util.h"
 #include "extensions/common/manifest.h"
 #include "extensions/common/mojom/context_type.mojom.h"
@@ -268,7 +267,7 @@
     }
 
     if (options->alpha_enabled) {
-      const char* const kAllowlist[] = {
+      static constexpr const char* kAllowlist[] = {
 #if BUILDFLAG(IS_CHROMEOS)
           "B58B99751225318C7EB8CF4688B5434661083E07",  // http://crbug.com/410550
           "06BE211D5F014BAB34BC22D9DDA09C63A81D828E",  // http://crbug.com/425539
@@ -285,8 +284,7 @@
           "F16F23C83C5F6DAD9B65A120448B34056DD80691",
           "0F585FB1D0FDFBEBCE1FEB5E9DFFB6DA476B8C9B"};
       if (AppWindowClient::Get()->IsCurrentChannelOlderThanDev() &&
-          !SimpleFeature::IsIdInArray(extension_id(), kAllowlist,
-                                      std::size(kAllowlist))) {
+          !base::Contains(kAllowlist, extension()->hashed_id().value())) {
         return RespondNow(
             Error(app_window_constants::kAlphaEnabledWrongChannel));
       }
diff --git a/extensions/browser/api/system_cpu/cpu_info_provider.cc b/extensions/browser/api/system_cpu/cpu_info_provider.cc
index 5b000be..51234b34 100644
--- a/extensions/browser/api/system_cpu/cpu_info_provider.cc
+++ b/extensions/browser/api/system_cpu/cpu_info_provider.cc
@@ -46,7 +46,6 @@
   }
 
 #if BUILDFLAG(IS_CHROMEOS)
-  // TODO(crbug.com/381925994): Port temperature reading to Android.
   using CPUTemperatureInfo =
       chromeos::system::CPUTemperatureReader::CPUTemperatureInfo;
   std::vector<CPUTemperatureInfo> cpu_temp_info =
diff --git a/extensions/browser/content_verifier/content_verify_job.cc b/extensions/browser/content_verifier/content_verify_job.cc
index f30b458..b6b5b5e 100644
--- a/extensions/browser/content_verifier/content_verify_job.cc
+++ b/extensions/browser/content_verifier/content_verify_job.cc
@@ -2,15 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "extensions/browser/content_verifier/content_verify_job.h"
 
 #include <algorithm>
 
+#include "base/containers/span.h"
 #include "base/functional/bind.h"
 #include "base/lazy_instance.h"
 #include "base/memory/raw_ptr.h"
@@ -149,7 +145,7 @@
     DCHECK_EQ(read_error_, MOJO_RESULT_OK);
     std::string tmp;
     queue_.swap(tmp);
-    BytesReadImpl(std::data(tmp), tmp.size(), MOJO_RESULT_OK);
+    BytesReadImpl(tmp, MOJO_RESULT_OK);
     if (failed_) {
       return;
     }
@@ -160,12 +156,11 @@
   }
 }
 
-void ContentVerifyJob::BytesRead(const char* data,
-                                 int count,
+void ContentVerifyJob::BytesRead(base::span<const char> data,
                                  MojoResult read_result) {
   base::AutoLock auto_lock(lock_);
   DCHECK(!done_reading_);
-  BytesReadImpl(data, count, read_result);
+  BytesReadImpl(data, read_result);
 }
 
 void ContentVerifyJob::DoneReading() {
@@ -231,8 +226,7 @@
   }
 }
 
-void ContentVerifyJob::BytesReadImpl(const char* data,
-                                     int count,
+void ContentVerifyJob::BytesReadImpl(base::span<const char> data,
                                      MojoResult read_result) {
   ScopedElapsedTimer timer(&time_spent_);
   if (failed_)
@@ -250,13 +244,13 @@
   }
 
   if (!hashes_ready_) {
-    queue_.append(data, count);
+    queue_.append(data.begin(), data.end());
     return;
   }
   if (hash_reader_->status() != ContentHashReader::InitStatus::SUCCESS) {
     return;
   }
-  DCHECK_GE(count, 0);
+  const int count = data.size();
   int bytes_added = 0;
 
   while (bytes_added < count) {
@@ -272,7 +266,7 @@
         std::min(hash_reader_->block_size() - current_hash_byte_count_,
                  count - bytes_added);
     DCHECK_GT(bytes_to_hash, 0);
-    current_hash_->Update(data + bytes_added, bytes_to_hash);
+    current_hash_->Update(&data[bytes_added], bytes_to_hash);
     bytes_added += bytes_to_hash;
     current_hash_byte_count_ += bytes_to_hash;
     total_bytes_read_ += bytes_to_hash;
diff --git a/extensions/browser/content_verifier/content_verify_job.h b/extensions/browser/content_verifier/content_verify_job.h
index af1397c..9f6eb814 100644
--- a/extensions/browser/content_verifier/content_verify_job.h
+++ b/extensions/browser/content_verifier/content_verify_job.h
@@ -10,6 +10,7 @@
 #include <memory>
 #include <string>
 
+#include "base/containers/span.h"
 #include "base/files/file_path.h"
 #include "base/functional/callback.h"
 #include "base/memory/ref_counted.h"
@@ -85,7 +86,7 @@
   // Make sure to call DoneReading so that any final bytes that were read that
   // didn't align exactly on a block size boundary get their hash checked as
   // well.
-  void BytesRead(const char* data, int count, MojoResult read_result);
+  void BytesRead(base::span<const char> data, MojoResult read_result);
 
   // Call once when finished adding bytes via OnDone.
   void DoneReading();
@@ -134,7 +135,7 @@
   void OnHashMismatch();
 
   // Same as BytesRead, but is run without acquiring lock.
-  void BytesReadImpl(const char* data, int count, MojoResult read_result);
+  void BytesReadImpl(base::span<const char> data, MojoResult read_result);
 
   // Called each time we're done adding bytes for the current block, and are
   // ready to finish the hash operation for those bytes and make sure it
diff --git a/extensions/browser/content_verifier/content_verify_job_unittest.cc b/extensions/browser/content_verifier/content_verify_job_unittest.cc
index 00be89f..5f97b03 100644
--- a/extensions/browser/content_verifier/content_verify_job_unittest.cc
+++ b/extensions/browser/content_verifier/content_verify_job_unittest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/containers/span.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
@@ -135,8 +136,7 @@
           auto read_data = resource_contents.value_or(std::string_view{});
           MojoResult read_result =
               resource_contents ? MOJO_RESULT_OK : MOJO_RESULT_NOT_FOUND;
-          verify_job->BytesRead(read_data.data(), read_data.size(),
-                                read_result);
+          verify_job->BytesRead(read_data, read_result);
           verify_job->DoneReading();
         },
         std::move(resource_contents));
@@ -186,7 +186,7 @@
              base::DoNothing());
 
     for (const MojoResult read_error : read_errors) {
-      verify_job->BytesRead(nullptr, 0, read_error);
+      verify_job->BytesRead({}, read_error);
     }
     verify_job->DoneReading();
 
@@ -852,7 +852,7 @@
             DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
             job->Start(content_verifier.get(), extension->version(),
                        extension->manifest_version(), base::DoNothing());
-            job->BytesRead(nullptr, 0u, read_result);
+            job->BytesRead({}, read_result);
             job->DoneReading();
             std::move(done_callback).Run();
           };
diff --git a/extensions/browser/extension_protocols.cc b/extensions/browser/extension_protocols.cc
index e69020ed..1f0fbbae 100644
--- a/extensions/browser/extension_protocols.cc
+++ b/extensions/browser/extension_protocols.cc
@@ -16,6 +16,7 @@
 
 #include "base/base64.h"
 #include "base/compiler_specific.h"
+#include "base/containers/span.h"
 #include "base/feature_list.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
@@ -485,8 +486,9 @@
         // Note: We still pass the data to |verify_job_|, even if there was a
         // read error, because some errors are ignorable. See
         // ContentVerifyJob::BytesRead() for more details.
-        verify_job_->BytesRead(static_cast<const char*>(buffer.data()),
-                               result->bytes_read, result->result);
+        verify_job_->BytesRead(
+            buffer.subspan(0u, static_cast<size_t>(result->bytes_read)),
+            result->result);
       }
     }
   }
diff --git a/extensions/browser/extension_registrar.cc b/extensions/browser/extension_registrar.cc
index e313349..db1b24e 100644
--- a/extensions/browser/extension_registrar.cc
+++ b/extensions/browser/extension_registrar.cc
@@ -78,6 +78,25 @@
     scoped_refptr<const Extension> extension) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
+  if (!Manifest::IsValidLocation(extension->location())) {
+    // TODO(devlin): We should *never* add an extension with an invalid
+    // location, but some bugs (e.g. crbug.com/692069) seem to indicate we do.
+    // Track down the cases when this can happen, and remove this
+    // DumpWithoutCrashing() (possibly replacing it with a CHECK).
+    DEBUG_ALIAS_FOR_CSTR(extension_id_copy, extension->id().c_str(), 33);
+    ManifestLocation location = extension->location();
+    int creation_flags = extension->creation_flags();
+    Manifest::Type type = extension->manifest()->type();
+    base::debug::Alias(&location);
+    base::debug::Alias(&creation_flags);
+    base::debug::Alias(&type);
+    NOTREACHED();
+  }
+
+  if (!delegate_->CanAddExtension(extension.get())) {
+    return;
+  }
+
   bool is_extension_loaded = false;
   const Extension* old = registry_->GetInstalledExtension(extension->id());
   if (old) {
@@ -124,6 +143,20 @@
     }
     AddNewExtension(extension);
   }
+
+  if (registry_->disabled_extensions().Contains(extension->id())) {
+    // Show the extension disabled error if a permissions increase or a remote
+    // installation is the reason it was disabled, and no other reasons exist.
+    int reasons = extension_prefs_->GetDisableReasons(extension->id());
+    const int kReasonMask = disable_reason::DISABLE_PERMISSIONS_INCREASE |
+                            disable_reason::DISABLE_REMOTE_INSTALL;
+    if (reasons & kReasonMask && !(reasons & ~kReasonMask)) {
+      delegate_->ShowExtensionDisabledError(
+          extension.get(),
+          extension_prefs_->HasDisableReason(
+              extension->id(), disable_reason::DISABLE_REMOTE_INSTALL));
+    }
+  }
 }
 
 void ExtensionRegistrar::AddNewExtension(
@@ -462,6 +495,16 @@
   }
 }
 
+void ExtensionRegistrar::UnblockAllExtensions() {
+  const ExtensionSet to_unblock =
+      registry_->GenerateInstalledExtensionsSet(ExtensionRegistry::BLOCKED);
+
+  for (const auto& extension : to_unblock) {
+    registry_->RemoveBlocked(extension->id());
+    AddExtension(extension.get());
+  }
+}
+
 void ExtensionRegistrar::OnUnpackedExtensionReloadFailed(
     const base::FilePath& path) {
   failed_to_reload_unpacked_extensions_.insert(path);
diff --git a/extensions/browser/extension_registrar.h b/extensions/browser/extension_registrar.h
index 9d934c46..151665e7 100644
--- a/extensions/browser/extension_registrar.h
+++ b/extensions/browser/extension_registrar.h
@@ -81,6 +81,15 @@
         const base::FilePath& path,
         LoadErrorBehavior load_error_behavior) = 0;
 
+    // Informs the user that an extension was disabled after upgrading to higher
+    // permissions. If |is_remote_install| is true, the extension was disabled
+    // because it was installed remotely.
+    virtual void ShowExtensionDisabledError(const Extension* extension,
+                                            bool is_remote_install) = 0;
+
+    // Returns true if |extension| can be added.
+    virtual bool CanAddExtension(const Extension* extension) = 0;
+
     // Returns true if the extension is allowed to be enabled or disabled,
     // respectively.
     virtual bool CanEnableExtension(const Extension* extension) = 0;
@@ -161,6 +170,10 @@
   // are exempt from being Blocked (see CanBlockExtension in .cc file).
   void BlockAllExtensions();
 
+  // All blocked extensions are reverted to their previous state, and are
+  // reloaded. Newly added extensions are no longer automatically blocked.
+  void UnblockAllExtensions();
+
   // Deactivates the extension, adding its id to the list of terminated
   // extensions.
   void TerminateExtension(const ExtensionId& extension_id);
diff --git a/extensions/browser/extension_registrar_unittest.cc b/extensions/browser/extension_registrar_unittest.cc
index d3ffec3c..b616f5e 100644
--- a/extensions/browser/extension_registrar_unittest.cc
+++ b/extensions/browser/extension_registrar_unittest.cc
@@ -65,6 +65,7 @@
   ~TestExtensionRegistrarDelegate() override = default;
 
   // ExtensionRegistrar::Delegate:
+  MOCK_METHOD1(CanAddExtension, bool(const Extension*));
   MOCK_METHOD2(PreAddExtension,
                void(const Extension* extension,
                     const Extension* old_extension));
@@ -76,6 +77,7 @@
                void(const ExtensionId& extension_id,
                     const base::FilePath& path,
                     LoadErrorBehavior load_error_behavior));
+  MOCK_METHOD2(ShowExtensionDisabledError, void(const Extension*, bool));
   MOCK_METHOD1(CanEnableExtension, bool(const Extension* extension));
   MOCK_METHOD1(CanDisableExtension, bool(const Extension* extension));
   MOCK_METHOD1(ShouldBlockExtension, bool(const Extension* extension));
@@ -99,6 +101,8 @@
     registrar_.emplace(browser_context(), delegate());
 
     // Mock defaults.
+    ON_CALL(delegate_, CanAddExtension(extension_.get()))
+        .WillByDefault(Return(true));
     ON_CALL(delegate_, CanEnableExtension(extension_.get()))
         .WillByDefault(Return(true));
     ON_CALL(delegate_, CanDisableExtension(extension_.get()))
diff --git a/extensions/browser/extension_user_script_loader.cc b/extensions/browser/extension_user_script_loader.cc
index 1963932a..74ad4ae04 100644
--- a/extensions/browser/extension_user_script_loader.cc
+++ b/extensions/browser/extension_user_script_loader.cc
@@ -16,6 +16,7 @@
 #include <utility>
 
 #include "base/containers/contains.h"
+#include "base/containers/span.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
@@ -128,9 +129,9 @@
       extension_id, extension_root, relative_path, verifier));
   CHECK(job);
   if (content) {
-    job->BytesRead(content->data(), content->size(), MOJO_RESULT_OK);
+    job->BytesRead(*content, MOJO_RESULT_OK);
   } else {
-    job->BytesRead("", 0u, MOJO_RESULT_NOT_FOUND);
+    job->BytesRead({}, MOJO_RESULT_NOT_FOUND);
   }
   job->DoneReading();
 }
diff --git a/extensions/browser/guest_view/web_view/web_view_guest.cc b/extensions/browser/guest_view/web_view/web_view_guest.cc
index d8e69df..8a85c535 100644
--- a/extensions/browser/guest_view/web_view/web_view_guest.cc
+++ b/extensions/browser/guest_view/web_view/web_view_guest.cc
@@ -878,6 +878,31 @@
       std::make_unique<GuestViewEvent>(webview::kEventClose, std::move(args)));
 }
 
+void WebViewGuest::GuestRequestMediaAccessPermission(
+    const content::MediaStreamRequest& request,
+    content::MediaResponseCallback callback) {
+  if (IsOwnedByControlledFrameEmbedder()) {
+    web_view_permission_helper_->RequestMediaAccessPermissionForControlledFrame(
+        web_contents(), request, std::move(callback));
+    return;
+  }
+  web_view_permission_helper_->RequestMediaAccessPermission(
+      request, std::move(callback));
+}
+
+bool WebViewGuest::GuestCheckMediaAccessPermission(
+    content::RenderFrameHost* render_frame_host,
+    const url::Origin& security_origin,
+    blink::mojom::MediaStreamType type) {
+  if (IsOwnedByControlledFrameEmbedder()) {
+    return web_view_permission_helper_
+        ->CheckMediaAccessPermissionForControlledFrame(render_frame_host,
+                                                       security_origin, type);
+  }
+  return web_view_permission_helper_->CheckMediaAccessPermission(
+      render_frame_host, security_origin, type);
+}
+
 void WebViewGuest::CreateNewGuestWebViewWindow(
     const content::OpenURLParams& params) {
   GuestViewManager* guest_manager =
@@ -1493,13 +1518,7 @@
     content::MediaResponseCallback callback) {
   CHECK(!base::FeatureList::IsEnabled(features::kGuestViewMPArch));
 
-  if (IsOwnedByControlledFrameEmbedder()) {
-    web_view_permission_helper_->RequestMediaAccessPermissionForControlledFrame(
-        source, request, std::move(callback));
-    return;
-  }
-  web_view_permission_helper_->RequestMediaAccessPermission(
-      source, request, std::move(callback));
+  GuestRequestMediaAccessPermission(request, std::move(callback));
 }
 
 bool WebViewGuest::CheckMediaAccessPermission(
@@ -1508,13 +1527,8 @@
     blink::mojom::MediaStreamType type) {
   CHECK(!base::FeatureList::IsEnabled(features::kGuestViewMPArch));
 
-  if (IsOwnedByControlledFrameEmbedder()) {
-    return web_view_permission_helper_
-        ->CheckMediaAccessPermissionForControlledFrame(render_frame_host,
-                                                       security_origin, type);
-  }
-  return web_view_permission_helper_->CheckMediaAccessPermission(
-      render_frame_host, security_origin, type);
+  return GuestCheckMediaAccessPermission(render_frame_host, security_origin,
+                                         type);
 }
 
 void WebViewGuest::CanDownload(const GURL& url,
diff --git a/extensions/browser/guest_view/web_view/web_view_guest.h b/extensions/browser/guest_view/web_view/web_view_guest.h
index 9cd35aa..5ec3d53d 100644
--- a/extensions/browser/guest_view/web_view/web_view_guest.h
+++ b/extensions/browser/guest_view/web_view/web_view_guest.h
@@ -236,6 +236,13 @@
                     base::OnceCallback<void(content::NavigationHandle&)>
                         navigation_handle_callback) final;
   void GuestClose() final;
+  void GuestRequestMediaAccessPermission(
+      const content::MediaStreamRequest& request,
+      content::MediaResponseCallback callback) final;
+  bool GuestCheckMediaAccessPermission(
+      content::RenderFrameHost* render_frame_host,
+      const url::Origin& security_origin,
+      blink::mojom::MediaStreamType type) final;
 
   // GuestpageHolder::Delegate implementation.
   bool GuestHandleContextMenu(content::RenderFrameHost& render_frame_host,
diff --git a/extensions/browser/guest_view/web_view/web_view_permission_helper.cc b/extensions/browser/guest_view/web_view/web_view_permission_helper.cc
index 3c59a1e..d7bfdc5 100644
--- a/extensions/browser/guest_view/web_view/web_view_permission_helper.cc
+++ b/extensions/browser/guest_view/web_view/web_view_permission_helper.cc
@@ -189,7 +189,6 @@
 }
 
 void WebViewPermissionHelper::RequestMediaAccessPermission(
-    content::WebContents* source,
     const content::MediaStreamRequest& request,
     content::MediaResponseCallback callback) {
   base::Value::Dict request_info;
diff --git a/extensions/browser/guest_view/web_view/web_view_permission_helper.h b/extensions/browser/guest_view/web_view/web_view_permission_helper.h
index 37e43ef..da39733 100644
--- a/extensions/browser/guest_view/web_view/web_view_permission_helper.h
+++ b/extensions/browser/guest_view/web_view/web_view_permission_helper.h
@@ -68,8 +68,7 @@
   static WebViewPermissionHelper* FromRenderFrameHostId(
       const content::GlobalRenderFrameHostId& render_frame_host_id);
 
-  void RequestMediaAccessPermission(content::WebContents* source,
-                                    const content::MediaStreamRequest& request,
+  void RequestMediaAccessPermission(const content::MediaStreamRequest& request,
                                     content::MediaResponseCallback callback);
 
   void RequestMediaAccessPermissionForControlledFrame(
diff --git a/extensions/common/features/simple_feature.cc b/extensions/common/features/simple_feature.cc
index 7512f9d4c..0fc07f1 100644
--- a/extensions/common/features/simple_feature.cc
+++ b/extensions/common/features/simple_feature.cc
@@ -474,20 +474,6 @@
 }
 
 // static
-bool SimpleFeature::IsIdInArray(const ExtensionId& extension_id,
-                                const char* const array[],
-                                size_t array_length) {
-  if (!IsValidExtensionId(extension_id))
-    return false;
-
-  const char* const* start = array;
-  const char* const* end = array + array_length;
-
-  return ((std::find(start, end, extension_id) != end) ||
-          (std::find(start, end, HashedIdInHex(extension_id)) != end));
-}
-
-// static
 bool SimpleFeature::IsIdInList(const HashedExtensionId& hashed_id,
                                const std::vector<std::string>& list) {
   if (!IsValidHashedExtensionId(hashed_id))
diff --git a/extensions/common/features/simple_feature.h b/extensions/common/features/simple_feature.h
index d3f1557..0a3419f 100644
--- a/extensions/common/features/simple_feature.h
+++ b/extensions/common/features/simple_feature.h
@@ -110,10 +110,6 @@
       DelegatedAvailabilityCheckHandler handler) override;
   bool HasDelegatedAvailabilityCheckHandler() const override;
 
-  static bool IsIdInArray(const ExtensionId& extension_id,
-                          const char* const array[],
-                          size_t array_length);
-
   // Similar to mojom::ManifestLocation, these are the classes of locations
   // supported in feature files. These should only be used in this class and in
   // generated files.
diff --git a/extensions/common/features/simple_feature_unittest.cc b/extensions/common/features/simple_feature_unittest.cc
index 5a0738c..110731b 100644
--- a/extensions/common/features/simple_feature_unittest.cc
+++ b/extensions/common/features/simple_feature_unittest.cc
@@ -772,25 +772,6 @@
                 .result());
 }
 
-TEST_F(SimpleFeatureTest, IsIdInArray) {
-  EXPECT_FALSE(SimpleFeature::IsIdInArray("", {}, 0));
-  EXPECT_FALSE(SimpleFeature::IsIdInArray(
-      "bbbbccccdddddddddeeeeeeffffgghhh", {}, 0));
-
-  const char* const kIdArray[] = {
-    "bbbbccccdddddddddeeeeeeffffgghhh",
-    // aaaabbbbccccddddeeeeffffgggghhhh
-    "9A0417016F345C934A1A88F55CA17C05014EEEBA"
-  };
-  EXPECT_FALSE(SimpleFeature::IsIdInArray("", kIdArray, std::size(kIdArray)));
-  EXPECT_FALSE(SimpleFeature::IsIdInArray("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
-                                          kIdArray, std::size(kIdArray)));
-  EXPECT_TRUE(SimpleFeature::IsIdInArray("bbbbccccdddddddddeeeeeeffffgghhh",
-                                         kIdArray, std::size(kIdArray)));
-  EXPECT_TRUE(SimpleFeature::IsIdInArray("aaaabbbbccccddddeeeeffffgggghhhh",
-                                         kIdArray, std::size(kIdArray)));
-}
-
 // Tests that all combinations of feature channel and Chrome channel correctly
 // compute feature availability.
 TEST_F(SimpleFeatureTest, SupportedChannel) {
diff --git a/extensions/renderer/resource_bundle_source_map.cc b/extensions/renderer/resource_bundle_source_map.cc
index 3dfef6c..3509d86b 100644
--- a/extensions/renderer/resource_bundle_source_map.cc
+++ b/extensions/renderer/resource_bundle_source_map.cc
@@ -76,17 +76,14 @@
   bool is_gzipped = resource_bundle_->IsGzipped(info.id);
   if (is_gzipped) {
     info.cached = std::make_unique<std::string>();
-    uint32_t size = compression::GetUncompressedSize(resource);
-    info.cached->resize(size);
-    std::string_view uncompressed(*info.cached);
-    if (!compression::GzipUncompress(resource, uncompressed)) {
+    if (!compression::GzipUncompress(resource, info.cached.get())) {
       // Let |info.cached| point to an empty string, so that the next time when
       // the resource is requested, the method returns an empty string directly,
       // instead of trying to uncompress again.
       info.cached->clear();
       return v8::Local<v8::String>();
     }
-    resource = uncompressed;
+    resource = *info.cached;
   }
 
   return ConvertString(isolate, resource);
diff --git a/extensions/shell/browser/shell_extension_loader.cc b/extensions/shell/browser/shell_extension_loader.cc
index ce42667..853beb01 100644
--- a/extensions/shell/browser/shell_extension_loader.cc
+++ b/extensions/shell/browser/shell_extension_loader.cc
@@ -14,6 +14,7 @@
 #include "extensions/browser/extension_file_task_runner.h"
 #include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registry.h"
+#include "extensions/common/extension.h"
 #include "extensions/common/file_util.h"
 
 namespace extensions {
@@ -116,6 +117,10 @@
   keep_alive_requester_.StopTrackingReload(old_extension_id);
 }
 
+bool ShellExtensionLoader::CanAddExtension(const Extension* extension) {
+  return true;
+}
+
 void ShellExtensionLoader::PreAddExtension(const Extension* extension,
                                            const Extension* old_extension) {
   if (old_extension)
@@ -156,6 +161,10 @@
   did_schedule_reload_ = true;
 }
 
+void ShellExtensionLoader::ShowExtensionDisabledError(
+    const Extension* extension,
+    bool is_remote_install) {}
+
 bool ShellExtensionLoader::CanEnableExtension(const Extension* extension) {
   return true;
 }
diff --git a/extensions/shell/browser/shell_extension_loader.h b/extensions/shell/browser/shell_extension_loader.h
index ee6b82b..bc59ef60 100644
--- a/extensions/shell/browser/shell_extension_loader.h
+++ b/extensions/shell/browser/shell_extension_loader.h
@@ -52,6 +52,7 @@
                              scoped_refptr<const Extension> extension);
 
   // ExtensionRegistrar::Delegate:
+  bool CanAddExtension(const Extension* extension) override;
   void PreAddExtension(const Extension* extension,
                        const Extension* old_extension) override;
   void PostActivateExtension(scoped_refptr<const Extension> extension) override;
@@ -61,6 +62,8 @@
       const ExtensionId& extension_id,
       const base::FilePath& path,
       ExtensionRegistrar::LoadErrorBehavior load_error_behavior) override;
+  void ShowExtensionDisabledError(const Extension* extension,
+                                  bool is_remote_install) override;
   bool CanEnableExtension(const Extension* extension) override;
   bool CanDisableExtension(const Extension* extension) override;
   bool ShouldBlockExtension(const Extension* extension) override;
diff --git a/headless/lib/browser/protocol/target_handler.cc b/headless/lib/browser/protocol/target_handler.cc
index a5f67a8..b19f063 100644
--- a/headless/lib/browser/protocol/target_handler.cc
+++ b/headless/lib/browser/protocol/target_handler.cc
@@ -32,6 +32,7 @@
     std::optional<int> top,
     std::optional<int> width,
     std::optional<int> height,
+    std::optional<std::string> window_state,
     std::optional<std::string> context_id,
     std::optional<bool> enable_begin_frame_control,
     std::optional<bool> new_window,
diff --git a/headless/lib/browser/protocol/target_handler.h b/headless/lib/browser/protocol/target_handler.h
index 33204fb..e162d88 100644
--- a/headless/lib/browser/protocol/target_handler.h
+++ b/headless/lib/browser/protocol/target_handler.h
@@ -33,6 +33,7 @@
                         std::optional<int> top,
                         std::optional<int> width,
                         std::optional<int> height,
+                        std::optional<std::string> window_state,
                         std::optional<std::string> context_id,
                         std::optional<bool> enable_begin_frame_control,
                         std::optional<bool> new_window,
diff --git a/headless/test/headless_web_contents_browsertest.cc b/headless/test/headless_web_contents_browsertest.cc
index c0c7375..1d9445b 100644
--- a/headless/test/headless_web_contents_browsertest.cc
+++ b/headless/test/headless_web_contents_browsertest.cc
@@ -390,9 +390,6 @@
         content::DevToolsAgentHost::GetForId(targetId)->GetWebContents());
 
     devtools_client_.AttachToWebContents(web_contents_->web_contents());
-    devtools_client_.AddEventHandler("Page.loadEventFired",
-                                     on_load_event_fired_handler_);
-
     devtools_client_.SendCommand(
         "Page.enable",
         base::BindOnce(
@@ -401,6 +398,8 @@
   }
 
   void OnPageDomainEnabled(base::Value::Dict) {
+    devtools_client_.AddEventHandler("Page.loadEventFired",
+                                     on_load_event_fired_handler_);
     devtools_client_.SendCommand(
         "Page.navigate",
         Param("url", embedded_test_server()->GetURL(GetTestHtmlFile()).spec()));
@@ -474,7 +473,20 @@
   void StartFrames() override { BeginFrame(true); }
 
   void OnFrameFinished(base::Value::Dict result) override {
+    // TODO(crbug.com/385523803): The screenshot capturing logic is currently
+    // flaky for a first screenshot after a navigation, see bug for detaiils.
+    // This works around the flake by retrying the command in case the first
+    // one returns without screenshot.
     if (num_begin_frames_ == 1) {
+      CHECK_EQ(num_retries_, 0);
+      if (!result.FindStringByDottedPath("result.screenshotData")) {
+        num_retries_ += 1;
+        BeginFrame(true);
+        return;
+      }
+    }
+    int frame_number = num_begin_frames_ - num_retries_;
+    if (frame_number == 1) {
       // First BeginFrame should have caused damage and have a screenshot.
       EXPECT_TRUE(DictBool(result, "result.hasDamage"));
 
@@ -493,13 +505,13 @@
       SkColor actual_color = result_bitmap.getColor(100, 100);
       EXPECT_EQ(expected_color, actual_color);
     } else {
-      DCHECK_EQ(2, num_begin_frames_);
+      DCHECK_EQ(2, frame_number);
       // Can't guarantee that the second BeginFrame didn't have damage, but it
       // should not have a screenshot.
       EXPECT_FALSE(result.FindStringByDottedPath("result.screenshotData"));
     }
 
-    if (num_begin_frames_ < 2) {
+    if (frame_number < 2) {
       // Don't capture a screenshot in the second BeginFrame.
       BeginFrame(false);
     } else {
@@ -508,6 +520,8 @@
       PostFinishAsynchronousTest();
     }
   }
+
+  int num_retries_ = 0;
 };
 
 HEADLESS_DEVTOOLED_TEST_F(HeadlessWebContentsBeginFrameControlBasicTest);
diff --git a/infra/config/generated/builder-owners/clank-engprod@google.com.txt b/infra/config/generated/builder-owners/clank-engprod@google.com.txt
index 4312df81..a801c73 100644
--- a/infra/config/generated/builder-owners/clank-engprod@google.com.txt
+++ b/infra/config/generated/builder-owners/clank-engprod@google.com.txt
@@ -7,6 +7,7 @@
 ci/Deterministic Android
 ci/Deterministic Android (dbg)
 ci/android-10-arm64-rel
+ci/android-10-x86-fyi-rel
 ci/android-12-x64-rel
 ci/android-12l-landscape-x64-dbg-tests
 ci/android-12l-x64-dbg-tests
@@ -39,6 +40,7 @@
 ci/android-pie-arm64-dbg
 ci/android-pie-arm64-rel
 ci/android-pie-x86-rel
+try/android-10-x86-fyi-rel
 try/android-12-x64-rel
 try/android-12l-landscape-x64-dbg
 try/android-12l-x64-rel-cq
diff --git a/infra/config/generated/builders/ci/android-10-x86-fyi-rel/gn-args.json b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/gn-args.json
new file mode 100644
index 0000000..e421386a
--- /dev/null
+++ b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/gn-args.json
@@ -0,0 +1,19 @@
+{
+  "gn_args": {
+    "android_static_analysis": "off",
+    "dcheck_always_on": false,
+    "debuggable_apks": false,
+    "ffmpeg_branding": "Chrome",
+    "is_component_build": false,
+    "is_debug": false,
+    "proprietary_codecs": true,
+    "strip_debug_info": true,
+    "symbol_level": 1,
+    "system_webview_package_name": "com.google.android.apps.chrome",
+    "system_webview_shell_package_name": "org.chromium.my_webview_shell",
+    "target_cpu": "x86",
+    "target_os": "android",
+    "use_remoteexec": true,
+    "use_siso": true
+  }
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/ci/android-10-x86-fyi-rel/properties.json b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/properties.json
new file mode 100644
index 0000000..13b97a3
--- /dev/null
+++ b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/properties.json
@@ -0,0 +1,78 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "additional_exclusions": [
+        "infra/config/generated/builders/ci/android-10-x86-fyi-rel/gn-args.json"
+      ],
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "ci",
+              "builder": "android-10-x86-fyi-rel",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-android-archive",
+              "builder_group": "chromium.android.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_android_config": {
+                "config": "x86_builder_mb"
+              },
+              "legacy_chromium_config": {
+                "build_config": "Release",
+                "config": "android",
+                "target_bits": 32,
+                "target_platform": "android"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "android"
+                ],
+                "config": "chromium"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "ci",
+          "builder": "android-10-x86-fyi-rel",
+          "project": "chromium"
+        }
+      ],
+      "mirroring_builder_group_and_names": [
+        {
+          "builder": "android-10-x86-fyi-rel",
+          "group": "tryserver.chromium.android"
+        }
+      ],
+      "targets_spec_directory": "src/infra/config/generated/builders/ci/android-10-x86-fyi-rel/targets"
+    }
+  },
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "metrics_project": "chromium-reclient-metrics",
+    "scandeps_server": true
+  },
+  "$build/siso": {
+    "configs": [
+      "builder"
+    ],
+    "enable_cloud_profiler": true,
+    "enable_cloud_trace": true,
+    "experiments": [],
+    "project": "rbe-chromium-trusted",
+    "remote_jobs": 250
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.android.fyi",
+  "recipe": "chromium"
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/ci/android-10-x86-fyi-rel/shadow-properties.json b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/shadow-properties.json
new file mode 100644
index 0000000..673c7c0
--- /dev/null
+++ b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/shadow-properties.json
@@ -0,0 +1,17 @@
+{
+  "$build/reclient": {
+    "instance": "rbe-chromium-untrusted",
+    "metrics_project": "chromium-reclient-metrics",
+    "scandeps_server": true
+  },
+  "$build/siso": {
+    "configs": [
+      "builder"
+    ],
+    "enable_cloud_profiler": true,
+    "enable_cloud_trace": true,
+    "experiments": [],
+    "project": "rbe-chromium-untrusted",
+    "remote_jobs": 250
+  }
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/ci/android-10-x86-fyi-rel/targets/chromium.android.fyi.json b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/targets/chromium.android.fyi.json
new file mode 100644
index 0000000..2ba0a2e
--- /dev/null
+++ b/infra/config/generated/builders/ci/android-10-x86-fyi-rel/targets/chromium.android.fyi.json
@@ -0,0 +1,4386 @@
+{
+  "android-10-x86-fyi-rel": {
+    "additional_compile_targets": [
+      "chrome_nocompile_tests"
+    ],
+    "gtest_tests": [
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "absl_hardening_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "absl_hardening_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "absl_hardening_tests",
+        "test_id_prefix": "ninja://third_party/abseil-cpp:absl_hardening_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_browsertests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 9
+        },
+        "test": "android_browsertests",
+        "test_id_prefix": "ninja://chrome/test:android_browsertests/"
+      },
+      {
+        "args": [
+          "--test-launcher-batch-limit=1",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_sync_integration_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_sync_integration_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "android_sync_integration_tests",
+        "test_id_prefix": "ninja://chrome/test:android_sync_integration_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_webview_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_webview_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "android_webview_unittests",
+        "test_id_prefix": "ninja://android_webview/test:android_webview_unittests/"
+      },
+      {
+        "args": [
+          "-v",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "angle_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "angle_unittests",
+        "test_id_prefix": "ninja://third_party/angle/src/tests:angle_unittests/",
+        "use_isolated_scripts_api": true
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "base_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "base_unittests",
+        "test_id_prefix": "ninja://base:base_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_common_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "blink_common_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_common_unittests",
+        "test_id_prefix": "ninja://third_party/blink/common:blink_common_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_heap_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "blink_heap_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_heap_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/platform/heap:blink_heap_unittests/"
+      },
+      {
+        "args": [
+          "--git-revision=${got_revision}",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_platform_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "blink_platform_unittests",
+        "precommit_args": [
+          "--gerrit-issue=${patch_issue}",
+          "--gerrit-patchset=${patch_set}",
+          "--buildbucket-id=${buildbucket_build_id}"
+        ],
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_platform_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_platform_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_crypto_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "boringssl_crypto_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "boringssl_crypto_tests",
+        "test_id_prefix": "ninja://third_party/boringssl:boringssl_crypto_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_ssl_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "boringssl_ssl_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "boringssl_ssl_tests",
+        "test_id_prefix": "ninja://third_party/boringssl:boringssl_ssl_tests/"
+      },
+      {
+        "args": [
+          "--gtest_filter=-*UsingRealWebcam*",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "capture_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "capture_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "capture_unittests",
+        "test_id_prefix": "ninja://media/capture:capture_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cast_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "cast_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "cast_unittests",
+        "test_id_prefix": "ninja://media/cast:cast_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cc_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "cc_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "cc_unittests",
+        "test_id_prefix": "ninja://cc:cc_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_smoke_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_smoke_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_public_smoke_test",
+        "test_id_prefix": "ninja://chrome/android:chrome_public_smoke_test/"
+      },
+      {
+        "args": [
+          "--git-revision=${got_revision}",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_test_apk",
+        "precommit_args": [
+          "--gerrit-issue=${patch_issue}",
+          "--gerrit-patchset=${patch_set}",
+          "--buildbucket-id=${buildbucket_build_id}"
+        ],
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 20
+        },
+        "test": "chrome_public_test_apk",
+        "test_id_prefix": "ninja://chrome/android:chrome_public_test_apk/"
+      },
+      {
+        "args": [
+          "--git-revision=${got_revision}",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_unit_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_unit_test_apk",
+        "precommit_args": [
+          "--gerrit-issue=${patch_issue}",
+          "--gerrit-patchset=${patch_set}",
+          "--buildbucket-id=${buildbucket_build_id}"
+        ],
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
+        },
+        "test": "chrome_public_unit_test_apk",
+        "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "components_browsertests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
+        },
+        "test": "components_browsertests",
+        "test_id_prefix": "ninja://components:components_browsertests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "components_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 6
+        },
+        "test": "components_unittests",
+        "test_id_prefix": "ninja://components:components_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "content_browsertests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 15
+        },
+        "test": "content_browsertests",
+        "test_id_prefix": "ninja://content/test:content_browsertests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_shell_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "content_shell_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 6
+        },
+        "test": "content_shell_test_apk",
+        "test_id_prefix": "ninja://content/shell/android:content_shell_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "content_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "content_unittests",
+        "test_id_prefix": "ninja://content/test:content_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crashpad_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "crashpad_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "crashpad_tests",
+        "test_id_prefix": "ninja://third_party/crashpad/crashpad:crashpad_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crypto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "crypto_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "crypto_unittests",
+        "test_id_prefix": "ninja://crypto:crypto_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "device_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "device_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "device_unittests",
+        "test_id_prefix": "ninja://device:device_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "display_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "display_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "display_unittests",
+        "test_id_prefix": "ninja://ui/display:display_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "ci_only": true,
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "env_chromium_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "env_chromium_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "env_chromium_unittests",
+        "test_id_prefix": "ninja://third_party/leveldatabase:env_chromium_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "events_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "events_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "events_unittests",
+        "test_id_prefix": "ninja://ui/events:events_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "fuzzing_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "fuzzing_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "fuzzing_unittests",
+        "test_id_prefix": "ninja://testing/libfuzzer/tests:fuzzing_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gcm_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gcm_unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gcm_unit_tests",
+        "test_id_prefix": "ninja://google_apis/gcm:gcm_unit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gfx_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gfx_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gfx_unittests",
+        "test_id_prefix": "ninja://ui/gfx:gfx_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gin_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gin_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gin_unittests",
+        "test_id_prefix": "ninja://gin:gin_unittests/"
+      },
+      {
+        "args": [
+          "--use-cmd-decoder=validating",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_tests_validating"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gl_tests_validating",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gl_tests",
+        "test_id_prefix": "ninja://gpu:gl_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gl_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gl_unittests",
+        "test_id_prefix": "ninja://ui/gl:gl_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "google_apis_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "google_apis_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "google_apis_unittests",
+        "test_id_prefix": "ninja://google_apis:google_apis_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gpu_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gpu_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gpu_unittests",
+        "test_id_prefix": "ninja://gpu:gpu_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gwp_asan_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gwp_asan_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gwp_asan_unittests",
+        "test_id_prefix": "ninja://components/gwp_asan:gwp_asan_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ipc_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ipc_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ipc_tests",
+        "test_id_prefix": "ninja://ipc:ipc_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "latency_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "latency_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "latency_unittests",
+        "test_id_prefix": "ninja://ui/latency:latency_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "ci_only": true,
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "leveldb_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "leveldb_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "leveldb_unittests",
+        "test_id_prefix": "ninja://third_party/leveldatabase:leveldb_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "libjingle_xmpp_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "libjingle_xmpp_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "libjingle_xmpp_unittests",
+        "test_id_prefix": "ninja://third_party/libjingle_xmpp:libjingle_xmpp_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "liburlpattern_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "liburlpattern_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "liburlpattern_unittests",
+        "test_id_prefix": "ninja://third_party/liburlpattern:liburlpattern_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "media_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "media_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "media_unittests",
+        "test_id_prefix": "ninja://media:media_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "midi_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "midi_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "midi_unittests",
+        "test_id_prefix": "ninja://media/midi:midi_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "mojo_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "mojo_test_apk",
+        "test_id_prefix": "ninja://mojo/public/java/system:mojo_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "mojo_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "mojo_unittests",
+        "test_id_prefix": "ninja://mojo:mojo_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "net_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "net_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "net_unittests",
+        "test_id_prefix": "ninja://net:net_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "perfetto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "perfetto_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "perfetto_unittests",
+        "test_id_prefix": "ninja://third_party/perfetto:perfetto_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sandbox_linux_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "sandbox_linux_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "sandbox_linux_unittests",
+        "test_id_prefix": "ninja://sandbox/linux:sandbox_linux_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "services_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "services_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "services_unittests",
+        "test_id_prefix": "ninja://services:services_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "shell_dialogs_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "shell_dialogs_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "shell_dialogs_unittests",
+        "test_id_prefix": "ninja://ui/shell_dialogs:shell_dialogs_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "skia_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "skia_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "skia_unittests",
+        "test_id_prefix": "ninja://skia:skia_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sql_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "sql_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "sql_unittests",
+        "test_id_prefix": "ninja://sql:sql_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "storage_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "storage_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "storage_unittests",
+        "test_id_prefix": "ninja://storage:storage_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "system_webview_shell_layout_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "system_webview_shell_layout_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "system_webview_shell_layout_test_apk",
+        "test_id_prefix": "ninja://android_webview/tools/system_webview_shell:system_webview_shell_layout_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "trichrome_chrome_bundle_smoke_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "trichrome_chrome_bundle_smoke_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "trichrome_chrome_bundle_smoke_test",
+        "test_id_prefix": "ninja://chrome/android:trichrome_chrome_bundle_smoke_test/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_android_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ui_android_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_android_unittests",
+        "test_id_prefix": "ninja://ui/android:ui_android_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ui_base_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_base_unittests",
+        "test_id_prefix": "ninja://ui/base:ui_base_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_touch_selection_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ui_touch_selection_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_touch_selection_unittests",
+        "test_id_prefix": "ninja://ui/touch_selection:ui_touch_selection_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "unit_tests",
+        "test_id_prefix": "ninja://chrome/test:unit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "url_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "url_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "url_unittests",
+        "test_id_prefix": "ninja://url:url_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "viz_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "viz_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "viz_unittests",
+        "test_id_prefix": "ninja://components/viz:viz_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webkit_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webkit_unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 6
+        },
+        "test": "blink_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/controller:blink_unittests/"
+      },
+      {
+        "args": [
+          "--webview-process-mode=multiple",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_instrumentation_test_apk_multiple_process_mode"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_instrumentation_test_apk_multiple_process_mode",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 5
+        },
+        "test": "webview_instrumentation_test_apk",
+        "test_id_prefix": "ninja://android_webview/test:webview_instrumentation_test_apk/"
+      },
+      {
+        "args": [
+          "--store-tombstones",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_trichrome_cts_tests full_mode"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_trichrome_cts_tests full_mode",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/android_webview/tools/cts_archive",
+              "location": "android_webview/tools/cts_archive/cipd",
+              "revision": "UYQZhJpB3MWpJIAcesI-M1bqRoTghiKCYr_SD9tPDewC"
+            }
+          ],
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "webview_trichrome_cts_tests",
+        "test_id_prefix": "ninja://android_webview/test:webview_trichrome_cts_tests/",
+        "variant_id": "full_mode"
+      },
+      {
+        "args": [
+          "--store-tombstones",
+          "--exclude-annotation",
+          "AppModeFull",
+          "--test-apk-as-instant",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_trichrome_cts_tests instant_mode"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_trichrome_cts_tests instant_mode",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/android_webview/tools/cts_archive",
+              "location": "android_webview/tools/cts_archive/cipd",
+              "revision": "UYQZhJpB3MWpJIAcesI-M1bqRoTghiKCYr_SD9tPDewC"
+            }
+          ],
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webview_trichrome_cts_tests",
+        "test_id_prefix": "ninja://android_webview/test:webview_trichrome_cts_tests/",
+        "variant_id": "instant_mode"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_ui_test_app_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_ui_test_app_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webview_ui_test_app_test_apk",
+        "test_id_prefix": "ninja://android_webview/tools/automated_ui_tests:webview_ui_test_app_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "wtf_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "wtf_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "wtf_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/platform/wtf:wtf_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "zlib_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "zlib_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "zlib_unittests",
+        "test_id_prefix": "ninja://third_party/zlib:zlib_unittests/"
+      }
+    ],
+    "isolated_scripts": [
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "android_webview_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "android_webview_junit_tests",
+        "test_id_prefix": "ninja://android_webview/test:android_webview_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "base_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "base_junit_tests",
+        "test_id_prefix": "ninja://base:base_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "build_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "build_junit_tests",
+        "test_id_prefix": "ninja://build/android:build_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "chrome_java_test_pagecontroller_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_java_test_pagecontroller_junit_tests",
+        "test_id_prefix": "ninja://chrome/test/android:chrome_java_test_pagecontroller_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "chrome_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_junit_tests",
+        "test_id_prefix": "ninja://chrome/android:chrome_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "components_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "components_junit_tests",
+        "test_id_prefix": "ninja://components:components_junit_tests/"
+      },
+      {
+        "args": [
+          "--gtest-benchmark-name=components_perftests",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--smoke-test-mode"
+          ],
+          "script": "//tools/perf/process_perf_results.py"
+        },
+        "name": "components_perftests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "components_perftests",
+        "test_id_prefix": "ninja://components:components_perftests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "content_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "content_junit_tests",
+        "test_id_prefix": "ninja://content/public/android:content_junit_tests/"
+      },
+      {
+        "args": [
+          "--platform=android",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "content_shell_crash_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "content_shell_crash_test",
+        "test_id_prefix": "ninja://content/shell:content_shell_crash_test/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "device_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "device_junit_tests",
+        "test_id_prefix": "ninja://device:device_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "junit_unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "junit_unit_tests",
+        "test_id_prefix": "ninja://testing/android/junit:junit_unit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "keyboard_accessory_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "keyboard_accessory_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/features/keyboard_accessory:keyboard_accessory_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "media_base_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "media_base_junit_tests",
+        "test_id_prefix": "ninja://media/base/android:media_base_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "module_installer_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "module_installer_junit_tests",
+        "test_id_prefix": "ninja://components/module_installer/android:module_installer_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "monochrome_public_apk_checker",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_os_flavor": null,
+            "device_playstore_version": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "monochrome_public_apk_checker",
+        "test_id_prefix": "ninja://chrome/android/monochrome:monochrome_public_apk_checker/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "net_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "net_junit_tests",
+        "test_id_prefix": "ninja://net/android:net_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "paint_preview_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "paint_preview_junit_tests",
+        "test_id_prefix": "ninja://components/paint_preview/player/android:paint_preview_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "password_check_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "password_check_junit_tests",
+        "test_id_prefix": "ninja://chrome/browser/password_check/android:password_check_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "password_manager_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "password_manager_junit_tests",
+        "test_id_prefix": "ninja://chrome/browser/password_manager/android:password_manager_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "services_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "services_junit_tests",
+        "test_id_prefix": "ninja://services:services_junit_tests/"
+      },
+      {
+        "args": [
+          "BrowserMinidumpTest",
+          "--browser=android-chromium",
+          "-v",
+          "--passthrough",
+          "--retry-limit=2",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "telemetry_chromium_minidump_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "telemetry_perf_unittests_android_chrome",
+        "test_id_prefix": "ninja://chrome/test:telemetry_perf_unittests_android_chrome/"
+      },
+      {
+        "args": [
+          "BrowserMinidumpTest",
+          "--browser=android-chromium-monochrome",
+          "-v",
+          "--passthrough",
+          "--retry-limit=2",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "telemetry_monochrome_minidump_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "telemetry_perf_unittests_android_monochrome",
+        "test_id_prefix": "ninja://chrome/test:telemetry_perf_unittests_android_monochrome/"
+      },
+      {
+        "args": [
+          "--extra-browser-args=--enable-crashpad",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "telemetry_perf_unittests_android_chrome",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "idempotent": false,
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 12
+        },
+        "test": "telemetry_perf_unittests_android_chrome",
+        "test_id_prefix": "ninja://chrome/test:telemetry_perf_unittests_android_chrome/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "touch_to_fill_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "touch_to_fill_junit_tests",
+        "test_id_prefix": "ninja://chrome/browser/touch_to_fill/password_manager/android:touch_to_fill_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "ui_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_junit_tests",
+        "test_id_prefix": "ninja://ui:ui_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "webapk_client_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webapk_client_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/webapk/libs/client:webapk_client_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "webapk_shell_apk_h2o_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webapk_shell_apk_h2o_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/webapk/shell_apk:webapk_shell_apk_h2o_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "webapk_shell_apk_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webapk_shell_apk_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/webapk/shell_apk:webapk_shell_apk_junit_tests/"
+      }
+    ],
+    "scripts": [
+      {
+        "name": "check_network_annotations",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "script": "check_network_annotations.py"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/ci/android-15-x64-rel/targets/chromium.android.json b/infra/config/generated/builders/ci/android-15-x64-rel/targets/chromium.android.json
index 9b1e712..2a79dd83e 100644
--- a/infra/config/generated/builders/ci/android-15-x64-rel/targets/chromium.android.json
+++ b/infra/config/generated/builders/ci/android-15-x64-rel/targets/chromium.android.json
@@ -90,7 +90,7 @@
             }
           },
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 14
+          "shards": 10
         },
         "test": "android_browsertests",
         "test_id_prefix": "ninja://chrome/test:android_browsertests/"
diff --git a/infra/config/generated/builders/gn_args_locations.json b/infra/config/generated/builders/gn_args_locations.json
index b11156f..1879bfd8 100644
--- a/infra/config/generated/builders/gn_args_locations.json
+++ b/infra/config/generated/builders/gn_args_locations.json
@@ -79,6 +79,7 @@
     "android-desktop-x64-compile-rel": "ci/android-desktop-x64-compile-rel/gn-args.json"
   },
   "chromium.android.fyi": {
+    "android-10-x86-fyi-rel": "ci/android-10-x86-fyi-rel/gn-args.json",
     "android-12l-x64-fyi-dbg": "ci/android-12l-x64-fyi-dbg/gn-args.json",
     "android-13-x64-fyi-rel": "ci/android-13-x64-fyi-rel/gn-args.json",
     "android-14-arm64-fyi-rel": "ci/android-14-arm64-fyi-rel/gn-args.json",
@@ -551,6 +552,7 @@
   },
   "tryserver.chromium.android": {
     "android-10-arm64-rel": "try/android-10-arm64-rel/gn-args.json",
+    "android-10-x86-fyi-rel": "try/android-10-x86-fyi-rel/gn-args.json",
     "android-11-x86-rel": "try/android-11-x86-rel/gn-args.json",
     "android-12-x64-dbg": "try/android-12-x64-dbg/gn-args.json",
     "android-12-x64-rel": "try/android-12-x64-rel/gn-args.json",
diff --git a/infra/config/generated/builders/try/android-10-x86-fyi-rel/gn-args.json b/infra/config/generated/builders/try/android-10-x86-fyi-rel/gn-args.json
new file mode 100644
index 0000000..78a028a
--- /dev/null
+++ b/infra/config/generated/builders/try/android-10-x86-fyi-rel/gn-args.json
@@ -0,0 +1,20 @@
+{
+  "gn_args": {
+    "android_static_analysis": "off",
+    "dcheck_always_on": true,
+    "debuggable_apks": false,
+    "ffmpeg_branding": "Chrome",
+    "is_component_build": false,
+    "is_debug": false,
+    "proprietary_codecs": true,
+    "strip_debug_info": true,
+    "symbol_level": 0,
+    "system_webview_package_name": "com.google.android.apps.chrome",
+    "system_webview_shell_package_name": "org.chromium.my_webview_shell",
+    "target_cpu": "x86",
+    "target_os": "android",
+    "use_reclient": false,
+    "use_remoteexec": true,
+    "use_siso": true
+  }
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/try/android-10-x86-fyi-rel/properties.json b/infra/config/generated/builders/try/android-10-x86-fyi-rel/properties.json
new file mode 100644
index 0000000..987ad8d
--- /dev/null
+++ b/infra/config/generated/builders/try/android-10-x86-fyi-rel/properties.json
@@ -0,0 +1,71 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "additional_exclusions": [
+        "infra/config/generated/builders/try/android-10-x86-fyi-rel/gn-args.json"
+      ],
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "ci",
+              "builder": "android-10-x86-fyi-rel",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-android-archive",
+              "builder_group": "chromium.android.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_android_config": {
+                "config": "x86_builder_mb"
+              },
+              "legacy_chromium_config": {
+                "build_config": "Release",
+                "config": "android",
+                "target_bits": 32,
+                "target_platform": "android"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "android"
+                ],
+                "config": "chromium"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "ci",
+          "builder": "android-10-x86-fyi-rel",
+          "project": "chromium"
+        }
+      ],
+      "targets_spec_directory": "src/infra/config/generated/builders/try/android-10-x86-fyi-rel/targets"
+    }
+  },
+  "$build/siso": {
+    "configs": [
+      "builder",
+      "remote-link"
+    ],
+    "enable_cloud_monitoring": true,
+    "enable_cloud_profiler": true,
+    "enable_cloud_trace": true,
+    "experiments": [],
+    "metrics_project": "chromium-reclient-metrics",
+    "output_local_strategy": "greedy",
+    "project": "rbe-chromium-untrusted",
+    "remote_jobs": 150
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "tryserver.chromium.android",
+  "recipe": "chromium_trybot"
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/try/android-10-x86-fyi-rel/targets/chromium.android.fyi.json b/infra/config/generated/builders/try/android-10-x86-fyi-rel/targets/chromium.android.fyi.json
new file mode 100644
index 0000000..2ba0a2e
--- /dev/null
+++ b/infra/config/generated/builders/try/android-10-x86-fyi-rel/targets/chromium.android.fyi.json
@@ -0,0 +1,4386 @@
+{
+  "android-10-x86-fyi-rel": {
+    "additional_compile_targets": [
+      "chrome_nocompile_tests"
+    ],
+    "gtest_tests": [
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "absl_hardening_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "absl_hardening_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "absl_hardening_tests",
+        "test_id_prefix": "ninja://third_party/abseil-cpp:absl_hardening_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_browsertests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 9
+        },
+        "test": "android_browsertests",
+        "test_id_prefix": "ninja://chrome/test:android_browsertests/"
+      },
+      {
+        "args": [
+          "--test-launcher-batch-limit=1",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_sync_integration_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_sync_integration_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "android_sync_integration_tests",
+        "test_id_prefix": "ninja://chrome/test:android_sync_integration_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_webview_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_webview_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "android_webview_unittests",
+        "test_id_prefix": "ninja://android_webview/test:android_webview_unittests/"
+      },
+      {
+        "args": [
+          "-v",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "angle_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "angle_unittests",
+        "test_id_prefix": "ninja://third_party/angle/src/tests:angle_unittests/",
+        "use_isolated_scripts_api": true
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "base_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "base_unittests",
+        "test_id_prefix": "ninja://base:base_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_common_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "blink_common_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_common_unittests",
+        "test_id_prefix": "ninja://third_party/blink/common:blink_common_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_heap_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "blink_heap_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_heap_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/platform/heap:blink_heap_unittests/"
+      },
+      {
+        "args": [
+          "--git-revision=${got_revision}",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_platform_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "blink_platform_unittests",
+        "precommit_args": [
+          "--gerrit-issue=${patch_issue}",
+          "--gerrit-patchset=${patch_set}",
+          "--buildbucket-id=${buildbucket_build_id}"
+        ],
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_platform_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_platform_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_crypto_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "boringssl_crypto_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "boringssl_crypto_tests",
+        "test_id_prefix": "ninja://third_party/boringssl:boringssl_crypto_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_ssl_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "boringssl_ssl_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "boringssl_ssl_tests",
+        "test_id_prefix": "ninja://third_party/boringssl:boringssl_ssl_tests/"
+      },
+      {
+        "args": [
+          "--gtest_filter=-*UsingRealWebcam*",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "capture_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "capture_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "capture_unittests",
+        "test_id_prefix": "ninja://media/capture:capture_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cast_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "cast_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "cast_unittests",
+        "test_id_prefix": "ninja://media/cast:cast_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cc_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "cc_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "cc_unittests",
+        "test_id_prefix": "ninja://cc:cc_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_smoke_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_smoke_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_public_smoke_test",
+        "test_id_prefix": "ninja://chrome/android:chrome_public_smoke_test/"
+      },
+      {
+        "args": [
+          "--git-revision=${got_revision}",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_test_apk",
+        "precommit_args": [
+          "--gerrit-issue=${patch_issue}",
+          "--gerrit-patchset=${patch_set}",
+          "--buildbucket-id=${buildbucket_build_id}"
+        ],
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 20
+        },
+        "test": "chrome_public_test_apk",
+        "test_id_prefix": "ninja://chrome/android:chrome_public_test_apk/"
+      },
+      {
+        "args": [
+          "--git-revision=${got_revision}",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_unit_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_unit_test_apk",
+        "precommit_args": [
+          "--gerrit-issue=${patch_issue}",
+          "--gerrit-patchset=${patch_set}",
+          "--buildbucket-id=${buildbucket_build_id}"
+        ],
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
+        },
+        "test": "chrome_public_unit_test_apk",
+        "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "components_browsertests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
+        },
+        "test": "components_browsertests",
+        "test_id_prefix": "ninja://components:components_browsertests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "components_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 6
+        },
+        "test": "components_unittests",
+        "test_id_prefix": "ninja://components:components_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "content_browsertests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 15
+        },
+        "test": "content_browsertests",
+        "test_id_prefix": "ninja://content/test:content_browsertests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_shell_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "content_shell_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 6
+        },
+        "test": "content_shell_test_apk",
+        "test_id_prefix": "ninja://content/shell/android:content_shell_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "content_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "content_unittests",
+        "test_id_prefix": "ninja://content/test:content_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crashpad_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "crashpad_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "crashpad_tests",
+        "test_id_prefix": "ninja://third_party/crashpad/crashpad:crashpad_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crypto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "crypto_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "crypto_unittests",
+        "test_id_prefix": "ninja://crypto:crypto_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "device_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "device_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "device_unittests",
+        "test_id_prefix": "ninja://device:device_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "display_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "display_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "display_unittests",
+        "test_id_prefix": "ninja://ui/display:display_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "ci_only": true,
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "env_chromium_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "env_chromium_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "env_chromium_unittests",
+        "test_id_prefix": "ninja://third_party/leveldatabase:env_chromium_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "events_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "events_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "events_unittests",
+        "test_id_prefix": "ninja://ui/events:events_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "fuzzing_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "fuzzing_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "fuzzing_unittests",
+        "test_id_prefix": "ninja://testing/libfuzzer/tests:fuzzing_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gcm_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gcm_unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gcm_unit_tests",
+        "test_id_prefix": "ninja://google_apis/gcm:gcm_unit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gfx_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gfx_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gfx_unittests",
+        "test_id_prefix": "ninja://ui/gfx:gfx_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gin_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gin_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gin_unittests",
+        "test_id_prefix": "ninja://gin:gin_unittests/"
+      },
+      {
+        "args": [
+          "--use-cmd-decoder=validating",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_tests_validating"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gl_tests_validating",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gl_tests",
+        "test_id_prefix": "ninja://gpu:gl_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gl_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gl_unittests",
+        "test_id_prefix": "ninja://ui/gl:gl_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "google_apis_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "google_apis_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "google_apis_unittests",
+        "test_id_prefix": "ninja://google_apis:google_apis_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gpu_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gpu_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gpu_unittests",
+        "test_id_prefix": "ninja://gpu:gpu_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gwp_asan_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "gwp_asan_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gwp_asan_unittests",
+        "test_id_prefix": "ninja://components/gwp_asan:gwp_asan_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ipc_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ipc_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ipc_tests",
+        "test_id_prefix": "ninja://ipc:ipc_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "latency_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "latency_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "latency_unittests",
+        "test_id_prefix": "ninja://ui/latency:latency_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "ci_only": true,
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "leveldb_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "leveldb_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "leveldb_unittests",
+        "test_id_prefix": "ninja://third_party/leveldatabase:leveldb_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "libjingle_xmpp_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "libjingle_xmpp_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "libjingle_xmpp_unittests",
+        "test_id_prefix": "ninja://third_party/libjingle_xmpp:libjingle_xmpp_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "liburlpattern_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "liburlpattern_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "liburlpattern_unittests",
+        "test_id_prefix": "ninja://third_party/liburlpattern:liburlpattern_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "media_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "media_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "media_unittests",
+        "test_id_prefix": "ninja://media:media_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "midi_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "midi_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "midi_unittests",
+        "test_id_prefix": "ninja://media/midi:midi_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "mojo_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "mojo_test_apk",
+        "test_id_prefix": "ninja://mojo/public/java/system:mojo_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "mojo_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "mojo_unittests",
+        "test_id_prefix": "ninja://mojo:mojo_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "net_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "net_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "net_unittests",
+        "test_id_prefix": "ninja://net:net_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "perfetto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "perfetto_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "perfetto_unittests",
+        "test_id_prefix": "ninja://third_party/perfetto:perfetto_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sandbox_linux_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "sandbox_linux_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "sandbox_linux_unittests",
+        "test_id_prefix": "ninja://sandbox/linux:sandbox_linux_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "services_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "services_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "services_unittests",
+        "test_id_prefix": "ninja://services:services_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "shell_dialogs_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "shell_dialogs_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "shell_dialogs_unittests",
+        "test_id_prefix": "ninja://ui/shell_dialogs:shell_dialogs_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "skia_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "skia_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "skia_unittests",
+        "test_id_prefix": "ninja://skia:skia_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sql_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "sql_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "sql_unittests",
+        "test_id_prefix": "ninja://sql:sql_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "storage_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "storage_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "storage_unittests",
+        "test_id_prefix": "ninja://storage:storage_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "system_webview_shell_layout_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "system_webview_shell_layout_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "system_webview_shell_layout_test_apk",
+        "test_id_prefix": "ninja://android_webview/tools/system_webview_shell:system_webview_shell_layout_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "trichrome_chrome_bundle_smoke_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "trichrome_chrome_bundle_smoke_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "trichrome_chrome_bundle_smoke_test",
+        "test_id_prefix": "ninja://chrome/android:trichrome_chrome_bundle_smoke_test/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_android_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ui_android_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_android_unittests",
+        "test_id_prefix": "ninja://ui/android:ui_android_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ui_base_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_base_unittests",
+        "test_id_prefix": "ninja://ui/base:ui_base_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_touch_selection_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "ui_touch_selection_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_touch_selection_unittests",
+        "test_id_prefix": "ninja://ui/touch_selection:ui_touch_selection_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "unit_tests",
+        "test_id_prefix": "ninja://chrome/test:unit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "url_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "url_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "url_unittests",
+        "test_id_prefix": "ninja://url:url_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "viz_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "viz_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "viz_unittests",
+        "test_id_prefix": "ninja://components/viz:viz_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webkit_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webkit_unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 6
+        },
+        "test": "blink_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/controller:blink_unittests/"
+      },
+      {
+        "args": [
+          "--webview-process-mode=multiple",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_instrumentation_test_apk_multiple_process_mode"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_instrumentation_test_apk_multiple_process_mode",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 5
+        },
+        "test": "webview_instrumentation_test_apk",
+        "test_id_prefix": "ninja://android_webview/test:webview_instrumentation_test_apk/"
+      },
+      {
+        "args": [
+          "--store-tombstones",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_trichrome_cts_tests full_mode"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_trichrome_cts_tests full_mode",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/android_webview/tools/cts_archive",
+              "location": "android_webview/tools/cts_archive/cipd",
+              "revision": "UYQZhJpB3MWpJIAcesI-M1bqRoTghiKCYr_SD9tPDewC"
+            }
+          ],
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "webview_trichrome_cts_tests",
+        "test_id_prefix": "ninja://android_webview/test:webview_trichrome_cts_tests/",
+        "variant_id": "full_mode"
+      },
+      {
+        "args": [
+          "--store-tombstones",
+          "--exclude-annotation",
+          "AppModeFull",
+          "--test-apk-as-instant",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_trichrome_cts_tests instant_mode"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_trichrome_cts_tests instant_mode",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/android_webview/tools/cts_archive",
+              "location": "android_webview/tools/cts_archive/cipd",
+              "revision": "UYQZhJpB3MWpJIAcesI-M1bqRoTghiKCYr_SD9tPDewC"
+            }
+          ],
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webview_trichrome_cts_tests",
+        "test_id_prefix": "ninja://android_webview/test:webview_trichrome_cts_tests/",
+        "variant_id": "instant_mode"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_ui_test_app_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "webview_ui_test_app_test_apk",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webview_ui_test_app_test_apk",
+        "test_id_prefix": "ninja://android_webview/tools/automated_ui_tests:webview_ui_test_app_test_apk/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "wtf_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "wtf_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "wtf_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/platform/wtf:wtf_unittests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "zlib_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "zlib_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "zlib_unittests",
+        "test_id_prefix": "ninja://third_party/zlib:zlib_unittests/"
+      }
+    ],
+    "isolated_scripts": [
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "android_webview_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "android_webview_junit_tests",
+        "test_id_prefix": "ninja://android_webview/test:android_webview_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "base_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "base_junit_tests",
+        "test_id_prefix": "ninja://base:base_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "build_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "build_junit_tests",
+        "test_id_prefix": "ninja://build/android:build_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "chrome_java_test_pagecontroller_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_java_test_pagecontroller_junit_tests",
+        "test_id_prefix": "ninja://chrome/test/android:chrome_java_test_pagecontroller_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "chrome_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_junit_tests",
+        "test_id_prefix": "ninja://chrome/android:chrome_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "components_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "components_junit_tests",
+        "test_id_prefix": "ninja://components:components_junit_tests/"
+      },
+      {
+        "args": [
+          "--gtest-benchmark-name=components_perftests",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "args": [
+            "--smoke-test-mode"
+          ],
+          "script": "//tools/perf/process_perf_results.py"
+        },
+        "name": "components_perftests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "components_perftests",
+        "test_id_prefix": "ninja://components:components_perftests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "content_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "content_junit_tests",
+        "test_id_prefix": "ninja://content/public/android:content_junit_tests/"
+      },
+      {
+        "args": [
+          "--platform=android",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "content_shell_crash_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "content_shell_crash_test",
+        "test_id_prefix": "ninja://content/shell:content_shell_crash_test/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "device_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "device_junit_tests",
+        "test_id_prefix": "ninja://device:device_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "junit_unit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "junit_unit_tests",
+        "test_id_prefix": "ninja://testing/android/junit:junit_unit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "keyboard_accessory_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "keyboard_accessory_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/features/keyboard_accessory:keyboard_accessory_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "media_base_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "media_base_junit_tests",
+        "test_id_prefix": "ninja://media/base/android:media_base_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "module_installer_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "module_installer_junit_tests",
+        "test_id_prefix": "ninja://components/module_installer/android:module_installer_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "monochrome_public_apk_checker",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_os_flavor": null,
+            "device_playstore_version": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "monochrome_public_apk_checker",
+        "test_id_prefix": "ninja://chrome/android/monochrome:monochrome_public_apk_checker/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "net_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "net_junit_tests",
+        "test_id_prefix": "ninja://net/android:net_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "paint_preview_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "paint_preview_junit_tests",
+        "test_id_prefix": "ninja://components/paint_preview/player/android:paint_preview_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "password_check_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "password_check_junit_tests",
+        "test_id_prefix": "ninja://chrome/browser/password_check/android:password_check_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "password_manager_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "password_manager_junit_tests",
+        "test_id_prefix": "ninja://chrome/browser/password_manager/android:password_manager_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "services_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "services_junit_tests",
+        "test_id_prefix": "ninja://services:services_junit_tests/"
+      },
+      {
+        "args": [
+          "BrowserMinidumpTest",
+          "--browser=android-chromium",
+          "-v",
+          "--passthrough",
+          "--retry-limit=2",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "telemetry_chromium_minidump_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "telemetry_perf_unittests_android_chrome",
+        "test_id_prefix": "ninja://chrome/test:telemetry_perf_unittests_android_chrome/"
+      },
+      {
+        "args": [
+          "BrowserMinidumpTest",
+          "--browser=android-chromium-monochrome",
+          "-v",
+          "--passthrough",
+          "--retry-limit=2",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "telemetry_monochrome_minidump_unittests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "telemetry_perf_unittests_android_monochrome",
+        "test_id_prefix": "ninja://chrome/test:telemetry_perf_unittests_android_monochrome/"
+      },
+      {
+        "args": [
+          "--extra-browser-args=--enable-crashpad",
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "telemetry_perf_unittests_android_chrome",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "4",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "idempotent": false,
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 12
+        },
+        "test": "telemetry_perf_unittests_android_chrome",
+        "test_id_prefix": "ninja://chrome/test:telemetry_perf_unittests_android_chrome/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "touch_to_fill_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "touch_to_fill_junit_tests",
+        "test_id_prefix": "ninja://chrome/browser/touch_to_fill/password_manager/android:touch_to_fill_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "ui_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_junit_tests",
+        "test_id_prefix": "ninja://ui:ui_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "webapk_client_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webapk_client_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/webapk/libs/client:webapk_client_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "webapk_shell_apk_h2o_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webapk_shell_apk_h2o_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/webapk/shell_apk:webapk_shell_apk_h2o_junit_tests/"
+      },
+      {
+        "args": [
+          "--use-persistent-shell",
+          "--avd-config=../../tools/android/avd/proto/android_29_google_apis_x86.textpb"
+        ],
+        "isolate_profile_data": true,
+        "merge": {
+          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
+        },
+        "name": "webapk_shell_apk_junit_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests"
+          },
+          "named_caches": [
+            {
+              "name": "android_29_google_apis_x86",
+              "path": ".android_emulator/android_29_google_apis_x86"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_29_google_apis_x86"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "webapk_shell_apk_junit_tests",
+        "test_id_prefix": "ninja://chrome/android/webapk/shell_apk:webapk_shell_apk_junit_tests/"
+      }
+    ],
+    "scripts": [
+      {
+        "name": "check_network_annotations",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "script": "check_network_annotations.py"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/try/android-15-x64-rel/targets/chromium.android.json b/infra/config/generated/builders/try/android-15-x64-rel/targets/chromium.android.json
index 9b1e712..2a79dd83e 100644
--- a/infra/config/generated/builders/try/android-15-x64-rel/targets/chromium.android.json
+++ b/infra/config/generated/builders/try/android-15-x64-rel/targets/chromium.android.json
@@ -90,7 +90,7 @@
             }
           },
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 14
+          "shards": 10
         },
         "test": "android_browsertests",
         "test_id_prefix": "ninja://chrome/test:android_browsertests/"
diff --git a/infra/config/generated/builders/try/android-x64-rel/targets/chromium.android.json b/infra/config/generated/builders/try/android-x64-rel/targets/chromium.android.json
index 8045728..9921a8f3 100644
--- a/infra/config/generated/builders/try/android-x64-rel/targets/chromium.android.json
+++ b/infra/config/generated/builders/try/android-x64-rel/targets/chromium.android.json
@@ -147,7 +147,7 @@
             }
           },
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 14
+          "shards": 10
         },
         "test": "android_browsertests",
         "test_id_prefix": "ninja://chrome/test:android_browsertests/"
diff --git a/infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json b/infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json
index c9d5fa0..97a6afe 100644
--- a/infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json
+++ b/infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json
@@ -66,5 +66,5 @@
   },
   "builder_group": "tryserver.chromium.mac",
   "recipe": "chromium_trybot",
-  "xcode_build_version": "15f31d"
+  "xcode_build_version": "16c5032a"
 }
\ No newline at end of file
diff --git a/infra/config/generated/cq-builders.md b/infra/config/generated/cq-builders.md
index b77d47ee..287d3851f 100644
--- a/infra/config/generated/cq-builders.md
+++ b/infra/config/generated/cq-builders.md
@@ -92,6 +92,36 @@
   Location filters:
   * [`//infra/config/generated/cq-usage/full.cfg`](https://cs.chromium.org/search?q=+file:infra/config/generated/cq-usage/full.cfg)
 
+* [optimization_guide-linux](https://ci.chromium.org/p/chrome/builders/try/optimization_guide-linux) ([definition](https://source.corp.google.com/search?q=+file:/try/.*\.star$+""optimization_guide-linux""))
+
+  Location filters:
+  * [`//chrome/browser/ai/.+`](https://cs.chromium.org/chromium/src/chrome/browser/ai/)
+  * [`//components/optimization_guide/.+`](https://cs.chromium.org/chromium/src/components/optimization_guide/)
+  * [`//services/on_device_model/.+`](https://cs.chromium.org/chromium/src/services/on_device_model/)
+
+  This builder is only run when the CL owner is in the group:
+  * `[optimization-guide-try-opt-in](https://chrome-infra-auth.appspot.com/auth/lookup?p=optimization-guide-try-opt-in)`
+
+* [optimization_guide-mac-arm64](https://ci.chromium.org/p/chrome/builders/try/optimization_guide-mac-arm64) ([definition](https://source.corp.google.com/search?q=+file:/try/.*\.star$+""optimization_guide-mac-arm64""))
+
+  Location filters:
+  * [`//chrome/browser/ai/.+`](https://cs.chromium.org/chromium/src/chrome/browser/ai/)
+  * [`//components/optimization_guide/.+`](https://cs.chromium.org/chromium/src/components/optimization_guide/)
+  * [`//services/on_device_model/.+`](https://cs.chromium.org/chromium/src/services/on_device_model/)
+
+  This builder is only run when the CL owner is in the group:
+  * `[optimization-guide-try-opt-in](https://chrome-infra-auth.appspot.com/auth/lookup?p=optimization-guide-try-opt-in)`
+
+* [optimization_guide-win64](https://ci.chromium.org/p/chrome/builders/try/optimization_guide-win64) ([definition](https://source.corp.google.com/search?q=+file:/try/.*\.star$+""optimization_guide-win64""))
+
+  Location filters:
+  * [`//chrome/browser/ai/.+`](https://cs.chromium.org/chromium/src/chrome/browser/ai/)
+  * [`//components/optimization_guide/.+`](https://cs.chromium.org/chromium/src/components/optimization_guide/)
+  * [`//services/on_device_model/.+`](https://cs.chromium.org/chromium/src/services/on_device_model/)
+
+  This builder is only run when the CL owner is in the group:
+  * `[optimization-guide-try-opt-in](https://chrome-infra-auth.appspot.com/auth/lookup?p=optimization-guide-try-opt-in)`
+
 ### chromium
 * [3pp-linux-amd64-packager](https://ci.chromium.org/p/chromium/builders/try/3pp-linux-amd64-packager) ([definition](https://cs.chromium.org/search?q=+file:/try/.*\.star$+""3pp-linux-amd64-packager""))
 
@@ -774,31 +804,6 @@
 by CQ. These are often used to test new configurations before they are added
 as required builders.
 
-### chrome
-* [optimization_guide-linux](https://ci.chromium.org/p/chrome/builders/try/optimization_guide-linux) ([definition](https://source.corp.google.com/search?q=+file:/try/.*\.star$+""optimization_guide-linux""))
-  * Experiment percentage: 100.0
-
-  Location filters:
-  * [`//chrome/browser/ai/.+`](https://cs.chromium.org/chromium/src/chrome/browser/ai/)
-  * [`//components/optimization_guide/.+`](https://cs.chromium.org/chromium/src/components/optimization_guide/)
-  * [`//services/on_device_model/.+`](https://cs.chromium.org/chromium/src/services/on_device_model/)
-
-* [optimization_guide-mac-arm64](https://ci.chromium.org/p/chrome/builders/try/optimization_guide-mac-arm64) ([definition](https://source.corp.google.com/search?q=+file:/try/.*\.star$+""optimization_guide-mac-arm64""))
-  * Experiment percentage: 100.0
-
-  Location filters:
-  * [`//chrome/browser/ai/.+`](https://cs.chromium.org/chromium/src/chrome/browser/ai/)
-  * [`//components/optimization_guide/.+`](https://cs.chromium.org/chromium/src/components/optimization_guide/)
-  * [`//services/on_device_model/.+`](https://cs.chromium.org/chromium/src/services/on_device_model/)
-
-* [optimization_guide-win64](https://ci.chromium.org/p/chrome/builders/try/optimization_guide-win64) ([definition](https://source.corp.google.com/search?q=+file:/try/.*\.star$+""optimization_guide-win64""))
-  * Experiment percentage: 100.0
-
-  Location filters:
-  * [`//chrome/browser/ai/.+`](https://cs.chromium.org/chromium/src/chrome/browser/ai/)
-  * [`//components/optimization_guide/.+`](https://cs.chromium.org/chromium/src/components/optimization_guide/)
-  * [`//services/on_device_model/.+`](https://cs.chromium.org/chromium/src/services/on_device_model/)
-
 ### chromium
 * [chromeos-js-coverage-rel](https://ci.chromium.org/p/chromium/builders/try/chromeos-js-coverage-rel) ([definition](https://cs.chromium.org/search?q=+file:/try/.*\.star$+""chromeos-js-coverage-rel""))
   * Experiment percentage: 50.0
diff --git a/infra/config/generated/cq-usage/full.cfg b/infra/config/generated/cq-usage/full.cfg
index d2be6ef..d44568a79 100644
--- a/infra/config/generated/cq-usage/full.cfg
+++ b/infra/config/generated/cq-usage/full.cfg
@@ -42,6 +42,114 @@
         }
       }
       builders {
+        name: "chrome/try/optimization_guide-linux"
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "chrome/browser/ai/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "components/optimization_guide/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "services/on_device_model/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "infra/config/.+"
+          exclude: true
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "docs/.+"
+          exclude: true
+        }
+        owner_whitelist_group: "optimization-guide-try-opt-in"
+      }
+      builders {
+        name: "chrome/try/optimization_guide-mac-arm64"
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "chrome/browser/ai/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "components/optimization_guide/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "services/on_device_model/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "infra/config/.+"
+          exclude: true
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "docs/.+"
+          exclude: true
+        }
+        owner_whitelist_group: "optimization-guide-try-opt-in"
+      }
+      builders {
+        name: "chrome/try/optimization_guide-win64"
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "chrome/browser/ai/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "components/optimization_guide/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "services/on_device_model/.+"
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "infra/config/.+"
+          exclude: true
+        }
+        location_filters {
+          gerrit_host_regexp: ".*"
+          gerrit_project_regexp: ".*"
+          gerrit_ref_regexp: ".*"
+          path_regexp: "docs/.+"
+          exclude: true
+        }
+        owner_whitelist_group: "optimization-guide-try-opt-in"
+      }
+      builders {
         name: "chrome/try/win-branded-compile-rel"
         location_filters {
           gerrit_host_regexp: ".*"
diff --git a/infra/config/generated/health-specs/health-specs.json b/infra/config/generated/health-specs/health-specs.json
index 22e83e1..3ac6e36f 100644
--- a/infra/config/generated/health-specs/health-specs.json
+++ b/infra/config/generated/health-specs/health-specs.json
@@ -6372,6 +6372,27 @@
           }
         ]
       },
+      "android-10-x86-fyi-rel": {
+        "contact_team_email": "clank-engprod@google.com",
+        "problem_specs": [
+          {
+            "name": "Unhealthy",
+            "period_days": 7,
+            "score": 5,
+            "thresholds": {
+              "_default": "_default"
+            }
+          },
+          {
+            "name": "Low Value",
+            "period_days": 90,
+            "score": 1,
+            "thresholds": {
+              "_default": "_default"
+            }
+          }
+        ]
+      },
       "android-11-x86-fyi-rel": {
         "problem_specs": [
           {
diff --git a/infra/config/generated/luci/commit-queue.cfg b/infra/config/generated/luci/commit-queue.cfg
index 90dc5f1f..1521a188 100644
--- a/infra/config/generated/luci/commit-queue.cfg
+++ b/infra/config/generated/luci/commit-queue.cfg
@@ -451,7 +451,6 @@
       builders {
         name: "chrome/try/optimization_guide-linux"
         result_visibility: COMMENT_LEVEL_RESTRICTED
-        experiment_percentage: 100
         location_filters {
           gerrit_host_regexp: ".*"
           gerrit_project_regexp: ".*"
@@ -489,7 +488,6 @@
       builders {
         name: "chrome/try/optimization_guide-mac-arm64"
         result_visibility: COMMENT_LEVEL_RESTRICTED
-        experiment_percentage: 100
         location_filters {
           gerrit_host_regexp: ".*"
           gerrit_project_regexp: ".*"
@@ -541,7 +539,6 @@
       builders {
         name: "chrome/try/optimization_guide-win64"
         result_visibility: COMMENT_LEVEL_RESTRICTED
-        experiment_percentage: 100
         location_filters {
           gerrit_host_regexp: ".*"
           gerrit_project_regexp: ".*"
@@ -759,6 +756,10 @@
         includable_only: true
       }
       builders {
+        name: "chromium/try/android-10-x86-fyi-rel"
+        includable_only: true
+      }
+      builders {
         name: "chromium/try/android-11-x86-rel"
         includable_only: true
       }
diff --git a/infra/config/generated/luci/cr-buildbucket.cfg b/infra/config/generated/luci/cr-buildbucket.cfg
index 7ada85e..e411965 100644
--- a/infra/config/generated/luci/cr-buildbucket.cfg
+++ b/infra/config/generated/luci/cr-buildbucket.cfg
@@ -36466,6 +36466,118 @@
       }
     }
     builders {
+      name: "android-10-x86-fyi-rel"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cores:8"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Ubuntu-22.04"
+      dimensions: "pool:luci.chromium.ci"
+      dimensions: "ssd:0"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/ci/android-10-x86-fyi-rel/properties.json",'
+        '    "shadow_properties_file": "infra/config/generated/builders/ci/android-10-x86-fyi-rel/shadow-properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.android.fyi",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      priority: 35
+      execution_timeout_secs: 10800
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium.use_per_builder_build_dir_name"
+        value: 100
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://(chrome|content)/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*_wpt_tests/.+)|(ninja://[^/]*headless_shell_wpt/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "Run chromium tests on Android 10 emulators.<br/>This builder is mirrored by any of the following try builders:<br/><ul><li><a href=\"https://ci.chromium.org/p/chromium/builders/try/android-10-x86-fyi-rel\">android-10-x86-fyi-rel</a></li></ul>"
+      shadow_builder_adjustments {
+        service_account: "chromium-try-builder@chops-service-accounts.iam.gserviceaccount.com"
+        pool: "luci.chromium.try"
+        dimensions: "free_space:"
+        dimensions: "pool:luci.chromium.try"
+      }
+      contact_team_email: "clank-engprod@google.com"
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/cached_count"
+        predicates: "has(build.output.properties.is_cached)"
+        predicates: "string(build.output.properties.is_cached) == \"true\""
+      }
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/ran_tests_retry_shard_count"
+        predicates: "has(build.output.properties.ran_tests_retry_shard)"
+      }
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/ran_tests_without_patch_count"
+        predicates: "has(build.output.properties.ran_tests_without_patch)"
+      }
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/uncached_count"
+        predicates: "has(build.output.properties.is_cached)"
+        predicates: "string(build.output.properties.is_cached) == \"false\""
+      }
+    }
+    builders {
       name: "android-11-x86-fyi-rel"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
@@ -79546,6 +79658,118 @@
       }
     }
     builders {
+      name: "android-10-x86-fyi-rel"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cores:8"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Ubuntu-22.04"
+      dimensions: "pool:luci.chromium.try"
+      dimensions: "ssd:0"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/try/android-10-x86-fyi-rel/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "tryserver.chromium.android",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium_trybot"'
+        '}'
+      execution_timeout_secs: 14400
+      expiration_secs: 7200
+      grace_period {
+        seconds: 120
+      }
+      build_numbers: YES
+      service_account: "chromium-try-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium.use_per_builder_build_dir_name"
+        value: 100
+      }
+      experiments {
+        key: "luci.buildbucket.canary_software"
+        value: 5
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "try_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_try_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://(chrome|content)/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_try_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*_wpt_tests/.+)|(ninja://[^/]*headless_shell_wpt/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "<br>Run chromium tests on Android 10 emulators.<br/><br/>This builder mirrors the following CI builders:<br/><ul><li><a href=\"https://ci.chromium.org/p/chromium/builders/ci/android-10-x86-fyi-rel\">android-10-x86-fyi-rel</a></li></ul>"
+      contact_team_email: "clank-engprod@google.com"
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/cached_count"
+        predicates: "has(build.output.properties.is_cached)"
+        predicates: "string(build.output.properties.is_cached) == \"true\""
+      }
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/ran_tests_retry_shard_count"
+        predicates: "has(build.output.properties.ran_tests_retry_shard)"
+      }
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/ran_tests_without_patch_count"
+        predicates: "has(build.output.properties.ran_tests_without_patch)"
+      }
+      custom_metric_definitions {
+        name: "/chrome/infra/browser/builds/uncached_count"
+        predicates: "has(build.output.properties.is_cached)"
+        predicates: "string(build.output.properties.is_cached) == \"false\""
+      }
+    }
+    builders {
       name: "android-11-x86-rel"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
@@ -104410,8 +104634,8 @@
         seconds: 120
       }
       caches {
-        name: "xcode_ios_15f31d"
-        path: "xcode_ios_15f31d.app"
+        name: "xcode_ios_16c5032a"
+        path: "xcode_ios_16c5032a.app"
       }
       build_numbers: YES
       service_account: "chromium-try-builder@chops-service-accounts.iam.gserviceaccount.com"
diff --git a/infra/config/generated/luci/luci-milo.cfg b/infra/config/generated/luci/luci-milo.cfg
index d985911..78cde5c 100644
--- a/infra/config/generated/luci/luci-milo.cfg
+++ b/infra/config/generated/luci/luci-milo.cfg
@@ -5838,6 +5838,11 @@
     short_name: "16"
   }
   builders {
+    name: "buildbucket/luci.chromium.ci/android-10-x86-fyi-rel"
+    category: "emulator|x86|rel"
+    short_name: "10"
+  }
+  builders {
     name: "buildbucket/luci.chromium.ci/android-11-x86-fyi-rel"
     category: "emulator|x86|rel"
     short_name: "11"
@@ -16848,6 +16853,9 @@
     name: "buildbucket/luci.chromium.try/android-10-arm64-rel"
   }
   builders {
+    name: "buildbucket/luci.chromium.try/android-10-x86-fyi-rel"
+  }
+  builders {
     name: "buildbucket/luci.chromium.try/android-11-x86-rel"
   }
   builders {
@@ -18350,6 +18358,9 @@
     name: "buildbucket/luci.chromium.try/android-10-arm64-rel"
   }
   builders {
+    name: "buildbucket/luci.chromium.try/android-10-x86-fyi-rel"
+  }
+  builders {
     name: "buildbucket/luci.chromium.try/android-11-x86-rel"
   }
   builders {
diff --git a/infra/config/generated/luci/luci-scheduler.cfg b/infra/config/generated/luci/luci-scheduler.cfg
index 16adbbf..0000923 100644
--- a/infra/config/generated/luci/luci-scheduler.cfg
+++ b/infra/config/generated/luci/luci-scheduler.cfg
@@ -3257,6 +3257,15 @@
   }
 }
 job {
+  id: "android-10-x86-fyi-rel"
+  realm: "ci"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "android-10-x86-fyi-rel"
+  }
+}
+job {
   id: "android-11-x86-rel"
   realm: "ci"
   buildbucket {
@@ -6697,6 +6706,7 @@
   triggers: "Win x64 Builder (reclient)"
   triggers: "Windows deterministic"
   triggers: "android-10-arm64-rel"
+  triggers: "android-10-x86-fyi-rel"
   triggers: "android-11-x86-rel"
   triggers: "android-12-x64-rel"
   triggers: "android-12l-x64-fyi-dbg"
diff --git a/infra/config/generators/cq-builders-md.star b/infra/config/generators/cq-builders-md.star
index 0c90cf5..c61e63a 100644
--- a/infra/config/generators/cq-builders-md.star
+++ b/infra/config/generators/cq-builders-md.star
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-load("//lib/try.star", "location_filters_without_defaults", "try_")
+load("//lib/try.star", "location_filters_without_defaults", "owner_whitelist_group_without_defaults", "try_")
 load("//outages/config.star", outages_config = "config")
 
 _MD_HEADER = """\
@@ -76,6 +76,7 @@
 
 def _normalize_builder(builder):
     location_filters = location_filters_without_defaults(builder)
+    owner_whitelist_group = owner_whitelist_group_without_defaults(builder)
 
     return struct(
         name = builder.name,
@@ -84,6 +85,7 @@
         location_filters = location_filters,
         mode_allowlist = builder.mode_allowlist,
         equivalent_to = proto.clone(builder).equivalent_to,
+        owner_whitelist_group = owner_whitelist_group,
     )
 
 def _group_builders_by_section(builders):
@@ -217,6 +219,12 @@
                         title = details.title,
                         url = details.url,
                     ))
+            if b.owner_whitelist_group:
+                lines.append("")
+                lines.append("  This builder is only run when the CL owner is in the group:")
+                for g in b.owner_whitelist_group:
+                    lines.append("  * `[{group}](https://chrome-infra-auth.appspot.com/auth/lookup?p={group})`".format(group = g))
+
             if getattr(b, "equivalent_to") and b.equivalent_to.name:
                 eq_project, eq_bucket, eq_name = b.equivalent_to.name.split("/")
                 eq_builder_url = _TRY_BUILDER_VIEW_URL.format(
diff --git a/infra/config/generators/cq-usage.star b/infra/config/generators/cq-usage.star
index 80c2cfc6..3adf7b3 100644
--- a/infra/config/generators/cq-usage.star
+++ b/infra/config/generators/cq-usage.star
@@ -11,13 +11,13 @@
 """
 
 load("@stdlib//internal/luci/proto.star", "cq_pb")
-load("//lib/try.star", "location_filters_without_defaults")
+load("//lib/try.star", "location_filters_without_defaults", "owner_whitelist_group_without_defaults")
 load("//subprojects/chromium/fallback-cq.star", "fallback_cq")
 
 def _remove_none(l):
     return [e for e in l if e != None]
 
-def _trim_builder(builder, include_path_based):
+def _trim_builder(builder, include_path_and_whitelist_based):
     if builder.includable_only or builder.experiment_percentage:
         return None
 
@@ -26,35 +26,42 @@
 
     # The majority of CQ builders will exclude something because we don't
     # trigger most builders on changes to non-code directories (e.g. docs), so
-    # only consider them path-based if they have an include filter
-    if not include_path_based and location_filters_without_defaults(builder):
+    # only consider them path-based if they have an include filter. Likewise,
+    # only consider them owner-whitelist-group-based if they have an
+    # owner whitelist group outside of the default groups.
+    if not include_path_and_whitelist_based and (
+        location_filters_without_defaults(builder) or
+        owner_whitelist_group_without_defaults(builder)
+    ):
         return None
+
     trimmed = cq_pb.Verifiers.Tryjob.Builder(
         name = builder.name,
         disable_reuse = builder.disable_reuse,
     )
-    if include_path_based:
+    if include_path_and_whitelist_based:
         trimmed.location_filters = builder.location_filters
+        trimmed.owner_whitelist_group = builder.owner_whitelist_group
     return trimmed
 
-def _trim_tryjob(tryjob, include_path_based):
-    builders = _remove_none([_trim_builder(b, include_path_based) for b in tryjob.builders])
+def _trim_tryjob(tryjob, include_path_and_whitelist_based):
+    builders = _remove_none([_trim_builder(b, include_path_and_whitelist_based) for b in tryjob.builders])
     if not builders:
         return None
     tryjob = proto.clone(tryjob)
     tryjob.builders = builders
     return tryjob
 
-def _trim_verifiers(verifiers, include_path_based):
+def _trim_verifiers(verifiers, include_path_and_whitelist_based):
     if not proto.has(verifiers, "tryjob"):
         return None
-    tryjob = _trim_tryjob(verifiers.tryjob, include_path_based)
+    tryjob = _trim_tryjob(verifiers.tryjob, include_path_and_whitelist_based)
     if not tryjob:
         return None
     return cq_pb.Verifiers(tryjob = tryjob)
 
-def _trim_config_group(config_group, include_path_based):
-    verifiers = _trim_verifiers(config_group.verifiers, include_path_based)
+def _trim_config_group(config_group, include_path_and_whitelist_based):
+    verifiers = _trim_verifiers(config_group.verifiers, include_path_and_whitelist_based)
     if not verifiers:
         return None
     return cq_pb.ConfigGroup(
@@ -67,14 +74,14 @@
     cfg = ctx.output["luci/commit-queue.cfg"]
     ctx.output["cq-usage/default.cfg"] = cq_pb.Config(config_groups = _remove_none(
         [
-            _trim_config_group(g, include_path_based = False)
+            _trim_config_group(g, include_path_and_whitelist_based = False)
             for g in cfg.config_groups
             if g.name != fallback_cq.GROUP
         ],
     ))
     ctx.output["cq-usage/full.cfg"] = cq_pb.Config(config_groups = _remove_none(
         [
-            _trim_config_group(g, include_path_based = True)
+            _trim_config_group(g, include_path_and_whitelist_based = True)
             for g in cfg.config_groups
             if g.name != fallback_cq.GROUP
         ],
diff --git a/infra/config/lib/try.star b/infra/config/lib/try.star
index 7dc9a15d..830f110 100644
--- a/infra/config/lib/try.star
+++ b/infra/config/lib/try.star
@@ -71,6 +71,11 @@
 
     return filters
 
+def default_owner_whitelist_group_for_cq_bots(project):
+    if project.startswith("chrome"):
+        return ["googlers", "project-chromium-robot-committers"]
+    return []
+
 def location_filters_without_defaults(tryjob_builder_proto):
     default_filters = default_location_filters(tryjob_builder_proto.name)
     return [f for f in tryjob_builder_proto.location_filters if cq.location_filter(
@@ -80,6 +85,15 @@
         exclude = f.exclude,
     ) not in default_filters]
 
+def owner_whitelist_group_without_defaults(tryjob_builder_proto):
+    project = tryjob_builder_proto.name.split("/")[0]
+    default_group = default_owner_whitelist_group_for_cq_bots(project)
+    return [
+        g
+        for g in tryjob_builder_proto.owner_whitelist_group
+        if g not in default_group
+    ]
+
 # Intended to be used for the `caches` builder arg when no source checkout is
 # required.
 #
diff --git a/infra/config/subprojects/chrome/try.star b/infra/config/subprojects/chrome/try.star
index fc9cb648..8a75108 100644
--- a/infra/config/subprojects/chrome/try.star
+++ b/infra/config/subprojects/chrome/try.star
@@ -8,7 +8,7 @@
 # http://go/chromium-cq#internal-builders-on-the-cq.
 
 load("//lib/branches.star", "branches")
-load("//lib/try.star", "default_location_filters", "try_")
+load("//lib/try.star", "default_location_filters", "default_owner_whitelist_group_for_cq_bots", "try_")
 load("//project.star", "settings")
 
 def chrome_internal_verifier(
@@ -44,10 +44,7 @@
             builder = "{}:try/{}".format(settings.chrome_project, builder),
             cq_group = "cq",
             includable_only = True,
-            owner_whitelist = [
-                "googlers",
-                "project-chromium-robot-committers",
-            ],
+            owner_whitelist = default_owner_whitelist_group_for_cq_bots(settings.chrome_project),
             result_visibility = cq.COMMENT_LEVEL_RESTRICTED,
             **kwargs
         )
@@ -311,8 +308,6 @@
         "optimization-guide-try-opt-in",
     ],
     tryjob = try_.job(
-        # TODO: crbug.com/375065753 - Promote out of experimental once stable.
-        experiment_percentage = 100,
         location_filters = [
             "chrome/browser/ai/.+",
             "components/optimization_guide/.+",
@@ -327,8 +322,6 @@
         "optimization-guide-try-opt-in",
     ],
     tryjob = try_.job(
-        # TODO: crbug.com/375065753 - Promote out of experimental once stable.
-        experiment_percentage = 100,
         location_filters = [
             "chrome/browser/ai/.+",
             "components/optimization_guide/.+",
@@ -351,8 +344,6 @@
         "optimization-guide-try-opt-in",
     ],
     tryjob = try_.job(
-        # TODO: crbug.com/375065753 - Promote out of experimental once stable.
-        experiment_percentage = 100,
         location_filters = [
             "chrome/browser/ai/.+",
             "components/optimization_guide/.+",
diff --git a/infra/config/subprojects/chromium/ci/chromium.android.fyi.star b/infra/config/subprojects/chromium/ci/chromium.android.fyi.star
index d42c4e6..af3988c 100644
--- a/infra/config/subprojects/chromium/ci/chromium.android.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.android.fyi.star
@@ -457,6 +457,112 @@
     ),
 )
 
+ci.builder(
+    name = "android-10-x86-fyi-rel",
+    description_html = "Run chromium tests on Android 10 emulators.",
+    builder_spec = builder_config.builder_spec(
+        gclient_config = builder_config.gclient_config(
+            config = "chromium",
+            apply_configs = ["android"],
+        ),
+        chromium_config = builder_config.chromium_config(
+            config = "android",
+            build_config = builder_config.build_config.RELEASE,
+            target_bits = 32,
+            target_platform = builder_config.target_platform.ANDROID,
+        ),
+        android_config = builder_config.android_config(config = "x86_builder_mb"),
+        build_gs_bucket = "chromium-android-archive",
+    ),
+    gn_args = gn_args.config(
+        configs = [
+            "android_builder",
+            "release_builder",
+            "remoteexec",
+            "minimal_symbols",
+            "x86",
+            "strip_debug_info",
+            "android_fastbuild",
+            "webview_monochrome",
+            "webview_shell",
+        ],
+    ),
+    targets = targets.bundle(
+        targets = [
+            targets.bundle(
+                targets = [
+                    "android_10_emulator_fyi_gtests",
+                    "q_isolated_scripts",
+                ],
+                mixins = targets.mixin(
+                    args = [
+                        "--use-persistent-shell",
+                    ],
+                ),
+            ),
+            "chromium_android_scripts",
+        ],
+        additional_compile_targets = [
+            "chrome_nocompile_tests",
+        ],
+        mixins = [
+            "has_native_resultdb_integration",
+            "isolate_profile_data",
+            "10-x86-emulator",
+            "emulator-4-cores",
+            "linux-jammy",
+            "x86-64",
+        ],
+        per_test_modifications = {
+            "android_browsertests": targets.mixin(
+                swarming = targets.swarming(
+                    shards = 9,
+                ),
+            ),
+            "android_sync_integration_tests": targets.mixin(
+                swarming = targets.swarming(
+                    shards = 2,
+                ),
+            ),
+            "chrome_public_test_apk": targets.mixin(
+                swarming = targets.swarming(
+                    dimensions = {
+                        # use 8-core to shorten runtime
+                        "cores": "8",
+                    },
+                ),
+            ),
+            "components_browsertests": targets.mixin(
+                swarming = targets.swarming(
+                    shards = 4,
+                ),
+            ),
+            "content_shell_test_apk": targets.mixin(
+                swarming = targets.swarming(
+                    dimensions = {
+                        # use 8-core to shorten runtime
+                        "cores": "8",
+                    },
+                    shards = 6,
+                ),
+            ),
+            "services_unittests": targets.mixin(
+                swarming = targets.swarming(
+                    shards = 3,
+                ),
+            ),
+        },
+    ),
+    targets_settings = targets.settings(
+        os_type = targets.os_type.ANDROID,
+    ),
+    console_view_entry = consoles.console_view_entry(
+        category = "emulator|x86|rel",
+        short_name = "10",
+    ),
+    contact_team_email = "clank-engprod@google.com",
+)
+
 # TODO(crbug.com/40152686): This and android-12-x64-fyi-rel
 # are being kept around so that build links in the related
 # bugs are accessible
diff --git a/infra/config/subprojects/chromium/try/tryserver.chromium.android.star b/infra/config/subprojects/chromium/try/tryserver.chromium.android.star
index a88b161..88663e7 100644
--- a/infra/config/subprojects/chromium/try/tryserver.chromium.android.star
+++ b/infra/config/subprojects/chromium/try/tryserver.chromium.android.star
@@ -57,6 +57,21 @@
 )
 
 try_.builder(
+    name = "android-10-x86-fyi-rel",
+    mirrors = [
+        "ci/android-10-x86-fyi-rel",
+    ],
+    gn_args = gn_args.config(
+        configs = [
+            "ci/android-10-x86-fyi-rel",
+            "release_try_builder",
+        ],
+    ),
+    contact_team_email = "clank-engprod@google.com",
+    siso_remote_jobs = siso.remote_jobs.LOW_JOBS_FOR_CQ,
+)
+
+try_.builder(
     name = "android-11-x86-rel",
     mirrors = [
         "ci/android-11-x86-rel",
diff --git a/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star b/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star
index a7fdfc8..7c19778 100644
--- a/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star
+++ b/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star
@@ -557,7 +557,6 @@
     builderless = True,
     cpu = cpu.ARM64,
     execution_timeout = 4 * time.hour,
-    xcode = xcode.x15betabots,
 )
 
 ios_builder(
diff --git a/infra/config/targets/autoshard_exceptions.json b/infra/config/targets/autoshard_exceptions.json
index faa51bd..8145dd8 100644
--- a/infra/config/targets/autoshard_exceptions.json
+++ b/infra/config/targets/autoshard_exceptions.json
@@ -95,18 +95,18 @@
         "android-15-x64-rel": {
             "android_browsertests": {
                 "debug": {
-                    "avg_num_builds_per_peak_hour": 41,
-                    "estimated_bot_hour_delta": 0.3,
-                    "prev_avg_pending_time_sec": 21.6,
+                    "avg_num_builds_per_peak_hour": 69,
+                    "estimated_bot_hour_delta": -35.42,
+                    "prev_avg_pending_time_sec": 12.3,
                     "prev_p50_pending_time_sec": 1.0,
-                    "prev_p90_pending_time_sec": 68.0,
-                    "prev_percentile_duration_minutes": 17.04,
-                    "prev_shard_count": 12,
-                    "simulated_max_shard_duration": 14.64,
-                    "test_overhead_min": 0.21666666666666667,
+                    "prev_p90_pending_time_sec": 29.0,
+                    "prev_percentile_duration_minutes": 12.41,
+                    "prev_shard_count": 14,
+                    "simulated_max_shard_duration": 14.29,
+                    "test_overhead_min": 7.7,
                     "try_builder": "android-x64-rel"
                 },
-                "shards": 14
+                "shards": 10
             },
             "components_browsertests": {
                 "debug": {
diff --git a/infra/config/targets/bundles.star b/infra/config/targets/bundles.star
index 396db454..3933b1e 100644
--- a/infra/config/targets/bundles.star
+++ b/infra/config/targets/bundles.star
@@ -18,6 +18,28 @@
 )
 
 targets.bundle(
+    name = "android_10_emulator_fyi_gtests",
+    targets = [
+        "android_emulator_specific_chrome_public_tests",
+        "android_trichrome_smoke_tests",
+        "android_smoke_tests",
+        "android_specific_chromium_gtests",  # Already includes gl_gtests.
+        "chromium_gtests",
+        "chromium_gtests_for_devices_with_graphical_output",
+        "linux_flavor_specific_chromium_gtests",
+        "system_webview_shell_instrumentation_tests",  # Not an experimental test
+        targets.bundle(
+            targets = "webview_trichrome_cts_tests_suite",
+            variants = [
+                "WEBVIEW_TRICHROME_FULL_CTS_TESTS",
+                "WEBVIEW_TRICHROME_INSTANT_CTS_TESTS",
+            ],
+        ),
+        "webview_ui_instrumentation_tests",
+    ],
+)
+
+targets.bundle(
     name = "android_11_emulator_gtests",
     targets = [
         "android_emulator_specific_chrome_public_tests",
@@ -5665,6 +5687,18 @@
 )
 
 targets.bundle(
+    name = "q_isolated_scripts",
+    targets = [
+        "android_isolated_scripts",
+        "chromium_junit_tests_scripts",
+        "components_perftests_isolated_scripts",
+        "monochrome_public_apk_checker_isolated_script",
+        "telemetry_android_minidump_unittests_isolated_scripts",
+        "telemetry_perf_unittests_isolated_scripts_android",
+    ],
+)
+
+targets.bundle(
     name = "pixel_browser_tests_gtests",
     targets = [
         "pixel_browser_tests",
diff --git a/ios/chrome/browser/autocomplete/model/remote_suggestions_service_factory.mm b/ios/chrome/browser/autocomplete/model/remote_suggestions_service_factory.mm
index 1121d7a..48986955 100644
--- a/ios/chrome/browser/autocomplete/model/remote_suggestions_service_factory.mm
+++ b/ios/chrome/browser/autocomplete/model/remote_suggestions_service_factory.mm
@@ -30,6 +30,7 @@
   ProfileIOS* profile = ProfileIOS::FromBrowserState(context);
   return std::make_unique<RemoteSuggestionsService>(
       /*document_suggestions_service=*/nullptr,
+      /*search_aggregator_suggestions_service=*/nullptr,
       profile->GetSharedURLLoaderFactory());
 }
 
diff --git a/ios/chrome/browser/settings/ui_bundled/password/password_checkup/password_checkup_egtest.mm b/ios/chrome/browser/settings/ui_bundled/password/password_checkup/password_checkup_egtest.mm
index e60e789..4aec14b 100644
--- a/ios/chrome/browser/settings/ui_bundled/password/password_checkup/password_checkup_egtest.mm
+++ b/ios/chrome/browser/settings/ui_bundled/password/password_checkup/password_checkup_egtest.mm
@@ -612,6 +612,10 @@
 
 // Tests restoring a muted compromised password warning.
 - (void)testPasswordCheckupRestoreCompromisedPasswordWarning {
+  // TODO(crbug.com/382251787): Test fails when run on iOS 18.2.
+  if (@available(iOS 18.2, *)) {
+    EARL_GREY_TEST_DISABLED(@"Fails on iOS 18.2");
+  }
   SaveMutedCompromisedPasswordFormToProfileStore();
 
   OpenPasswordCheckupHomepage(
diff --git a/ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_mediator_unittest.mm b/ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_mediator_unittest.mm
index 7a6184d..52df846 100644
--- a/ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_mediator_unittest.mm
+++ b/ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_mediator_unittest.mm
@@ -4,6 +4,7 @@
 
 #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_mediator.h"
 
+#import "base/rand_util.h"
 #import "base/run_loop.h"
 #import "base/test/scoped_feature_list.h"
 #import "base/test/task_environment.h"
diff --git a/ios/chrome/browser/settings/ui_bundled/password/password_settings_app_interface.mm b/ios/chrome/browser/settings/ui_bundled/password/password_settings_app_interface.mm
index c477deb..0f8a606 100644
--- a/ios/chrome/browser/settings/ui_bundled/password/password_settings_app_interface.mm
+++ b/ios/chrome/browser/settings/ui_bundled/password/password_settings_app_interface.mm
@@ -8,6 +8,7 @@
 
 #import "base/apple/foundation_util.h"
 #import "base/location.h"
+#import "base/rand_util.h"
 #import "base/strings/stringprintf.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/strings/utf_string_conversions.h"
diff --git a/ios_internal b/ios_internal
index 7c7a0077..53efd84 160000
--- a/ios_internal
+++ b/ios_internal
@@ -1 +1 @@
-Subproject commit 7c7a00774ea89513f74dcb2f89bf976d889ffb87
+Subproject commit 53efd8418bb70bf06616f417a7090f9d3b659343
diff --git a/media/base/android/java/src/org/chromium/media/VideoAcceleratorUtil.java b/media/base/android/java/src/org/chromium/media/VideoAcceleratorUtil.java
index 7e7f4ec..b6c7702 100644
--- a/media/base/android/java/src/org/chromium/media/VideoAcceleratorUtil.java
+++ b/media/base/android/java/src/org/chromium/media/VideoAcceleratorUtil.java
@@ -232,6 +232,10 @@
         return 1;
     }
 
+    private static int alignUp(int size, int alignment) {
+        return (size + alignment - 1) & ~(alignment - 1);
+    }
+
     /**
      * Returns an array of SupportedProfileAdapter entries since the NDK doesn't provide this
      * functionality :/
@@ -298,10 +302,17 @@
                         new Resolution(supportedWidths.getUpper(), supportedHeights.getUpper()));
                 LinkedHashMap<Integer, Resolution> frameRateResolutionMap =
                         new LinkedHashMap<Integer, Resolution>();
+
+                // Retrieve the alignment of the current codec.
+                int widthAlignment = videoCapabilities.getWidthAlignment();
+                int heightAlignment = videoCapabilities.getHeightAlignment();
                 // Compute the final supported resolution and framerate combinations.
                 for (Resolution supportedResolution : supportedResolutions) {
-                    int supportedWidth = supportedResolution.getWidth();
-                    int supportedHeight = supportedResolution.getHeight();
+                    // Adjust the width and height here based on the retrieved alignment. Otherwise,
+                    // if a width or height doesn't match the alignment, the function
+                    // `isSizeSupported()` below will return false.
+                    int supportedWidth = alignUp(supportedResolution.getWidth(), widthAlignment);
+                    int supportedHeight = alignUp(supportedResolution.getHeight(), heightAlignment);
                     if (!videoCapabilities.isSizeSupported(supportedWidth, supportedHeight)) {
                         continue;
                     }
@@ -313,7 +324,9 @@
                     int supportedFrameRate =
                             (int) Math.floor(supportedFrameRates.getUpper().doubleValue());
                     if (!frameRateResolutionMap.containsKey(supportedFrameRate)) {
-                        frameRateResolutionMap.put(supportedFrameRate, supportedResolution);
+                        frameRateResolutionMap.put(
+                                supportedFrameRate,
+                                new Resolution(supportedWidth, supportedHeight));
                     } else {
                         Resolution resolution = frameRateResolutionMap.get(supportedFrameRate);
                         // If the framerates of the two are the same, always use the higher
@@ -321,7 +334,9 @@
                         // useless result to the list.
                         if (supportedWidth >= resolution.getWidth()
                                 && supportedHeight >= resolution.getHeight()) {
-                            frameRateResolutionMap.put(supportedFrameRate, supportedResolution);
+                            frameRateResolutionMap.put(
+                                    supportedFrameRate,
+                                    new Resolution(supportedWidth, supportedHeight));
                         }
                     }
                 }
diff --git a/media/ffmpeg/scripts/robo_branch.py b/media/ffmpeg/scripts/robo_branch.py
index 84e548a..2b5ce3a8d 100755
--- a/media/ffmpeg/scripts/robo_branch.py
+++ b/media/ffmpeg/scripts/robo_branch.py
@@ -82,7 +82,7 @@
          "*autorename*"]) != ""
 
 
-def CreateAndCheckoutDatedSushiBranch(cfg):
+def CreateAndCheckoutDatedSushiBranchIfNeeded(cfg):
     """Create a dated branch from upstream/HEAD if we're not already on one."""
     if cfg.sushi_branch_name():
         shell.log(f"Already on sushi branch {cfg.sushi_branch_name()}")
diff --git a/media/ffmpeg/scripts/robosushi.py b/media/ffmpeg/scripts/robosushi.py
index 7c2dc315..2758b38 100755
--- a/media/ffmpeg/scripts/robosushi.py
+++ b/media/ffmpeg/scripts/robosushi.py
@@ -162,7 +162,7 @@
                     func=robo_build.ObliterateOldBuildOutputIfNeeded),
               Target(name="create_sushi_branch",
                      desc="Create a sushi-MDY branch if we're not on one",
-                     func=robo_branch.CreateAndCheckoutDatedSushiBranch),
+                     func=robo_branch.CreateAndCheckoutDatedSushiBranchIfNeeded),
               Target(name="merge_from_upstream",
                      desc="Merge upstream/master to our local sushi-MDY branch",
                      func=robo_branch.MergeUpstreamToSushiBranchIfNeeded),
diff --git a/media/gpu/h265_decoder.cc b/media/gpu/h265_decoder.cc
index f02cd489..87ca55a 100644
--- a/media/gpu/h265_decoder.cc
+++ b/media/gpu/h265_decoder.cc
@@ -53,7 +53,7 @@
     // Spec A.3.5
     case HEVCPROFILE_REXT:
       return bit_depth == 8u || bit_depth == 10u || bit_depth == 12u ||
-             bit_depth == 14u || bit_depth == 16u;
+             bit_depth == 16u;
     // Spec A.3.6
     case HEVCPROFILE_HIGH_THROUGHPUT:
       return bit_depth == 8u || bit_depth == 10u || bit_depth == 14u ||
diff --git a/media/test/data/README.md b/media/test/data/README.md
index 283db3b..23db0de 100644
--- a/media/test/data/README.md
+++ b/media/test/data/README.md
@@ -1380,6 +1380,11 @@
 #### blank-1x1.jpg
 1x1 small picture to test special cases.
 
+### PNG Test Files
+
+#### quick-brown-fox.png
+A picture with a resolution of `1280x720` has the words "The quick brown fox jumps over the lazy dog" in colorful text repeated many times on the left side, and on the right side, colorful vertical stripes are repeated many times. The image was created using Photoshop.
+
 ### MP4 files with non-square pixels.
 
 #### bear-640x360-non_square_pixel-with_pasp.mp4
@@ -1499,66 +1504,6 @@
 ffmpeg -i bear-1280x720-hevc-10bit.mp4 -vcodec copy -an bear-1280x720-hevc-10bit-no-audio.mp4
 ```
 
-#### bear-1280x720-hevc-8bit-422.mp4
-HEVC video stream with 8-bit 422 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720.mp4 -vcodec hevc -pix_fmt yuv422p bear-1280x720-hevc-8bit-422.mp4
-```
-
-#### bear-1280x720-hevc-8bit-422-no-audio.mp4
-HEVC video stream with 8-bit 422 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720-hevc-8bit-422.mp4 -vcodec copy -an bear-1280x720-hevc-8bit-422-no-audio.mp4
-```
-
-#### bear-1280x720-hevc-8bit-444-no-audio.mp4
-HEVC video stream with 8-bit 444 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720.mp4 -vcodec hevc -an -pix_fmt yuv444p bear-1280x720-hevc-8bit-444-no-audio.mp4
-```
-
-#### bear-1280x720-hevc-10bit-422.mp4
-HEVC video stream with 10-bit 422 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720.mp4 -vcodec libx265 -pix_fmt yuv422p10le bear-1280x720-hevc-10bit-422.mp4
-```
-
-#### bear-1280x720-hevc-10bit-422-no-audio.mp4
-HEVC video stream with 10-bit 422 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720-hevc-10bit-422.mp4 -vcodec copy -an bear-1280x720-hevc-10bit-422-no-audio.mp4
-```
-
-#### bear-1280x720-hevc-10bit-444.mp4
-HEVC video stream with 10-bit 444 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720.mp4 -vcodec libx265 -pix_fmt yuv444p10le bear-1280x720-hevc-10bit-444.mp4
-```
-
-#### bear-1280x720-hevc-10bit-444-no-audio.mp4
-HEVC video stream with 10-bit 444 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720-hevc-10bit-444.mp4 -vcodec copy -an bear-1280x720-hevc-10bit-444-no-audio.mp4
-```
-
-#### bear-1280x720-hevc-12bit-420.mp4
-HEVC video stream with 12-bit 420 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720.mp4 -vcodec libx265 -pix_fmt yuv420p12le bear-1280x720-hevc-12bit-420.mp4
-```
-
-#### bear-1280x720-hevc-12bit-422.mp4
-HEVC video stream with 12-bit 422 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720.mp4 -vcodec libx265 -pix_fmt yuv422p12le bear-1280x720-hevc-12bit-422.mp4
-```
-
-#### bear-1280x720-hevc-12bit-444.mp4
-HEVC video stream with 12-bit 444 range extension profile, generated with
-```
-ffmpeg -i bear-1280x720.mp4 -vcodec libx265 -pix_fmt yuv444p12le bear-1280x720-hevc-12bit-444.mp4
-```
-
 #### bear-1280x720-hevc-10bit-hdr10.mp4
 HEVC video stream with HDR10 metadata included, generated with
 ````
@@ -1571,6 +1516,78 @@
 ffmpeg -i bear-1280x720.mp4 -vf "scale=3840:2160,setpts=4*PTS" -c:v libx265 -crf 28 -c:a copy bear-3840x2160-hevc.mp4
 ```
 
+#### quick-brown-fox-1280x720-hevc-rext-8bit-400-no-audio.mp4
+HEVC video stream with 8-bit 400 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt gray -vcodec libx265 -x265-params range=full:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-8bit-400-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-8bit-420-no-audio.mp4
+HEVC video stream with 8-bit 420 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv420p -profile:v main-intra -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-8bit-420-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4
+HEVC video stream with 8-bit 422 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv422p -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4
+HEVC video stream with 8-bit 444 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv444p -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-10bit-400-no-audio.mp4
+HEVC video stream with 10-bit 400 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt gray10le -vcodec libx265 -x265-params range=full:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-10bit-400-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-10bit-420-no-audio.mp4
+HEVC video stream with 10-bit 420 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv420p10le -profile:v main10-intra -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-10bit-420-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4
+HEVC video stream with 10-bit 422 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv422p10le -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4
+HEVC video stream with 10-bit 444 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv444p10le -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-12bit-400-no-audio.mp4
+HEVC video stream with 12-bit 400 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt gray12le -vcodec libx265 -x265-params range=full:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-12bit-400-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-12bit-420-no-audio.mp4
+HEVC video stream with 12-bit 420 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv420p12le -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-12bit-420-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-12bit-422-no-audio.mp4
+HEVC video stream with 12-bit 422 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv422p12le -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-12bit-422-no-audio.mp4
+```
+
+#### quick-brown-fox-1280x720-hevc-rext-12bit-444-no-audio.mp4
+HEVC video stream with 12-bit 444 range extension profile generated from `quick-brown-fox.png`, generated with
+```
+ffmpeg -i quick-brown-fox.png -pix_fmt yuv444p12le -vcodec libx265 -x265-params range=limited:colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709 -r 1 -t 1 -vtag hvc1 quick-brown-fox-1280x720-hevc-rext-12bit-444-no-audio.mp4
+```
+
 ### MP4 file with Dolby Vision
 
 #### glass-blowing2-dolby-vision-profile-5-frag.mp4
diff --git a/media/test/data/bear-1280x720-hevc-10bit-422-no-audio.mp4 b/media/test/data/bear-1280x720-hevc-10bit-422-no-audio.mp4
deleted file mode 100644
index 1134532..0000000
--- a/media/test/data/bear-1280x720-hevc-10bit-422-no-audio.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-10bit-422.mp4 b/media/test/data/bear-1280x720-hevc-10bit-422.mp4
deleted file mode 100644
index 4c198dd..0000000
--- a/media/test/data/bear-1280x720-hevc-10bit-422.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-10bit-444-no-audio.mp4 b/media/test/data/bear-1280x720-hevc-10bit-444-no-audio.mp4
deleted file mode 100644
index 54f51598..0000000
--- a/media/test/data/bear-1280x720-hevc-10bit-444-no-audio.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-10bit-444.mp4 b/media/test/data/bear-1280x720-hevc-10bit-444.mp4
deleted file mode 100644
index 0af30a2..0000000
--- a/media/test/data/bear-1280x720-hevc-10bit-444.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-12bit-420.mp4 b/media/test/data/bear-1280x720-hevc-12bit-420.mp4
deleted file mode 100644
index baec739..0000000
--- a/media/test/data/bear-1280x720-hevc-12bit-420.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-12bit-422.mp4 b/media/test/data/bear-1280x720-hevc-12bit-422.mp4
deleted file mode 100644
index 4334d032..0000000
--- a/media/test/data/bear-1280x720-hevc-12bit-422.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-12bit-444.mp4 b/media/test/data/bear-1280x720-hevc-12bit-444.mp4
deleted file mode 100644
index bd25fa2..0000000
--- a/media/test/data/bear-1280x720-hevc-12bit-444.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-8bit-422-no-audio.mp4 b/media/test/data/bear-1280x720-hevc-8bit-422-no-audio.mp4
deleted file mode 100644
index cd956478..0000000
--- a/media/test/data/bear-1280x720-hevc-8bit-422-no-audio.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-8bit-422.mp4 b/media/test/data/bear-1280x720-hevc-8bit-422.mp4
deleted file mode 100644
index cd412b5e..0000000
--- a/media/test/data/bear-1280x720-hevc-8bit-422.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/bear-1280x720-hevc-8bit-444-no-audio.mp4 b/media/test/data/bear-1280x720-hevc-8bit-444-no-audio.mp4
deleted file mode 100644
index a0b9a0e..0000000
--- a/media/test/data/bear-1280x720-hevc-8bit-444-no-audio.mp4
+++ /dev/null
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-400-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-400-no-audio.mp4
new file mode 100644
index 0000000..34ef689
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-400-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-420-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-420-no-audio.mp4
new file mode 100644
index 0000000..8ee9dee
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-420-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4
new file mode 100644
index 0000000..5d338fd
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4
new file mode 100644
index 0000000..f88cc55
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-400-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-400-no-audio.mp4
new file mode 100644
index 0000000..8ea3ab3
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-400-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-420-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-420-no-audio.mp4
new file mode 100644
index 0000000..0ff9fd5a
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-420-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-422-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-422-no-audio.mp4
new file mode 100644
index 0000000..aa55c5dd
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-422-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-444-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-444-no-audio.mp4
new file mode 100644
index 0000000..bc059f4
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-444-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-400-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-400-no-audio.mp4
new file mode 100644
index 0000000..1ce8916
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-400-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-420-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-420-no-audio.mp4
new file mode 100644
index 0000000..b58bb5d
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-420-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4
new file mode 100644
index 0000000..5ddb5d0
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4 b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4
new file mode 100644
index 0000000..9cd149c
--- /dev/null
+++ b/media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4
Binary files differ
diff --git a/media/test/data/quick-brown-fox.png b/media/test/data/quick-brown-fox.png
new file mode 100644
index 0000000..c54293e
--- /dev/null
+++ b/media/test/data/quick-brown-fox.png
Binary files differ
diff --git a/media/test/media_bundle_data.filelist b/media/test/media_bundle_data.filelist
index f00003da..0d9559aa 100644
--- a/media/test/media_bundle_data.filelist
+++ b/media/test/media_bundle_data.filelist
@@ -68,20 +68,10 @@
 data/bear-1280x720-av_frag.mp4
 data/bear-1280x720-av_with-aud-nalus_frag.mp4
 data/bear-1280x720-avt_subt_frag.mp4
-data/bear-1280x720-hevc-10bit-422-no-audio.mp4
-data/bear-1280x720-hevc-10bit-422.mp4
-data/bear-1280x720-hevc-10bit-444-no-audio.mp4
-data/bear-1280x720-hevc-10bit-444.mp4
 data/bear-1280x720-hevc-10bit-hdr10.hevc
 data/bear-1280x720-hevc-10bit-hdr10.mp4
 data/bear-1280x720-hevc-10bit-no-audio.mp4
 data/bear-1280x720-hevc-10bit.mp4
-data/bear-1280x720-hevc-12bit-420.mp4
-data/bear-1280x720-hevc-12bit-422.mp4
-data/bear-1280x720-hevc-12bit-444.mp4
-data/bear-1280x720-hevc-8bit-422-no-audio.mp4
-data/bear-1280x720-hevc-8bit-422.mp4
-data/bear-1280x720-hevc-8bit-444-no-audio.mp4
 data/bear-1280x720-hevc-no-audio.mp4
 data/bear-1280x720-hevc.mp4
 data/bear-1280x720-hls-clear-mpl.m3u8
@@ -437,6 +427,19 @@
 data/puppets-480x270.nv12.yuv.json
 data/puppets-640x360.nv12.yuv.json
 data/puppets-640x360_in_640x480.nv12.yuv.json
+data/quick-brown-fox-1280x720-hevc-rext-8bit-400-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-8bit-420-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-10bit-400-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-10bit-420-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-12bit-400-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-12bit-420-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-12bit-422-no-audio.mp4
+data/quick-brown-fox-1280x720-hevc-rext-12bit-444-no-audio.mp4
+data/quick-brown-fox.png
 data/rapid_video_change_test.html
 data/red-a500hz.webm
 data/red-green.h264
diff --git a/media/unit_tests_bundle_data.filelist b/media/unit_tests_bundle_data.filelist
index 8b71532..272b027 100644
--- a/media/unit_tests_bundle_data.filelist
+++ b/media/unit_tests_bundle_data.filelist
@@ -80,20 +80,10 @@
 //media/test/data/bear-1280x720-av_frag.mp4
 //media/test/data/bear-1280x720-av_with-aud-nalus_frag.mp4
 //media/test/data/bear-1280x720-avt_subt_frag.mp4
-//media/test/data/bear-1280x720-hevc-10bit-422-no-audio.mp4
-//media/test/data/bear-1280x720-hevc-10bit-422.mp4
-//media/test/data/bear-1280x720-hevc-10bit-444-no-audio.mp4
-//media/test/data/bear-1280x720-hevc-10bit-444.mp4
 //media/test/data/bear-1280x720-hevc-10bit-hdr10.hevc
 //media/test/data/bear-1280x720-hevc-10bit-hdr10.mp4
 //media/test/data/bear-1280x720-hevc-10bit-no-audio.mp4
 //media/test/data/bear-1280x720-hevc-10bit.mp4
-//media/test/data/bear-1280x720-hevc-12bit-420.mp4
-//media/test/data/bear-1280x720-hevc-12bit-422.mp4
-//media/test/data/bear-1280x720-hevc-12bit-444.mp4
-//media/test/data/bear-1280x720-hevc-8bit-422-no-audio.mp4
-//media/test/data/bear-1280x720-hevc-8bit-422.mp4
-//media/test/data/bear-1280x720-hevc-8bit-444-no-audio.mp4
 //media/test/data/bear-1280x720-hevc-no-audio.mp4
 //media/test/data/bear-1280x720-hevc.mp4
 //media/test/data/bear-1280x720-hls-clear-mpl.m3u8
@@ -449,6 +439,19 @@
 //media/test/data/puppets-480x270.nv12.yuv.json
 //media/test/data/puppets-640x360.nv12.yuv.json
 //media/test/data/puppets-640x360_in_640x480.nv12.yuv.json
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-400-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-420-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-422-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-8bit-444-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-400-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-420-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-422-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-10bit-444-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-400-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-420-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-422-no-audio.mp4
+//media/test/data/quick-brown-fox-1280x720-hevc-rext-12bit-444-no-audio.mp4
+//media/test/data/quick-brown-fox.png
 //media/test/data/rapid_video_change_test.html
 //media/test/data/red-a500hz.webm
 //media/test/data/red-green.h264
diff --git a/net/cookies/canonical_cookie.cc b/net/cookies/canonical_cookie.cc
index 7bcdf41..0a4b8e1 100644
--- a/net/cookies/canonical_cookie.cc
+++ b/net/cookies/canonical_cookie.cc
@@ -83,16 +83,6 @@
 static constexpr int kMinutesInTwelveHours = 12 * 60;
 static constexpr int kMinutesInTwentyFourHours = 24 * 60;
 
-// Determine the cookie domain to use for setting the specified cookie.
-std::optional<std::string> GetCookieDomain(const GURL& url,
-                                           const ParsedCookie& pc,
-                                           CookieInclusionStatus& status) {
-  std::string domain_string;
-  if (pc.HasDomain())
-    domain_string = pc.Domain();
-  return cookie_util::GetCookieDomainWithString(url, domain_string, status);
-}
-
 // Compares cookies using name, domain and path, so that "equivalent" cookies
 // (per RFC 2965) are equal to each other.
 int PartialCookieOrdering(const CanonicalCookie& a, const CanonicalCookie& b) {
@@ -349,7 +339,9 @@
                             !base::IsStringASCII(parsed_cookie.Domain()));
 
   std::optional<std::string> cookie_domain =
-      GetCookieDomain(url, parsed_cookie, *status);
+      cookie_util::GetCookieDomainWithString(
+          url, parsed_cookie.HasDomain() ? parsed_cookie.Domain() : "",
+          *status);
   if (!cookie_domain) {
     DVLOG(net::cookie_util::kVlogSetCookies)
         << "Create() failed to get a valid cookie domain";
diff --git a/net/third_party/quiche/src b/net/third_party/quiche/src
index 981c424..cd46f49 160000
--- a/net/third_party/quiche/src
+++ b/net/third_party/quiche/src
@@ -1 +1 @@
-Subproject commit 981c424462d9e5210dc843e92b325c93d3bee4e9
+Subproject commit cd46f4983659501081e81ac45237ec723d00f15e
diff --git a/pdf/pdf_ink_module.cc b/pdf/pdf_ink_module.cc
index 2595ed0..766219e 100644
--- a/pdf/pdf_ink_module.cc
+++ b/pdf/pdf_ink_module.cc
@@ -586,6 +586,9 @@
   state.start_time = std::nullopt;
   state.page_index = -1;
   state.input_last_event.reset();
+
+  MaybeSetDrawingBrushAndCursor();
+
   return true;
 }
 
@@ -669,6 +672,9 @@
   state.erasing = false;
   state.page_indices_with_erasures.clear();
   state.input_last_event_position.reset();
+
+  MaybeSetDrawingBrushAndCursor();
+
   return true;
 }
 
@@ -861,19 +867,17 @@
   std::optional<PdfInkBrush::Type> brush_type =
       PdfInkBrush::StringToType(brush_type_string);
   CHECK(brush_type.has_value());
-  // Do not adjust `current_tool_state_` if a drawing stroke is already
+  pending_drawing_brush_state_ = PendingDrawingBrushState{
+      SkColorSetRGB(color_r, color_g, color_b), size, brush_type.value()};
+
+  // Do not adjust current tool state if a drawing stroke is already
   // in-progress.  Changes to the tool state will only apply to subsequent
   // strokes.
-  if (is_erasing_stroke() || !drawing_stroke_state().start_time.has_value()) {
-    current_tool_state_.emplace<DrawingStrokeState>();
+  if (is_drawing_stroke() && drawing_stroke_state().start_time.has_value()) {
+    return;
   }
-  drawing_stroke_state().brush_type = brush_type.value();
 
-  PdfInkBrush& current_brush = GetDrawingBrush();
-  current_brush.SetColor(SkColorSetRGB(color_r, color_g, color_b));
-  current_brush.SetSize(size);
-
-  MaybeSetCursor();
+  MaybeSetDrawingBrushAndCursor();
 }
 
 void PdfInkModule::HandleSetAnnotationModeMessage(
@@ -1164,6 +1168,24 @@
   }
 }
 
+void PdfInkModule::MaybeSetDrawingBrushAndCursor() {
+  if (!pending_drawing_brush_state_.has_value()) {
+    return;
+  }
+
+  current_tool_state_.emplace<DrawingStrokeState>();
+  drawing_stroke_state().brush_type = pending_drawing_brush_state_->type;
+
+  PdfInkBrush& current_brush = GetDrawingBrush();
+  current_brush.SetColor(pending_drawing_brush_state_->color);
+  current_brush.SetSize(pending_drawing_brush_state_->size);
+
+  pending_drawing_brush_state_.reset();
+
+  // If the brush could have changed, reflect that in the cursor as well.
+  MaybeSetCursor();
+}
+
 void PdfInkModule::MaybeSetCursor() {
   if (!enabled()) {
     // Do nothing when disabled. The code outside of PdfInkModule will select a
diff --git a/pdf/pdf_ink_module.h b/pdf/pdf_ink_module.h
index ab853c8..04091f7 100644
--- a/pdf/pdf_ink_module.h
+++ b/pdf/pdf_ink_module.h
@@ -276,6 +276,14 @@
     std::optional<gfx::PointF> input_last_event_position;
   };
 
+  // Drawing brush state changes that are pending the completion of an
+  // in-progress stroke.
+  struct PendingDrawingBrushState {
+    SkColor color;
+    float size;
+    PdfInkBrush::Type type;
+  };
+
   // Returns whether the event was handled or not.
   bool OnMouseDown(const blink::WebMouseEvent& event);
   bool OnMouseUp(const blink::WebMouseEvent& event);
@@ -365,6 +373,8 @@
 
   void MaybeSetCursor();
 
+  void MaybeSetDrawingBrushAndCursor();
+
   const raw_ref<PdfInkModuleClient> client_;
 
   bool enabled_ = false;
@@ -378,11 +388,17 @@
   StrokeIdGenerator stroke_id_generator_;
 
   // Store a PdfInkBrush for each brush type so that the brush parameters are
-  // saved when swapping between brushes.
+  // saved when swapping between brushes.  The PdfInkBrushes should not be
+  // modified in the middle of an in-progress stroke.
   PdfInkBrush highlighter_brush_;
   PdfInkBrush pen_brush_;
   float eraser_size_ = 3.0f;
 
+  // The parameters that are to be applied to the drawing brushes when a new
+  // stroke is started.  These can be modified at any time, including in the
+  // middle of an in-progress stroke.
+  std::optional<PendingDrawingBrushState> pending_drawing_brush_state_;
+
   // The state of the current tool that is in use.
   absl::variant<DrawingStrokeState, EraserState> current_tool_state_;
 
diff --git a/pdf/pdf_ink_module_unittest.cc b/pdf/pdf_ink_module_unittest.cc
index 87b97d3c..1e3723c 100644
--- a/pdf/pdf_ink_module_unittest.cc
+++ b/pdf/pdf_ink_module_unittest.cc
@@ -1566,7 +1566,8 @@
           .Build();
   EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
 
-  // While the stroke is still in progress, change the pen color.
+  // While the stroke is still in progress, change the pen color.  This has no
+  // immediate effect on the in-progress stroke.
   TestAnnotationBrushMessageParams red_pen_message_params{/*color_r=*/242,
                                                           /*color_g=*/139,
                                                           /*color_b=*/130};
@@ -1575,14 +1576,9 @@
 
   // Continue with mouse movement and then mouse up at a new location.  Notice
   // that the events are handled and the new stroke is added.
-  // TODO(crbug.com/381908888): The in-progress stroke is affected by the
-  // color change.  Update the expectation to show the in-progress stroke is
-  // still black once the brush management in PdfInkModule protects against
-  // such changes.
   static constexpr int kPageIndex = 0;
-  EXPECT_CALL(client(),
-              StrokeAdded(kPageIndex, InkStrokeId(0),
-                          InkStrokeBrushColorEq(SkColorSetRGB(242, 139, 130))));
+  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(0),
+                                    InkStrokeBrushColorEq(SK_ColorBLACK)));
   blink::WebMouseEvent mouse_move_event =
       MouseEventBuilder()
           .SetType(blink::WebInputEvent::Type::kMouseMove)
@@ -1611,17 +1607,10 @@
   InitializeSimpleSinglePageBasicLayout();
 
   // Start drawing a stroke with a black pen.  The stroke will not finish
-  // until the mouse-up event.  The cursor image will be updated immediately
-  // each time the brush tool is changed, regardless of whether a stroke is
-  // in progress.
+  // until the mouse-up event.  The cursor image will be updated only when
+  // there is not a stroke in progress.
   EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
-  {
-    InSequence seq;
-    EXPECT_CALL(client(),
-                UpdateInkCursorImage(BitmapImageSizeEq(SkISize(6, 6))));
-    EXPECT_CALL(client(),
-                UpdateInkCursorImage(BitmapImageSizeEq(SkISize(8, 8))));
-  }
+  EXPECT_CALL(client(), UpdateInkCursorImage(BitmapImageSizeEq(SkISize(6, 6))));
   TestAnnotationBrushMessageParams message_params{/*color_r=*/0,
                                                   /*color_g=*/0,
                                                   /*color_b=*/0};
@@ -1633,19 +1622,22 @@
           .Build();
   EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
 
-  // While the stroke is still in progress, change the pen size.
+  // While the stroke is still in progress, change the pen size.  This has no
+  // immediate effect on the in-progress stroke.
   SelectBrushTool(PdfInkBrush::Type::kPen, 6.0f, message_params);
   VerifyAndClearExpectations();
 
   // Continue with mouse movement and then mouse up at a new location.  Notice
-  // that the events are handled and the new stroke is added.
-  // TODO(crbug.com/381908888): The in-progress stroke and cursor image are
-  // affected by the size change.  Update the expectation to show the
-  // in-progress stroke is still thin once the brush management in
-  // PdfInkModule protects against such changes.
+  // that the events are handled and the new stroke is added.  The cursor image
+  // also gets updated once the stroke has finished.
   static constexpr int kPageIndex = 0;
-  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(0),
-                                    InkStrokeBrushSizeEq(6.0f)));
+  {
+    InSequence seq;
+    EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(0),
+                                      InkStrokeBrushSizeEq(2.0f)));
+    EXPECT_CALL(client(),
+                UpdateInkCursorImage(BitmapImageSizeEq(SkISize(8, 8))));
+  }
   blink::WebMouseEvent mouse_move_event =
       MouseEventBuilder()
           .SetType(blink::WebInputEvent::Type::kMouseMove)
@@ -1900,17 +1892,10 @@
   InitializeSimpleSinglePageBasicLayout();
 
   // Start drawing a stroke with a black pen.  The stroke will not finish
-  // until the mouse-up event.  The cursor image will be updated immediately
-  // each time the brush tool is changed, regardless of whether a stroke is
-  // in progress.
+  // until the mouse-up event.  The cursor image will be updated only if a
+  // stroke is not in progress.
   EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
-  {
-    InSequence seq;
-    EXPECT_CALL(client(),
-                UpdateInkCursorImage(BitmapImageSizeEq(SkISize(6, 6))));
-    EXPECT_CALL(client(),
-                UpdateInkCursorImage(BitmapImageSizeEq(SkISize(10, 10))));
-  }
+  EXPECT_CALL(client(), UpdateInkCursorImage(BitmapImageSizeEq(SkISize(6, 6))));
   TestAnnotationBrushMessageParams pen_message_params{/*color_r=*/0,
                                                       /*color_g=*/0,
                                                       /*color_b=*/0};
@@ -1932,15 +1917,19 @@
   VerifyAndClearExpectations();
 
   // Continue with mouse movement and then mouse up at a new location.  Notice
-  // that the events are handled and the new stroke is added.
-  // TODO(crbug.com/381908888): The in-progress stroke and cursor image are
-  // affected by the brush type change.  Update the expectation to show the
-  // in-progress stroke is still a pen once the brush management in
-  // PdfInkModule protects against such changes.
+  // that the events are handled and the new stroke is added.  The cursor gets
+  // updated to reflect the tool change to highlighter only after the stroke
+  // is completed.
   static constexpr int kPageIndex = 0;
-  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(0),
-                                    InkStrokeDrawingBrushTypeEq(
-                                        PdfInkBrush::Type::kHighlighter)));
+  {
+    InSequence seq;
+    EXPECT_CALL(
+        client(),
+        StrokeAdded(kPageIndex, InkStrokeId(0),
+                    InkStrokeDrawingBrushTypeEq(PdfInkBrush::Type::kPen)));
+    EXPECT_CALL(client(),
+                UpdateInkCursorImage(BitmapImageSizeEq(SkISize(10, 10))));
+  }
   blink::WebMouseEvent mouse_move_event =
       MouseEventBuilder()
           .SetType(blink::WebInputEvent::Type::kMouseMove)
diff --git a/services/network/attribution/request_headers_internal.h b/services/network/attribution/request_headers_internal.h
index de8f803..036dc872 100644
--- a/services/network/attribution/request_headers_internal.h
+++ b/services/network/attribution/request_headers_internal.h
@@ -14,7 +14,7 @@
 namespace network {
 
 // Options controlling greasing during serialization of the
-// Attribution-Reporting-Eligible and Attribution-Reportnig-Support headers,
+// Attribution-Reporting-Eligible and Attribution-Reporting-Support headers,
 // which contain structured dictionaries.
 struct AttributionReportingHeaderGreaseOptions {
   // Where to apply a grease.
diff --git a/testing/buildbot/filters/ios.blink_platform_unittests.filter b/testing/buildbot/filters/ios.blink_platform_unittests.filter
index 4edeb1d..94ee86b 100644
--- a/testing/buildbot/filters/ios.blink_platform_unittests.filter
+++ b/testing/buildbot/filters/ios.blink_platform_unittests.filter
@@ -1,16 +1,3 @@
 # TODO(crbug.com/41492456): chooses a different safe-to-break offset than is
 # chosen on other platforms.
--ShapeResultTest.AddUnsafeToBreakRtl
-
-# TODO(crbug.com/346852677): The below tests have been failing since
-# https://crrev.com/5534747.
--HarfBuzzFaceTest.HarfBuzzGetNominalGlyph_TestVSOverrideVariantEmoji
--HarfBuzzFaceTest.HarfBuzzGetNominalGlyph_TestVariantEmojiEmoji
--HarfBuzzFaceTest.HarfBuzzGetNominalGlyph_TestVariantEmojiText
--HarfBuzzFaceTest.HarfBuzzGetNominalGlyph_TestVariantEmojiUnicode
-
-# TODO(crbug.com/379818888): These tests are failing on the ios-blink-dbg-fyi
-# bot.
--RTCVideoEncoderFactoryTest.GetSupportedFormatsReturnsAllExpectedModes
--RTCVideoEncoderFactoryTest.GetSupportedFormatsReturnsAllModesExceptH265L1T2AndL1T3
--RTCVideoEncoderFactoryTest.GetSupportedFormatsReturnsAllModesExceptH265L1T3
+-ShapeResultTest.AddUnsafeToBreakRtl
\ No newline at end of file
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 36d7d37..3c2371d 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -17951,6 +17951,30 @@
             ]
         }
     ],
+    "PrivacySandboxAdsNoticeCCTSurvey": [
+        {
+            "platforms": [
+                "android"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "params": {
+                        "app-id": "com.google.android.googlequicksearchbox",
+                        "eea-accepted-trigger-id": "EHJUDsZQd0ugnJ3q1cK0Ru5GreU3",
+                        "eea-control-trigger-id": "EHJUDsZQd0ugnJ3q1cK0Ru5GreU3",
+                        "eea-declined-trigger-id": "EHJUDsZQd0ugnJ3q1cK0Ru5GreU3",
+                        "probability": "1.0",
+                        "row-acknowledged-trigger-id": "EHJUDsZQd0ugnJ3q1cK0Ru5GreU3",
+                        "row-control-trigger-id": "EHJUDsZQd0ugnJ3q1cK0Ru5GreU3"
+                    },
+                    "enable_features": [
+                        "PrivacySandboxCctAdsNoticeSurvey"
+                    ]
+                }
+            ]
+        }
+    ],
     "PrivacySandboxAllowPromptForBlocked3PCookies": [
         {
             "platforms": [
@@ -22536,6 +22560,28 @@
             ]
         }
     ],
+    "SpeculativeImageDecodes": [
+        {
+            "platforms": [
+                "android",
+                "android_webview",
+                "chromeos",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "PreventDuplicateImageDecodes",
+                        "SendExplicitDecodeRequestsImmediately",
+                        "SpeculativeImageDecodes"
+                    ]
+                }
+            ]
+        }
+    ],
     "SplitCacheByNavigationInitiatorSite": [
         {
             "platforms": [
diff --git a/third_party/android_deps/BUILD.gn b/third_party/android_deps/BUILD.gn
index 211c794..ae4b8bf 100644
--- a/third_party/android_deps/BUILD.gn
+++ b/third_party/android_deps/BUILD.gn
@@ -299,7 +299,7 @@
   # This is generated, do not edit. Update BuildConfigGenerator.groovy instead.
   android_aar_prebuilt(
       "com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework_java") {
-    aar_path = "cipd/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/accessibility-test-framework-4.0.0.aar"
+    aar_path = "cipd/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/accessibility-test-framework-4.1.1.aar"
     info_path = "libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/com_google_android_accessibility_test_framework.info"
     enable_bytecode_checks = false
     testonly = true
@@ -311,10 +311,15 @@
       "//third_party/android_deps:material_design_java",
       "//third_party/android_deps:protobuf_lite_runtime_java",
       "//third_party/androidx:androidx_core_core_java",
+      "//third_party/androidx:androidx_test_espresso_espresso_core_java",
+      "//third_party/androidx:androidx_test_rules_java",
+      "//third_party/androidx:androidx_test_runner_java",
       "//third_party/androidx:androidx_test_services_storage_java",
       "//third_party/hamcrest:hamcrest_core_java",
       "//third_party/hamcrest:hamcrest_library_java",
     ]
+    proguard_configs =
+        [ "local_modifications/accessibility_test_framework.pcfg" ]
   }
 
   # This is generated, do not edit. Update BuildConfigGenerator.groovy instead.
@@ -399,12 +404,11 @@
   # This is generated, do not edit. Update BuildConfigGenerator.groovy instead.
   if (google_play_services_package == "//third_party/android_deps") {
     android_aar_prebuilt("google_play_services_basement_java") {
-      aar_path = "cipd/libs/com_google_android_gms_play_services_basement/play-services-basement-18.4.0.aar"
+      aar_path = "cipd/libs/com_google_android_gms_play_services_basement/play-services-basement-18.5.0.aar"
       info_path = "libs/com_google_android_gms_play_services_basement/com_google_android_gms_play_services_basement.info"
       enable_bytecode_checks = false
       deps = [
-        "//third_party/androidx:androidx_collection_collection_java",
-        "//third_party/androidx:androidx_core_core_java",
+        "//third_party/androidx:androidx_collection_collection_jvm_java",
         "//third_party/androidx:androidx_fragment_fragment_java",
       ]
 
@@ -497,7 +501,7 @@
   # This is generated, do not edit. Update BuildConfigGenerator.groovy instead.
   if (google_play_services_package == "//third_party/android_deps") {
     android_aar_prebuilt("google_play_services_identity_credentials_java") {
-      aar_path = "cipd/libs/com_google_android_gms_play_services_identity_credentials/play-services-identity-credentials-16.0.0-alpha02.aar"
+      aar_path = "cipd/libs/com_google_android_gms_play_services_identity_credentials/play-services-identity-credentials-16.0.0-alpha04.aar"
       info_path = "libs/com_google_android_gms_play_services_identity_credentials/com_google_android_gms_play_services_identity_credentials.info"
       enable_bytecode_checks = false
       deps = [
diff --git a/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy b/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy
index 343a01b..71ae93c 100644
--- a/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy
+++ b/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy
@@ -873,6 +873,9 @@
                 sb.append('  # Because of dep on byte_buddy_android_java.\n')
                 sb.append('  bypass_platform_checks = true\n')
                 break
+            case 'com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework':
+                sb.append('  proguard_configs = [ "local_modifications/accessibility_test_framework.pcfg" ]')
+                break
         }
     }
 
diff --git a/third_party/android_deps/buildSrc/src/main/groovy/ChromiumDepGraph.groovy b/third_party/android_deps/buildSrc/src/main/groovy/ChromiumDepGraph.groovy
index 6380885..7ce3af8 100644
--- a/third_party/android_deps/buildSrc/src/main/groovy/ChromiumDepGraph.groovy
+++ b/third_party/android_deps/buildSrc/src/main/groovy/ChromiumDepGraph.groovy
@@ -390,7 +390,7 @@
         // think in practice the only things getting annotated here will be a single level of
         // synthetic groups which depend on testOnly targets.
         dependencies.each { _, dep ->
-            dep.testOnly = dep.children.any { id ->
+            dep.testOnly |= dep.children.any { id ->
                 dependencies.get(id).testOnly
             }
         }
diff --git a/third_party/android_deps/buildSrc/src/main/groovy/ChromiumPlugin.groovy b/third_party/android_deps/buildSrc/src/main/groovy/ChromiumPlugin.groovy
index 8022c6ba..d3b73e9a 100644
--- a/third_party/android_deps/buildSrc/src/main/groovy/ChromiumPlugin.groovy
+++ b/third_party/android_deps/buildSrc/src/main/groovy/ChromiumPlugin.groovy
@@ -5,13 +5,28 @@
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.artifacts.DependencyResolveDetails
-import org.gradle.api.attributes.Bundling
+import org.gradle.api.attributes.*
+import org.gradle.api.attributes.java.TargetJvmEnvironment
 
 /**
  * Plugin designed to define the configuration names to be used in the Gradle files to describe the dependencies that
  * {@link ChromiumDepGraph} with pick up.
  */
 class ChromiumPlugin implements Plugin<Project> {
+    // Do not fail if environment != android
+    static class TargetJvmEnvironmentCompatibilityRules implements AttributeCompatibilityRule<TargetJvmEnvironment> {
+
+        // public constructor to make reflective initialization happy.
+        public TargetJvmEnvironmentCompatibilityRules() {}
+
+        @Override
+        public void execute(CompatibilityCheckDetails<TargetJvmEnvironment> details) {
+            // This means regardless of the actual value of the attribute, it is
+            // considered a match. Gradle still picks the closest though if multiple
+            // options are available (which is what we want).
+            details.compatible();
+        }
+    }
 
     void apply(Project project) {
         // The configurations here are going to be used in ChromiumDepGraph. Keep it up to date with the declarations
@@ -39,6 +54,12 @@
             androidTestCompile
         }
 
+        project.dependencies.attributesSchema {
+            attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE) {
+                getCompatibilityRules().add(TargetJvmEnvironmentCompatibilityRules.class)
+            }
+        }
+
         project.configurations.all {
             resolutionStrategy.eachDependency { DependencyResolveDetails details ->
                 if (project.ext.has('versionOverrideMap') && project.ext.versionOverrideMap) {
@@ -48,16 +69,21 @@
                         details.useVersion version
                     }
                 }
+            }
+            attributes {
+                attribute(Attribute.of("org.gradle.category", String), "library")
+                attribute(Attribute.of("org.gradle.usage", String), "java-runtime")
+                attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
+                        project.objects.named(TargetJvmEnvironment, TargetJvmEnvironment.ANDROID))
+            }
+        }
 
-                // Not ideal but necessary for https://crbug.com/359896493. If you can find a way to use attributes
-                // instead, please delete this code.
-                if (details.requested.name.endsWith("-desktop")) {
-                    String newName = details.requested.name.replace("-desktop", "-android")
-                    details.useTarget("${details.requested.group}:${newName}:${details.requested.version}")
-                } else if (details.requested.name.endsWith("-jvmstubs")) {
-                    String newName = details.requested.name.replace("-jvmstubs", "-android")
-                    details.useTarget("${details.requested.group}:${newName}:${details.requested.version}")
-                }
+        // testCompile config is for host side tests (Robolectric) so we prefer
+        // the non-android versions of deps if available.
+        project.configurations.testCompile {
+            attributes {
+                attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
+                        project.objects.named(TargetJvmEnvironment, TargetJvmEnvironment.STANDARD_JVM))
             }
         }
 
@@ -71,6 +97,7 @@
             // transitive false means do not also pull in the deps of these deps.
             transitive = false
         }
+
     }
 
 }
diff --git a/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/README.chromium b/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/README.chromium
index 11916b7c..efacd9f0 100644
--- a/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/README.chromium
+++ b/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/README.chromium
@@ -1,7 +1,7 @@
 Name: Accessibility Test Framework
 Short Name: accessibility-test-framework
 URL: https://github.com/google/Accessibility-Test-Framework-for-Android
-Version: 4.0.0
+Version: 4.1.1
 License: Android Software Development Kit License
 License File: LICENSE
 CPEPrefix: unknown
diff --git a/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/cipd.yaml b/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/cipd.yaml
index 117445c1..9e6bc4d 100644
--- a/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/cipd.yaml
+++ b/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework/cipd.yaml
@@ -3,8 +3,8 @@
 # found in the LICENSE file.
 
 # To create CIPD package run the following command.
-# cipd create --pkg-def cipd.yaml -tag version:2@4.0.0.cr1
+# cipd create --pkg-def cipd.yaml -tag version:2@4.1.1.cr1
 package: chromium/third_party/android_deps/libs/com_google_android_apps_common_testing_accessibility_framework_accessibility_test_framework
 description: "Accessibility Test Framework"
 data:
-- file: accessibility-test-framework-4.0.0.aar
+- file: accessibility-test-framework-4.1.1.aar
diff --git a/third_party/android_deps/libs/com_google_android_gms_play_services_basement/LICENSE b/third_party/android_deps/libs/com_google_android_gms_play_services_basement/LICENSE
index 11707fdf9..48881b26 100644
--- a/third_party/android_deps/libs/com_google_android_gms_play_services_basement/LICENSE
+++ b/third_party/android_deps/libs/com_google_android_gms_play_services_basement/LICENSE
@@ -4778,6 +4778,95 @@
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 
+------------------
+
+Creative Commons CC0 1.0 Universal
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL
+SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT
+RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS.
+CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE
+INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES
+RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific
+works ("Commons") that the public can reliably and without fear of later
+claims of infringement build upon, modify, incorporate in other works, reuse
+and redistribute as freely as possible in any form whatsoever and for any
+purposes, including without limitation commercial purposes. These owners may
+contribute to the Commons to promote the ideal of a free culture and the
+further production of creative, cultural and scientific works, or to gain
+reputation or greater distribution for their Work in part through the use and
+efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation
+of additional consideration or compensation, the person associating CC0 with a
+Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
+and publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 
+
+i. the right to reproduce, adapt, distribute, perform, display, communicate,
+and translate a Work;
+
+ii. moral rights retained by the original author(s) and/or performer(s);
+
+iii. publicity and privacy rights pertaining to a person&apos;s image or
+likeness depicted in a Work;
+
+iv. rights protecting against unfair competition in regards to a Work, subject
+to the limitations in paragraph 4(a), below;
+
+v. rights protecting the extraction, dissemination, use and reuse of data in a
+Work;
+
+vi. database rights (such as those arising under Directive 96/9/EC of the
+European Parliament and of the Council of 11 March 1996 on the legal
+protection of databases, and under any national implementation thereof,
+including any amended or successor version of such directive); and
+
+vii. other similar, equivalent or corresponding rights throughout the world
+based on applicable law or treaty, and any national implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer&apos;s Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer&apos;s heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer&apos;s express Statement of Purpose. 
+
+3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer&apos;s express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer&apos;s Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer&apos;s express Statement of Purpose. 
+
+4. Limitations and Disclaimers. 
+
+a. No trademark or patent rights held by Affirmer are waived, abandoned,
+surrendered, licensed or otherwise affected by this document.
+
+b. Affirmer offers the Work as-is and makes no representations or warranties
+of any kind concerning the Work, express, implied, statutory or otherwise,
+including without limitation warranties of title, merchantability, fitness for
+a particular purpose, non infringement, or the absence of latent or other
+defects, accuracy, or the present or absence of errors, whether or not
+discoverable, all to the greatest extent permissible under applicable law.
+
+c. Affirmer disclaims responsibility for clearing rights of other persons that
+may apply to the Work or any use thereof, including without limitation any
+person&apos;s Copyright and Related Rights in the Work. Further, Affirmer
+disclaims responsibility for obtaining any necessary consents, permissions or
+other rights required for any use of the Work.
+
+d. Affirmer understands and acknowledges that Creative Commons is not a party
+to this document and has no duty or obligation with respect to this CC0 or use
+of the Work.
+
+
 Kotlin coroutines:
 
                                  Apache License
@@ -5961,4 +6050,208 @@
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
+   limitations under the License.
+
+kotlinx_serialization:
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
    limitations under the License.
\ No newline at end of file
diff --git a/third_party/android_deps/libs/com_google_android_gms_play_services_basement/README.chromium b/third_party/android_deps/libs/com_google_android_gms_play_services_basement/README.chromium
index 819563be..7eb14d11 100644
--- a/third_party/android_deps/libs/com_google_android_gms_play_services_basement/README.chromium
+++ b/third_party/android_deps/libs/com_google_android_gms_play_services_basement/README.chromium
@@ -1,7 +1,7 @@
 Name: play-services-basement
 Short Name: play-services-basement
 URL: https://developers.google.com/android/guides/setup
-Version: 18.4.0
+Version: 18.5.0
 License: Android Software Development Kit License
 License File: LICENSE
 CPEPrefix: unknown
diff --git a/third_party/android_deps/libs/com_google_android_gms_play_services_basement/cipd.yaml b/third_party/android_deps/libs/com_google_android_gms_play_services_basement/cipd.yaml
index db98789..fe94a50 100644
--- a/third_party/android_deps/libs/com_google_android_gms_play_services_basement/cipd.yaml
+++ b/third_party/android_deps/libs/com_google_android_gms_play_services_basement/cipd.yaml
@@ -3,8 +3,8 @@
 # found in the LICENSE file.
 
 # To create CIPD package run the following command.
-# cipd create --pkg-def cipd.yaml -tag version:2@18.4.0.cr1
+# cipd create --pkg-def cipd.yaml -tag version:2@18.5.0.cr1
 package: chromium/third_party/android_deps/libs/com_google_android_gms_play_services_basement
 description: "play-services-basement"
 data:
-- file: play-services-basement-18.4.0.aar
+- file: play-services-basement-18.5.0.aar
diff --git a/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/LICENSE b/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/LICENSE
index 353a0cb7..48881b26 100644
--- a/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/LICENSE
+++ b/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/LICENSE
@@ -6050,4 +6050,208 @@
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
+   limitations under the License.
+
+kotlinx_serialization:
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
    limitations under the License.
\ No newline at end of file
diff --git a/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/README.chromium b/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/README.chromium
index 3aa667b..fe35b62 100644
--- a/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/README.chromium
+++ b/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/README.chromium
@@ -1,7 +1,7 @@
 Name: play-services-identity-credentials
 Short Name: play-services-identity-credentials
 URL: https://developers.google.com/android/guides/setup
-Version: 16.0.0-alpha02
+Version: 16.0.0-alpha04
 License: Android Software Development Kit License
 License File: LICENSE
 CPEPrefix: unknown
diff --git a/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/cipd.yaml b/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/cipd.yaml
index 4a9baa8..d9a0d4732 100644
--- a/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/cipd.yaml
+++ b/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials/cipd.yaml
@@ -3,8 +3,8 @@
 # found in the LICENSE file.
 
 # To create CIPD package run the following command.
-# cipd create --pkg-def cipd.yaml -tag version:2@16.0.0-alpha02.cr1
+# cipd create --pkg-def cipd.yaml -tag version:2@16.0.0-alpha04.cr1
 package: chromium/third_party/android_deps/libs/com_google_android_gms_play_services_identity_credentials
 description: "play-services-identity-credentials"
 data:
-- file: play-services-identity-credentials-16.0.0-alpha02.aar
+- file: play-services-identity-credentials-16.0.0-alpha04.aar
diff --git a/third_party/android_deps/libs/javax_annotation_javax_annotation_api/README.chromium b/third_party/android_deps/libs/javax_annotation_javax_annotation_api/README.chromium
index 6edff20..f1516df 100644
--- a/third_party/android_deps/libs/javax_annotation_javax_annotation_api/README.chromium
+++ b/third_party/android_deps/libs/javax_annotation_javax_annotation_api/README.chromium
@@ -2,7 +2,7 @@
 Short Name: javax.annotation-api
 URL: http://jcp.org/en/jsr/detail?id=250
 Version: 1.3.2
-License: CDDL-1.1, GPL-2.0-with-classpath-exception
+License: CDDLv1.1
 License File: LICENSE
 CPEPrefix: unknown
 Security Critical: no
diff --git a/third_party/android_deps/libs/javax_annotation_jsr250_api/README.chromium b/third_party/android_deps/libs/javax_annotation_jsr250_api/README.chromium
index 4a22400a1..515002d 100644
--- a/third_party/android_deps/libs/javax_annotation_jsr250_api/README.chromium
+++ b/third_party/android_deps/libs/javax_annotation_jsr250_api/README.chromium
@@ -2,7 +2,7 @@
 Short Name: jsr250-api
 URL: http://jcp.org/aboutJava/communityprocess/final/jsr250/index.html
 Version: 1.0
-License: CDDL-1.0
+License: CDDLv1.0
 License File: LICENSE
 CPEPrefix: unknown
 Security Critical: no
diff --git a/third_party/android_deps/libs/org_jsoup_jsoup/LICENSE b/third_party/android_deps/libs/org_jsoup_jsoup/LICENSE
index 59280ab2..e4bf2be9f 100644
--- a/third_party/android_deps/libs/org_jsoup_jsoup/LICENSE
+++ b/third_party/android_deps/libs/org_jsoup_jsoup/LICENSE
@@ -1,6 +1,6 @@
 The MIT License
 
-Copyright (c) 2009-2024 Jonathan Hedley <https://jsoup.org/>
+Copyright (c) 2009-2025 Jonathan Hedley <https://jsoup.org/>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/third_party/android_deps/local_modifications/accessibility_test_framework.pcfg b/third_party/android_deps/local_modifications/accessibility_test_framework.pcfg
new file mode 100644
index 0000000..5dc8ae4
--- /dev/null
+++ b/third_party/android_deps/local_modifications/accessibility_test_framework.pcfg
@@ -0,0 +1,3 @@
+# This class is referenced by accessiblity_test_framework but is only needed
+# during compile (and we use a prebuilt).
+-dontwarn com.google.auto.value.AutoValue*
\ No newline at end of file
diff --git a/third_party/androidx/build.gradle.template b/third_party/androidx/build.gradle.template
index 40c8de6..da73d95 100644
--- a/third_party/androidx/build.gradle.template
+++ b/third_party/androidx/build.gradle.template
@@ -12,42 +12,6 @@
     mavenCentral()
 }
 
-// Do not fail if environment != android
-class TargetJvmEnvironmentCompatibilityRules implements AttributeCompatibilityRule<TargetJvmEnvironment> {
-
-    // public constructor to make reflective initialization happy.
-    public TargetJvmEnvironmentCompatibilityRules() {}
-
-    @Override
-    public void execute(CompatibilityCheckDetails<TargetJvmEnvironment> details) {
-        // This means regardless of the actual value of the attribute, it is
-        // considered a match. Gradle still picks the closest though if multiple
-        // options are available (which is what we want).
-        details.compatible();
-    }
-}
-
-dependencies.attributesSchema {
-    attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE) {
-        getCompatibilityRules().add(TargetJvmEnvironmentCompatibilityRules.class)
-    }
-}
-
-configurations.androidTestCompile {
-    attributes {
-        attribute(Attribute.of("org.gradle.category", String), "library")
-        attribute(Attribute.of("org.gradle.usage", String), "java-runtime")
-        attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(TargetJvmEnvironment, TargetJvmEnvironment.ANDROID))
-    }
-}
-configurations.compile {
-    attributes {
-        attribute(Attribute.of("org.gradle.category", String), "library")
-        attribute(Attribute.of("org.gradle.usage", String), "java-runtime")
-        attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(TargetJvmEnvironment, TargetJvmEnvironment.ANDROID))
-    }
-}
-
 dependencies {
     // Note about the configuration names: they are defined in buildSrc/ChromiumPlugin
 
diff --git a/third_party/angle b/third_party/angle
index 4b0f6f0..4c60a30 160000
--- a/third_party/angle
+++ b/third_party/angle
@@ -1 +1 @@
-Subproject commit 4b0f6f0649d2ed2c214cc093d6a86d4ce0009e52
+Subproject commit 4c60a308592af56dc745d8d1ba994e89ebb053db
diff --git a/third_party/blink/common/BUILD.gn b/third_party/blink/common/BUILD.gn
index 8c11d30..b278db6 100644
--- a/third_party/blink/common/BUILD.gn
+++ b/third_party/blink/common/BUILD.gn
@@ -284,7 +284,6 @@
     "scheduler/web_scheduler_tracked_feature.cc",
     "scheme_registry.cc",
     "security/address_space_feature.cc",
-    "service_worker/extended_service_worker_status_code.cc",
     "service_worker/service_worker_loader_helpers.cc",
     "service_worker/service_worker_router_rule.cc",
     "service_worker/service_worker_router_rule_mojom_traits.cc",
diff --git a/third_party/blink/common/service_worker/extended_service_worker_status_code.cc b/third_party/blink/common/service_worker/extended_service_worker_status_code.cc
deleted file mode 100644
index 3fa4ba44..0000000
--- a/third_party/blink/common/service_worker/extended_service_worker_status_code.cc
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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 "third_party/blink/public/common/service_worker/extended_service_worker_status_code.h"
-
-#include "base/notreached.h"
-
-namespace blink {
-
-const char* ExtendedServiceWorkerStatusToString(
-    ExtendedServiceWorkerStatusCode status) {
-  switch (status) {
-    case ExtendedServiceWorkerStatusCode::kUnknown:
-      return "Unknown status";
-  }
-  NOTREACHED();
-}
-
-}  // namespace blink
diff --git a/third_party/blink/common/widget/visual_properties.cc b/third_party/blink/common/widget/visual_properties.cc
index 135a690a..c9cf3aa1 100644
--- a/third_party/blink/common/widget/visual_properties.cc
+++ b/third_party/blink/common/widget/visual_properties.cc
@@ -19,7 +19,7 @@
          auto_resize_enabled == other.auto_resize_enabled &&
          min_size_for_auto_resize == other.min_size_for_auto_resize &&
          max_size_for_auto_resize == other.max_size_for_auto_resize &&
-         new_size == other.new_size &&
+         new_size_device_px == other.new_size_device_px &&
          visible_viewport_size == other.visible_viewport_size &&
          compositor_viewport_pixel_rect ==
              other.compositor_viewport_pixel_rect &&
diff --git a/third_party/blink/common/widget/visual_properties_mojom_traits.cc b/third_party/blink/common/widget/visual_properties_mojom_traits.cc
index 5a47c97..79854c13 100644
--- a/third_party/blink/common/widget/visual_properties_mojom_traits.cc
+++ b/third_party/blink/common/widget/visual_properties_mojom_traits.cc
@@ -18,7 +18,7 @@
   if (!data.ReadScreenInfos(&out->screen_infos) ||
       !data.ReadMinSizeForAutoResize(&out->min_size_for_auto_resize) ||
       !data.ReadMaxSizeForAutoResize(&out->max_size_for_auto_resize) ||
-      !data.ReadNewSize(&out->new_size) ||
+      !data.ReadNewSize(&out->new_size_device_px) ||
       !data.ReadVisibleViewportSize(&out->visible_viewport_size) ||
       !data.ReadCompositorViewportPixelRect(
           &out->compositor_viewport_pixel_rect) ||
diff --git a/third_party/blink/public/common/widget/visual_properties.h b/third_party/blink/public/common/widget/visual_properties.h
index 628ce647..a020546 100644
--- a/third_party/blink/public/common/widget/visual_properties.h
+++ b/third_party/blink/public/common/widget/visual_properties.h
@@ -64,8 +64,8 @@
   // The maximum size for Blink if auto-resize is enabled.
   gfx::Size max_size_for_auto_resize;
 
-  // The size for the widget in DIPs.
-  gfx::Size new_size;
+  // The size for the widget, in device pixels.
+  gfx::Size new_size_device_px;
 
   // The size of the area of the widget that is visible to the user, in DIPs.
   // The visible area may be empty if the visible area does not intersect with
@@ -75,15 +75,17 @@
   // as with an on-screen keyboard.
   gfx::Size visible_viewport_size;
 
-  // The rect of compositor's viewport in pixels. Note that for top level
-  // widgets this is roughly the DSF scaled new_size put into a rect. For child
-  // frame widgets it is a pixel-perfect bounds of the visible region of the
-  // widget. The size would be similar to visible_viewport_size, but in physical
-  // pixels and computed via very different means.
-  // TODO(danakj): It would be super nice to remove one of |new_size|,
-  // |visible_viewport_size| and |compositor_viewport_pixel_rect|. Their values
-  // overlap in purpose, creating a very confusing situation about which to use
-  // for what, and how they should relate or not.
+  // The rect of compositor's viewport in device pixels. Note that for top level
+  // widgets this is the same as |new_size| (when UseDevicePixelsForWidgetSizing
+  // is on; otherwise different by device pixel ratio) except that on Android
+  // it includes the size of the browser controls. For child frame widgets it
+  // is a pixel-perfect bounds of the visible region of the widget. The size
+  // would be similar to visible_viewport_size, but in device pixels and
+  // computed via very different means. TODO(danakj): It would be super nice to
+  // remove one of |new_size|, |visible_viewport_size| and
+  // |compositor_viewport_pixel_rect|. Their values overlap in purpose,
+  // creating a very confusing situation about which to use for what, and how
+  // they should relate or not.
   gfx::Rect compositor_viewport_pixel_rect;
 
   // Browser controls params such as top and bottom controls heights, whether
@@ -91,8 +93,9 @@
   cc::BrowserControlsParams browser_controls_params;
 
   // If shown and resizing the renderer, returns the height of the virtual
-  // keyboard in physical pixels. Otherwise, returns 0. Always 0 in a
+  // keyboard in device pixels. Otherwise, returns 0. Always 0 in a
   // non-outermost main frame.
+  // TODO(chrishtr): rename to virtual_keyboard_resize_height_device_px.
   int virtual_keyboard_resize_height_physical_px = 0;
 
   // Whether or not the focused node should be scrolled into view after the
diff --git a/third_party/blink/public/common/widget/visual_properties_mojom_traits.h b/third_party/blink/public/common/widget/visual_properties_mojom_traits.h
index aedd7b0..030373a4 100644
--- a/third_party/blink/public/common/widget/visual_properties_mojom_traits.h
+++ b/third_party/blink/public/common/widget/visual_properties_mojom_traits.h
@@ -42,7 +42,7 @@
   }
 
   static const gfx::Size& new_size(const blink::VisualProperties& r) {
-    return r.new_size;
+    return r.new_size_device_px;
   }
 
   static const gfx::Size& visible_viewport_size(
diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
index 583cc9fe..e10e5495 100644
--- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl
+++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
@@ -11174,6 +11174,14 @@
       string host
       integer port
 
+  # The state of the target window.
+  experimental type WindowState extends string
+    enum
+      normal
+      minimized
+      maximized
+      fullscreen
+
   # Activates (focuses) the target.
   command activateTarget
     parameters
@@ -11255,6 +11263,9 @@
       optional integer width
       # Frame height in DIP (requires newWindow to be true or headless shell).
       optional integer height
+      # Frame window state (requires newWindow to be true or headless shell).
+      # Default is normal.
+      optional WindowState windowState
       # The browser context to create the page in.
       experimental optional Browser.BrowserContextID browserContextId
       # Whether BeginFrames for this target will be controlled via DevTools (headless shell only,
diff --git a/third_party/blink/public/mojom/content_extraction/ai_page_content.mojom b/third_party/blink/public/mojom/content_extraction/ai_page_content.mojom
index 2345a33..78d92db 100644
--- a/third_party/blink/public/mojom/content_extraction/ai_page_content.mojom
+++ b/third_party/blink/public/mojom/content_extraction/ai_page_content.mojom
@@ -196,6 +196,16 @@
   AIPageContentNode root_node;
 };
 
+struct AIPageContentOptions {
+  // Indicates whether the geometry for each ContentNode should be included in
+  // the content.
+  bool include_geometry = true;
+
+  // Indicates whether this request is on the critical path, i.e., user visible.
+  // Requests which are not on the critical path can be de-prioritized to avoid
+  // blocking higher priority work.
+  bool on_critical_path = true;
+};
 
 // Used to obtain the AIPageContent representation for Documents. Lives in the
 // renderer process and called by the browser process to pull data for a
@@ -204,5 +214,5 @@
 interface AIPageContentAgent {
   // Provides the AIPageContent representation for this LocalFrame and all
   // LocalFrames accessible from the LocalFrameRoot.
-  GetAIPageContent() => (AIPageContent page_content);
+  GetAIPageContent(AIPageContentOptions request) => (AIPageContent page_content);
 };
diff --git a/third_party/blink/public/mojom/page/widget.mojom b/third_party/blink/public/mojom/page/widget.mojom
index 3efe898..896acd8 100644
--- a/third_party/blink/public/mojom/page/widget.mojom
+++ b/third_party/blink/public/mojom/page/widget.mojom
@@ -30,7 +30,8 @@
 [EnableIf=is_win]
 struct ProximateCharacterRangeBounds {
   gfx.mojom.Range range;
-  array<gfx.mojom.Rect> bounds;
+  // Character bounds are returned in DIP widget space.
+  array<gfx.mojom.Rect> widget_bounds_in_dips;
 };
 
 // This structure contains blink focus related data for stylus writing in
@@ -116,7 +117,7 @@
   // another message that writing has ended. This message responds with the
   // focused edit bounds and caret bounds after stylus writable element is
   // focused, and null if that element could not be focused.
-  OnStartStylusWriting([EnableIf=is_win] gfx.mojom.Rect focus_rect_in_widget)
+  OnStartStylusWriting([EnableIf=is_win] gfx.mojom.Rect focus_widget_rect_in_dips)
       => (StylusWritingFocusResult? focus_result);
 
   // Exposes an interface to the renderer to provide positional and styling
diff --git a/third_party/blink/renderer/bindings/core/v8/generated_code_helper.h b/third_party/blink/renderer/bindings/core/v8/generated_code_helper.h
index c88ac578..e338694 100644
--- a/third_party/blink/renderer/bindings/core/v8/generated_code_helper.h
+++ b/third_party/blink/renderer/bindings/core/v8/generated_code_helper.h
@@ -14,7 +14,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/events/event_target.h"
-#include "third_party/blink/renderer/core/frame/web_feature.h"
+#include "third_party/blink/renderer/core/frame/web_feature_forward.h"
 #include "third_party/blink/renderer/platform/bindings/exception_messages.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
 #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
diff --git a/third_party/blink/renderer/core/animation/animation.cc b/third_party/blink/renderer/core/animation/animation.cc
index 228be95a..3c47bcd 100644
--- a/third_party/blink/renderer/core/animation/animation.cc
+++ b/third_party/blink/renderer/core/animation/animation.cc
@@ -2311,12 +2311,28 @@
   if (target && keyframe_effect->Model() && keyframe_effect->IsCurrent()) {
     compositor_property_animations_have_no_effect_ =
         CompositorAnimations::CompositorPropertyAnimationsHaveNoEffect(
-            *target, *keyframe_effect->Model(), paint_artifact_compositor);
+            *target, this, *keyframe_effect->Model(),
+            paint_artifact_compositor);
   }
   if (compositor_property_animations_have_no_effect_ != had_no_effect)
     SetCompositorPending(CompositorPendingReason::kPendingEffectChange);
 }
 
+void Animation::OnPaintWorkletImageCreated() {
+  // If already queued up to make a compositing decision no further steps are
+  // required.
+  if (compositor_pending_) {
+    return;
+  }
+
+  if (!HasActiveAnimationsOnCompositor()) {
+    // We hit this state if target element is outside of the paint apron when
+    // the animation is created. Until painted, the animation has no visible
+    // effect. Once painted, we need to restart the animation on the compositor.
+    SetCompositorPending(CompositorPendingReason::kPaintWorkletImageCreated);
+  }
+}
+
 void Animation::StartAnimationOnCompositor(
     const PaintArtifactCompositor* paint_artifact_compositor) {
   DCHECK_EQ(
@@ -2374,12 +2390,14 @@
           timeline()->IsMonotonicallyIncreasing(), boundary_aligned);
 }
 
-Animation::NativePaintWorkletReasons Animation::GetNativePaintWorkletReasons() {
+Animation::NativePaintWorkletReasons Animation::GetNativePaintWorkletReasons()
+    const {
   if (native_paint_worklet_reasons_) {
     return native_paint_worklet_reasons_.value();
   }
   NativePaintWorkletReasons reasons = kNoPaintWorklet;
-  if (KeyframeEffect* keyframe_effect = DynamicTo<KeyframeEffect>(effect())) {
+  if (const KeyframeEffect* keyframe_effect =
+          DynamicTo<KeyframeEffect>(effect())) {
     if (RuntimeEnabledFeatures::CompositeBGColorAnimationEnabled() &&
         keyframe_effect->Affects(
             PropertyHandle(GetCSSPropertyBackgroundColor()))) {
@@ -2401,7 +2419,13 @@
   // Determine if we need to reset the cached state for a property that is
   // composited via a native paint worklet. If reset, it forces Paint to
   // re-evaluate whether to paint with a native paint worklet.
-  UpdateCompositedPaintStatus();
+  if (reason == CompositorPendingReason::kPaintWorkletImageCreated ||
+      reason == CompositorPendingReason::kPendingDowngrade) {
+    reason = CompositorPendingReason::kPendingRestart;
+    // Composited paint status has already be set so we can skip the update.
+  } else {
+    UpdateCompositedPaintStatus();
+  }
 
   if (RuntimeEnabledFeatures::
           CompositedAnimationsCancelledAsynchronouslyEnabled()) {
diff --git a/third_party/blink/renderer/core/animation/animation.h b/third_party/blink/renderer/core/animation/animation.h
index bc0a891..9116f9bb1 100644
--- a/third_party/blink/renderer/core/animation/animation.h
+++ b/third_party/blink/renderer/core/animation/animation.h
@@ -303,7 +303,12 @@
                            // including keyframes or active interval.
     kPendingCancel,        // Animation has been canceled, but could restart
                            // conditions permitting.
-    kPendingRestart        // Animation is to be restarted.
+    kPendingRestart,       // Animation is to be restarted.
+    kPaintWorkletImageCreated,  // A compositable animation was held in limbo
+                                // awaiting paint of the paint worklet image. It
+                                // can now be started on the compositor.
+    kPendingDowngrade  // Paint is forcing the animation to downgrade to
+                       // run on the main thread.
   };
   void SetCompositorPending(CompositorPendingReason reason);
 
@@ -381,6 +386,11 @@
   }
   bool AnimationHasNoEffect() const { return animation_has_no_effect_; }
 
+  // A native paint worklet animation has no visible effect until the deferred
+  // paint image has been generated. If the animation is not currently
+  // composited we need to restart it on the compositor.
+  void OnPaintWorkletImageCreated();
+
   bool WaitingOnDeferredStartTime() {
     return !start_time_ && (pending_play_ || pending_pause_);
   }
@@ -400,7 +410,7 @@
   };
 
   using NativePaintWorkletReasons = uint32_t;
-  NativePaintWorkletReasons GetNativePaintWorkletReasons();
+  NativePaintWorkletReasons GetNativePaintWorkletReasons() const;
 
  protected:
   DispatchEventResult DispatchEventInternal(Event&) override;
@@ -596,8 +606,10 @@
   // In the event of the keyframes changing, we need a new evaluation, of
   // the composited status for native paint worklet eligible properties.
   // A change in the playState can also necessitate a composited style update.
-  std::optional<NativePaintWorkletReasons> native_paint_worklet_reasons_;
-  std::optional<NativePaintWorkletReasons> prior_native_paint_worklet_reasons_;
+  mutable std::optional<NativePaintWorkletReasons>
+      native_paint_worklet_reasons_;
+  mutable std::optional<NativePaintWorkletReasons>
+      prior_native_paint_worklet_reasons_;
 
   // TODO(crbug.com/960944): Consider reintroducing kPause and cleanup use of
   // mutually exclusive pending_play_ and pending_pause_ flags.
diff --git a/third_party/blink/renderer/core/animation/compositor_animations.cc b/third_party/blink/renderer/core/animation/compositor_animations.cc
index 16998b902..82a3232 100644
--- a/third_party/blink/renderer/core/animation/compositor_animations.cc
+++ b/third_party/blink/renderer/core/animation/compositor_animations.cc
@@ -168,28 +168,18 @@
   }
 }
 
-// True if it is either a no-op background-color animation, or a no-op custom
-// property animation.
-bool IsNoOpPaintWorkletOrVariableAnimation(const PropertyHandle& property,
-                                      const LayoutObject* layout_object) {
-  // If the background color paint worklet was painted, a unique id will be
-  // generated. See BackgroundColorPaintWorklet::GetBGColorPaintWorkletParams
-  // for details.
-  // Similar to that, if a CSS paint worklet was painted, a unique id will be
-  // generated. See CSSPaintValue::GetImage for details.
-  bool has_unique_id = layout_object->FirstFragment().HasUniqueId();
-  if (has_unique_id)
+// True if it is a no-op custom property animation.
+bool IsNoOpVariableAnimation(const PropertyHandle& property,
+                             const LayoutObject* layout_object) {
+  // If a CSS paint worklet was painted, a unique id will be generated. See
+  // CSSPaintValue::GetImage for details.
+  // TODO(kevers): Verify that we properly latch to the animation if initially
+  // outside the paint apron and scrolled into the viewport.
+  if (layout_object->FirstFragment().HasUniqueId()) {
     return false;
-  // Now the |has_unique_id| == false.
-  bool is_no_op_bgcolor_anim =
-      RuntimeEnabledFeatures::CompositeBGColorAnimationEnabled() &&
-      property.GetCSSProperty().PropertyID() == CSSPropertyID::kBackgroundColor;
-  bool is_no_op_clip_anim =
-      RuntimeEnabledFeatures::CompositeClipPathAnimationEnabled() &&
-      property.GetCSSProperty().PropertyID() == CSSPropertyID::kClipPath;
-  bool is_no_op_variable_anim =
-      property.GetCSSProperty().PropertyID() == CSSPropertyID::kVariable;
-  return is_no_op_variable_anim || is_no_op_clip_anim || is_no_op_bgcolor_anim;
+  }
+
+  return property.GetCSSProperty().PropertyID() == CSSPropertyID::kVariable;
 }
 
 bool CompositedAnimationRequiresProperties(const PropertyHandle& property,
@@ -440,24 +430,9 @@
     }
   }
 
-  if (CompositorPropertyAnimationsHaveNoEffect(target_element, effect,
+  if (CompositorPropertyAnimationsHaveNoEffect(target_element, animation_to_add,
+                                               effect,
                                                paint_artifact_compositor)) {
-#if DCHECK_IS_ON()
-    if (effect.Affects(PropertyHandle(GetCSSPropertyBackgroundColor()))) {
-      ElementAnimations* element_animations =
-          target_element.GetElementAnimations();
-      DCHECK(element_animations &&
-             element_animations->CompositedBackgroundColorStatus() !=
-                 ElementAnimations::CompositedPaintStatus::kComposited);
-    }
-    if (effect.Affects(PropertyHandle(GetCSSPropertyClipPath()))) {
-      ElementAnimations* element_animations =
-          target_element.GetElementAnimations();
-      DCHECK(element_animations &&
-             element_animations->CompositedClipPathStatus() !=
-                 ElementAnimations::CompositedPaintStatus::kComposited);
-    }
-#endif
     reasons |= kAnimationHasNoVisibleChange;
   }
 
@@ -480,6 +455,7 @@
 
 bool CompositorAnimations::CompositorPropertyAnimationsHaveNoEffect(
     const Element& target_element,
+    const Animation* animation_to_add,
     const EffectModel& effect,
     const PaintArtifactCompositor* paint_artifact_compositor) {
   LayoutObject* layout_object = target_element.GetLayoutObject();
@@ -531,6 +507,18 @@
     return true;
   }
 
+  // Properties composited via native paint worklets do not necessarily have
+  // a paint property. In such cases, the unique ID is assigned at paint time
+  // when the deferred image for the paint worklet is constructed.  If no ID
+  // has been assigned yet, it may be because the element is outside the paint
+  // apron. The animation should not run on the compositor until painted.
+  if (animation_to_add && animation_to_add->GetNativePaintWorkletReasons() !=
+                              Animation::kNoPaintWorklet) {
+    if (!layout_object || !layout_object->FirstFragment().HasUniqueId()) {
+      return true;
+    }
+  }
+
   return false;
 }
 
@@ -1115,8 +1103,7 @@
 
     // By default, it is a kInvalidElementId.
     CompositorElementId id;
-    if (!IsNoOpPaintWorkletOrVariableAnimation(
-            property, target_element.GetLayoutObject())) {
+    if (!IsNoOpVariableAnimation(property, target_element.GetLayoutObject())) {
       id = CompositorElementIdFromUniqueObjectId(
               target_element.GetLayoutObject()->UniqueId(),
               CompositorElementNamespaceForProperty(
diff --git a/third_party/blink/renderer/core/animation/compositor_animations.h b/third_party/blink/renderer/core/animation/compositor_animations.h
index 7396727e..77c841faa 100644
--- a/third_party/blink/renderer/core/animation/compositor_animations.h
+++ b/third_party/blink/renderer/core/animation/compositor_animations.h
@@ -142,6 +142,7 @@
       PropertyHandleSet* unsupported_properties = nullptr);
   static bool CompositorPropertyAnimationsHaveNoEffect(
       const Element& target_element,
+      const Animation* animation,
       const EffectModel& effect,
       const PaintArtifactCompositor*);
   static void CancelIncompatibleAnimationsOnCompositor(const Element&,
diff --git a/third_party/blink/renderer/core/animation/compositor_animations_test.cc b/third_party/blink/renderer/core/animation/compositor_animations_test.cc
index cb915e3..ff519ce0 100644
--- a/third_party/blink/renderer/core/animation/compositor_animations_test.cc
+++ b/third_party/blink/renderer/core/animation/compositor_animations_test.cc
@@ -2790,6 +2790,18 @@
   Animation* animation =
       target->GetElementAnimations()->Animations().begin()->key;
 
+  // The animation cannot be composited until the associated layout object
+  // has a UniqueObjectId.
+  EXPECT_EQ(CompositorAnimations::kAnimationHasNoVisibleChange,
+            animation->CheckCanStartAnimationOnCompositor(
+                GetDocument().View()->GetPaintArtifactCompositor()));
+
+  // Normally the ID would be set at paint time by the native paint worklet.
+  // Since running in a test environment, we don't actually create the deferred
+  // paint image. Thus, we set it manually.
+  LayoutObject* layout_object = target->GetLayoutObject();
+  layout_object->GetMutableForPainting().FirstFragment().EnsureId();
+
   EXPECT_EQ(CompositorAnimations::kNoFailure,
             animation->CheckCanStartAnimationOnCompositor(
                 GetDocument().View()->GetPaintArtifactCompositor()));
diff --git a/third_party/blink/renderer/core/css/resolver/media_query_result.h b/third_party/blink/renderer/core/css/resolver/media_query_result.h
index 85f80328..75e930e 100644
--- a/third_party/blink/renderer/core/css/resolver/media_query_result.h
+++ b/third_party/blink/renderer/core/css/resolver/media_query_result.h
@@ -26,7 +26,7 @@
 
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/css/media_list.h"
-#include "third_party/blink/renderer/core/css/media_query_exp.h"
+#include "third_party/blink/renderer/platform/heap/member.h"
 #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
 #include "third_party/blink/renderer/platform/wtf/vector_traits.h"
 
diff --git a/third_party/blink/renderer/core/css/rule_feature_set.cc b/third_party/blink/renderer/core/css/rule_feature_set.cc
index d84ee8d1..a273165 100644
--- a/third_party/blink/renderer/core/css/rule_feature_set.cc
+++ b/third_party/blink/renderer/core/css/rule_feature_set.cc
@@ -40,6 +40,7 @@
 #include "third_party/blink/renderer/core/css/invalidation/invalidation_set.h"
 #include "third_party/blink/renderer/core/css/invalidation/rule_invalidation_data_builder.h"
 #include "third_party/blink/renderer/core/css/invalidation/rule_invalidation_data_tracer.h"
+#include "third_party/blink/renderer/core/css/media_query_exp.h"
 #include "third_party/blink/renderer/core/css/style_scope.h"
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/dom/node.h"
diff --git a/third_party/blink/renderer/core/exported/web_page_popup_impl.cc b/third_party/blink/renderer/core/exported/web_page_popup_impl.cc
index 038e786..a2608eb 100644
--- a/third_party/blink/renderer/core/exported/web_page_popup_impl.cc
+++ b/third_party/blink/renderer/core/exported/web_page_popup_impl.cc
@@ -926,7 +926,7 @@
   widget_base_->LayerTreeHost()->SetExternalPageScaleFactor(
       combined_scale_factor, visual_properties.is_pinch_gesture_active);
 
-  Resize(widget_base_->DIPsToCeiledBlinkSpace(visual_properties.new_size));
+  Resize(visual_properties.new_size_device_px);
 }
 
 gfx::Rect WebPagePopupImpl::ViewportVisibleRect() {
diff --git a/third_party/blink/renderer/core/exported/web_view_test.cc b/third_party/blink/renderer/core/exported/web_view_test.cc
index 9b2d6b0f..59722f2 100644
--- a/third_party/blink/renderer/core/exported/web_view_test.cc
+++ b/third_party/blink/renderer/core/exported/web_view_test.cc
@@ -6298,7 +6298,7 @@
 
   blink::VisualProperties visual_properties;
   visual_properties.screen_infos = display::ScreenInfos(display::ScreenInfo());
-  visual_properties.new_size = gfx::Size(400, 300);
+  visual_properties.new_size_device_px = gfx::Size(400, 300);
   visual_properties.visible_viewport_size = gfx::Size(400, 300);
   visual_properties.screen_infos.mutable_current().rect = gfx::Rect(800, 600);
 
diff --git a/third_party/blink/renderer/core/frame/local_frame_view.h b/third_party/blink/renderer/core/frame/local_frame_view.h
index 79b0df5..38be833 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.h
+++ b/third_party/blink/renderer/core/frame/local_frame_view.h
@@ -158,20 +158,6 @@
     virtual void DidFinishLayout() {}
   };
 
-  // Enables forcing lifecycle updates for throttled frames.
-  class CORE_EXPORT DisallowThrottlingScope {
-    STACK_ALLOCATED();
-
-   public:
-    explicit DisallowThrottlingScope(const LocalFrameView& frame_view);
-    DisallowThrottlingScope(const DisallowThrottlingScope&) = delete;
-    DisallowThrottlingScope& operator=(const DisallowThrottlingScope&) = delete;
-    ~DisallowThrottlingScope() = default;
-
-   private:
-    base::AutoReset<bool> value_;
-  };
-
   explicit LocalFrameView(LocalFrame&);
   LocalFrameView(LocalFrame&, const gfx::Size& initial_size);
   ~LocalFrameView() override;
@@ -890,6 +876,19 @@
     base::AutoReset<bool> value_;
   };
 
+  class CORE_EXPORT DisallowThrottlingScope {
+    STACK_ALLOCATED();
+
+   public:
+    explicit DisallowThrottlingScope(const LocalFrameView& frame_view);
+    DisallowThrottlingScope(const DisallowThrottlingScope&) = delete;
+    DisallowThrottlingScope& operator=(const DisallowThrottlingScope&) = delete;
+    ~DisallowThrottlingScope() = default;
+
+   private:
+    base::AutoReset<bool> value_;
+  };
+
   // The logic to determine whether a view can be render throttled is delicate,
   // but in some cases we want to unconditionally force all views in a local
   // frame tree to be throttled. Having ForceThrottlingScope on the stack will
diff --git a/third_party/blink/renderer/core/frame/screen_metrics_emulator.cc b/third_party/blink/renderer/core/frame/screen_metrics_emulator.cc
index 31625e4..7077759 100644
--- a/third_party/blink/renderer/core/frame/screen_metrics_emulator.cc
+++ b/third_party/blink/renderer/core/frame/screen_metrics_emulator.cc
@@ -178,7 +178,7 @@
   DCHECK(!frame_widget_->AutoResizeMode());
 
   original_screen_infos_ = visual_properties.screen_infos;
-  original_widget_size_ = visual_properties.new_size;
+  original_widget_size_ = visual_properties.new_size_device_px;
   original_visible_viewport_size_ = visual_properties.visible_viewport_size;
   original_root_viewport_segments_ =
       visual_properties.root_widget_viewport_segments;
diff --git a/third_party/blink/renderer/core/frame/web_feature_forward.h b/third_party/blink/renderer/core/frame/web_feature_forward.h
index 0ca12e76..e89d601 100644
--- a/third_party/blink/renderer/core/frame/web_feature_forward.h
+++ b/third_party/blink/renderer/core/frame/web_feature_forward.h
@@ -23,6 +23,7 @@
 }  // namespace blink
 }  // namespace mojom
 using WebFeature = mojom::WebFeature;
+using WebDXFeature = mojom::blink::WebDXFeature;
 }  // namespace blink
 
 #endif  // THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_WEB_FEATURE_FORWARD_H_
diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc b/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc
index b0c4780..4edbb7e 100644
--- a/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc
@@ -677,7 +677,7 @@
 
 void WebFrameWidgetImpl::OnStartStylusWriting(
 #if BUILDFLAG(IS_WIN)
-    const gfx::Rect& focus_rect_in_widget,
+    const gfx::Rect& focus_widget_rect_in_dips,
 #endif  // BUILDFLAG(IS_WIN)
     OnStartStylusWritingCallback callback) {
   mojom::blink::StylusWritingFocusResultPtr focus_result;
@@ -692,14 +692,13 @@
   Element* stylus_writable_container = nullptr;
 #if BUILDFLAG(IS_WIN)
   PositionWithAffinity proximate_pivot_position;
-  if (!focus_rect_in_widget.IsEmpty()) {
-    // TODO(crbug.com/355578906): Hit test using `focus_rect_in_widget` rather
-    // than its CenterPoint(). The size of the rect will include the
+  if (!focus_widget_rect_in_dips.IsEmpty()) {
+    // TODO(crbug.com/355578906): Hit test using `focus_widget_rect_in_dips`
+    // rather than its CenterPoint(). The size of the rect will include the
     // "target screen area" inflated with "distance threshold" from
     // ITfFocusHandwritingTargetArgs::GetPointerTargetInfo.
-    gfx::PointF frame_point =
-        frame->GetPage()->GetVisualViewport().ViewportToRootFrame(
-            gfx::PointF(focus_rect_in_widget.CenterPoint()));
+    const gfx::PointF frame_point = ViewportToRootFrame(
+        DIPsToBlinkSpace(gfx::PointF(focus_widget_rect_in_dips.CenterPoint())));
     proximate_pivot_position =
         frame->PositionForPoint(PhysicalOffset::FromPointFFloor(frame_point));
     stylus_writable_container = GetStylusHandwritingControlFromNode(
@@ -1977,9 +1976,20 @@
     const VisualProperties& visual_properties) {
   gfx::Rect new_compositor_viewport_pixel_rect =
       visual_properties.compositor_viewport_pixel_rect;
+  gfx::Size new_size;
+  new_size = visual_properties.new_size_device_px;
+  if (device_scale_factor_for_testing_) {
+    DCHECK(non_testing_device_scale_factor_);
+    new_size =
+        gfx::ScaleToFlooredSize(new_size, device_scale_factor_for_testing_ /
+                                              non_testing_device_scale_factor_);
+    new_compositor_viewport_pixel_rect = gfx::ScaleToEnclosedRect(
+        new_compositor_viewport_pixel_rect,
+        device_scale_factor_for_testing_ / non_testing_device_scale_factor_);
+  }
+
   if (ForMainFrame()) {
-    if (size_ !=
-        widget_base_->DIPsToCeiledBlinkSpace(visual_properties.new_size)) {
+    if (size_ != new_size) {
       // Only hide popups when the size changes. Eg https://crbug.com/761908.
       View()->CancelPagePopup();
     }
@@ -2014,7 +2024,7 @@
 
   if (ForMainFrame()) {
     if (!AutoResizeMode()) {
-      size_ = widget_base_->DIPsToCeiledBlinkSpace(visual_properties.new_size);
+      size_ = new_size;
 
       View()->ResizeWithBrowserControls(
           size_.value(),
@@ -2031,17 +2041,18 @@
   } else {
     // Widgets in a WebView's frame tree without a local main frame
     // set the size of the WebView to be the |visible_viewport_size|, in order
-    // to limit compositing in (out of process) child frames to what is visible.
+    // to limit compositing in (out of process) child frames to what is
+    // visible.
     //
     // Note that child frames in the same process/WebView frame tree as the
-    // main frame do not do this in order to not clobber the source of truth in
-    // the main frame.
+    // main frame do not do this in order to not clobber the source of truth
+    // in the main frame.
     if (!View()->MainFrameImpl()) {
       View()->Resize(widget_base_->DIPsToCeiledBlinkSpace(
           widget_base_->VisibleViewportSizeInDIPs()));
     }
 
-    Resize(widget_base_->DIPsToCeiledBlinkSpace(visual_properties.new_size));
+    Resize(new_size);
   }
 }
 
@@ -5100,6 +5111,11 @@
   DCHECK(ForMainFrame());
   DCHECK_GE(factor, 0.f);
 
+  if (!device_scale_factor_for_testing_) {
+    non_testing_device_scale_factor_ =
+        widget_base_->GetOriginalDeviceScaleFactor();
+  }
+
   // Stash the window size before we adjust the scale factor, as subsequent
   // calls to convert will use the new scale factor.
   gfx::Size size_in_dips = widget_base_->BlinkSpaceToFlooredDIPs(Size());
@@ -5107,8 +5123,10 @@
 
   // Receiving a 0 is used to reset between tests, it removes the override in
   // order to listen to the browser for the next test.
-  if (!factor)
+  if (!factor) {
+    non_testing_device_scale_factor_ = 0;
     return;
+  }
 
   // We are changing the device scale factor from the renderer, so allocate a
   // new viz::LocalSurfaceId to avoid surface invariants violations in tests.
@@ -5122,7 +5140,7 @@
   if (!AutoResizeMode()) {
     // This picks up the new device scale factor as
     // `UpdateCompositorViewportAndScreenInfo()` has applied a new value.
-    Resize(widget_base_->DIPsToCeiledBlinkSpace(size_in_dips));
+    Resize(size_with_dsf);
   }
 }
 
diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_impl.h b/third_party/blink/renderer/core/frame/web_frame_widget_impl.h
index c9a9553..3dd204c 100644
--- a/third_party/blink/renderer/core/frame/web_frame_widget_impl.h
+++ b/third_party/blink/renderer/core/frame/web_frame_widget_impl.h
@@ -470,7 +470,7 @@
                          base::OnceClosure callback) override;
   void OnStartStylusWriting(
 #if BUILDFLAG(IS_WIN)
-      const gfx::Rect& focus_rect_in_widget,
+      const gfx::Rect& focus_widget_rect_in_dips,
 #endif  // BUILDFLAG(IS_WIN)
       OnStartStylusWritingCallback callback) override;
 #if BUILDFLAG(IS_ANDROID)
@@ -666,8 +666,8 @@
       bool enabled,
       const blink::DeviceEmulationParams& params);
   void SetScreenInfoAndSize(const display::ScreenInfos& screen_infos,
-                            const gfx::Size& widget_size,
-                            const gfx::Size& visible_viewport_size);
+                            const gfx::Size& widget_size_in_dips,
+                            const gfx::Size& visible_viewport_size_in_dips);
 
   // Update the surface allocation information, compositor viewport rect and
   // screen info on the widget.
@@ -1212,6 +1212,11 @@
   // frame widgets.
   float device_scale_factor_for_testing_ = 0;
 
+  // When device_scale_factor_for_testing_ is set (i.e. nonzero), this
+  // stores the device scale factor before the testing override was set.
+  // Otherwise it is set to zero.
+  float non_testing_device_scale_factor_ = 0;
+
   // This struct contains data that is only valid for main frame widgets.
   // You should use `main_data()` to access it.
   struct MainFrameData {
diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_test.cc b/third_party/blink/renderer/core/frame/web_frame_widget_test.cc
index 22d87ea3..e0f5471 100644
--- a/third_party/blink/renderer/core/frame/web_frame_widget_test.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_widget_test.cc
@@ -321,7 +321,7 @@
   void OnStartStylusWriting() {
     MockMainFrameWidget()->OnStartStylusWriting(
 #if BUILDFLAG(IS_WIN)
-        /*focus_rect_in_widget=*/gfx::Rect(),
+        /*focus_widget_rect_in_dips=*/gfx::Rect(),
 #endif  // BUILDFLAG(IS_WIN)
         base::DoNothing());
   }
@@ -623,9 +623,77 @@
   url_test_helpers::ServeAsynchronousRequests();
 }
 
+// Without extrinsic sizing (e.g., css width & height), an image's final decode
+// size can depend on both the image's intrinsic size and layout. Using only the
+// image's intrinsic size can result in a speculative decode that is too small
+// (will not be used), or too big (can cause small rendering differences as the
+// larger decode will be re-used and scaled). To avoid these issues, we should
+// wait for layout if the decoded size depends on it.
+TEST_F(WebFrameWidgetImplSimTest, SpeculativeDecodeNoSizeWaitsForLayout) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitWithFeatures(
+      /*enabled_features=*/
+      {features::kSpeculativeImageDecodes,
+       ::features::kSendExplicitDecodeRequestsImmediately},
+      /*disabled_features=*/{});
+  SimRequest image_request("https://example.com/image.png", "image/png");
+  auto* widget = WebView().MainFrameViewWidget();
+  widget->Resize(gfx::Size(800, 600));
+  SimRequest doc_request("https://example.com/test.html", "text/html");
+  LoadURL("https://example.com/test.html");
+
+  {
+    EXPECT_CALL(*MockMainFrameWidget(), RequestDecode(_, _)).Times(0);
+    doc_request.Complete(
+        R"HTML(<!DOCTYPE html>
+        <img id="i1" src="image.png">
+        <img id="i2" style="height: auto; max-height: 50px;" src="image.png">
+      )HTML");
+    Compositor().BeginFrame();
+    test::RunPendingTasks();
+    image_request.Complete(
+        *test::ReadFromFile(test::CoreTestDataPath("background_image.png")));
+  }
+
+  {
+    EXPECT_CALL(*MockMainFrameWidget(), RequestDecode(_, _)).Times(1);
+    widget->UpdateAllLifecyclePhases(DocumentUpdateReason::kTest);
+  }
+}
+
+// A speculative decode of an image with extrinsic sizes does not need to wait
+// for layout.
+TEST_F(WebFrameWidgetImplSimTest, SpeculativeDecodeWithExtrinsicSize) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitWithFeatures(
+      /*enabled_features=*/
+      {features::kSpeculativeImageDecodes,
+       ::features::kSendExplicitDecodeRequestsImmediately},
+      /*disabled_features=*/{});
+  SimRequest image_request("https://example.com/image.png", "image/png");
+  auto* widget = WebView().MainFrameViewWidget();
+  widget->Resize(gfx::Size(800, 600));
+  SimRequest doc_request("https://example.com/test.html", "text/html");
+  LoadURL("https://example.com/test.html");
+
+  {
+    EXPECT_CALL(*MockMainFrameWidget(), RequestDecode(_, _)).Times(1);
+    doc_request.Complete(
+        R"HTML(<!DOCTYPE html>
+        <img style="width: 100px" src="image.png">
+      )HTML");
+    Compositor().BeginFrame();
+    test::RunPendingTasks();
+    image_request.Complete(
+        *test::ReadFromFile(test::CoreTestDataPath("background_image.png")));
+    test::RunPendingTasks();
+  }
+}
+
 #if BUILDFLAG(IS_WIN)
 struct ProximateBoundsCollectionArgs final {
-  base::RepeatingCallback<gfx::Rect(const Document&)> get_focus_rect_in_widget;
+  base::RepeatingCallback<gfx::Rect(const Document&)>
+      get_focus_widget_rect_in_dips;
   std::string expected_focus_id;
   bool expect_null_proximate_bounds;
   gfx::Range expected_range;
@@ -664,8 +732,8 @@
     return proximate_bounds_collection_args_.expected_focus_id;
   }
 
-  gfx::Rect GetFocusRectInWidget(const Document& document) const {
-    return proximate_bounds_collection_args_.get_focus_rect_in_widget.Run(
+  gfx::Rect GetFocusWidgetRectInDips(const Document& document) const {
+    return proximate_bounds_collection_args_.get_focus_widget_rect_in_dips.Run(
         document);
   }
 
@@ -761,9 +829,9 @@
     EXPECT_EQ(GetDocument().FocusedElement(), nullptr);
   }
 
-  void OnStartStylusWriting(const gfx::Rect& focus_rect_in_widget) {
+  void OnStartStylusWriting(const gfx::Rect& focus_widget_rect_in_dips) {
     MockMainFrameWidget()->OnStartStylusWriting(
-        focus_rect_in_widget,
+        focus_widget_rect_in_dips,
         base::BindOnce(&WebFrameWidgetProximateBoundsCollectionSimTestBase::
                            OnStartStylusWritingComplete,
                        weak_factory_.GetWeakPtr()));
@@ -831,10 +899,10 @@
             /*enable_stylus_handwriting_win=*/true) {}
 
   void StartStylusWritingOnElementCenter(const Element& element) {
-    gfx::Rect focus_rect_in_widget(element.BoundsInWidget().CenterPoint(),
-                                   gfx::Size());
-    focus_rect_in_widget.Outset(gfx::Outsets(25));
-    OnStartStylusWriting(focus_rect_in_widget);
+    gfx::Rect focus_widget_rect_in_dips(element.BoundsInWidget().CenterPoint(),
+                                        gfx::Size());
+    focus_widget_rect_in_dips.Outset(gfx::Outsets(25));
+    OnStartStylusWriting(focus_widget_rect_in_dips);
   }
 };
 
@@ -964,15 +1032,15 @@
                 // directions relative to the pivot position up-to
                 // the `ProximateBoundsCollectionHalfLimit()`.
                 ProximateBoundsCollectionArgs{
-                    /*get_focus_rect_in_widget=*/base::BindRepeating(
+                    /*get_focus_widget_rect_in_dips=*/base::BindRepeating(
                         [](const Document& document) -> gfx::Rect {
                           const Element* target = document.getElementById(
                               AtomicString("target_editable"));
-                          gfx::Rect focus_rect_in_widget(
+                          gfx::Rect focus_widget_rect_in_dips(
                               target->BoundsInWidget().top_center(),
                               gfx::Size());
-                          focus_rect_in_widget.Outset(gfx::Outsets(25));
-                          return focus_rect_in_widget;
+                          focus_widget_rect_in_dips.Outset(gfx::Outsets(25));
+                          return focus_widget_rect_in_dips;
                         }),
                     /*expected_focus_id=*/"target_editable",
                     /*expect_null_proximate_bounds=*/false,
@@ -984,14 +1052,14 @@
                 // range only expands in one direction up-to the
                 // `ProximateBoundsCollectionHalfLimit()`.
                 ProximateBoundsCollectionArgs{
-                    /*get_focus_rect_in_widget=*/base::BindRepeating(
+                    /*get_focus_widget_rect_in_dips=*/base::BindRepeating(
                         [](const Document& document) -> gfx::Rect {
                           const Element* target = document.getElementById(
                               AtomicString("target_editable"));
-                          gfx::Rect focus_rect_in_widget(
+                          gfx::Rect focus_widget_rect_in_dips(
                               target->BoundsInWidget().origin(), gfx::Size());
-                          focus_rect_in_widget.Outset(gfx::Outsets(25));
-                          return focus_rect_in_widget;
+                          focus_widget_rect_in_dips.Outset(gfx::Outsets(25));
+                          return focus_widget_rect_in_dips;
                         }),
                     /*expected_focus_id=*/"target_editable",
                     /*expect_null_proximate_bounds=*/false,
@@ -1002,16 +1070,16 @@
                 // range only expands in one direction up-to the
                 // `ProximateBoundsCollectionHalfLimit()`.
                 ProximateBoundsCollectionArgs{
-                    /*get_focus_rect_in_widget=*/base::BindRepeating(
+                    /*get_focus_widget_rect_in_dips=*/base::BindRepeating(
                         [](const Document& document) -> gfx::Rect {
                           const Element* target = document.getElementById(
                               AtomicString("target_editable"));
-                          gfx::Rect focus_rect_in_widget(
+                          gfx::Rect focus_widget_rect_in_dips(
                               target->BoundsInWidget().top_right() -
                                   gfx::Vector2d(1, 0),
                               gfx::Size());
-                          focus_rect_in_widget.Outset(gfx::Outsets(25));
-                          return focus_rect_in_widget;
+                          focus_widget_rect_in_dips.Outset(gfx::Outsets(25));
+                          return focus_widget_rect_in_dips;
                         }),
                     /*expected_focus_id=*/"target_editable",
                     /*expect_null_proximate_bounds=*/false,
@@ -1019,38 +1087,38 @@
                     /*expected_bounds=*/
                     {gfx::Rect(240, 0, 10, 10), gfx::Rect(250, 0, 9, 10)}},
                 // Test that `touch_fallback` is focused when
-                // `focus_rect_in_widget` misses, but it shouldn't collect
+                // `focus_widget_rect_in_dips` misses, but it shouldn't collect
                 // bounds because the pivot offset cannot be determined.
                 ProximateBoundsCollectionArgs{
-                    /*get_focus_rect_in_widget=*/base::BindRepeating(
+                    /*get_focus_widget_rect_in_dips=*/base::BindRepeating(
                         [](const Document& document) -> gfx::Rect {
                           const Element* target = document.getElementById(
                               AtomicString("target_editable"));
-                          gfx::Rect focus_rect_in_widget(
+                          gfx::Rect focus_widget_rect_in_dips(
                               target->BoundsInWidget().right_center() +
                                   gfx::Vector2d(100, 0),
                               gfx::Size());
-                          focus_rect_in_widget.Outset(gfx::Outsets(25));
-                          return focus_rect_in_widget;
+                          focus_widget_rect_in_dips.Outset(gfx::Outsets(25));
+                          return focus_widget_rect_in_dips;
                         }),
                     /*expected_focus_id=*/"touch_fallback",
                     /*expect_null_proximate_bounds=*/true,
                     /*expected_range=*/gfx::Range(),
                     /*expected_bounds=*/{}},
                 // Test that `touch_fallback` is focused when
-                // `focus_rect_in_widget` hits non-editable content, but it
+                // `focus_widget_rect_in_dips` hits non-editable content, but it
                 // shouldn't collect bounds because the pivot offset cannot be
                 // determined.
                 ProximateBoundsCollectionArgs{
-                    /*get_focus_rect_in_widget=*/base::BindRepeating(
+                    /*get_focus_widget_rect_in_dips=*/base::BindRepeating(
                         [](const Document& document) -> gfx::Rect {
                           const Element* target = document.getElementById(
                               AtomicString("target_readonly"));
-                          gfx::Rect focus_rect_in_widget(
+                          gfx::Rect focus_widget_rect_in_dips(
                               target->BoundsInWidget().CenterPoint(),
                               gfx::Size());
-                          focus_rect_in_widget.Outset(gfx::Outsets(25));
-                          return focus_rect_in_widget;
+                          focus_widget_rect_in_dips.Outset(gfx::Outsets(25));
+                          return focus_widget_rect_in_dips;
                         }),
                     /*expected_focus_id=*/"touch_fallback",
                     /*expect_null_proximate_bounds=*/true,
@@ -1061,7 +1129,7 @@
        TestProximateBoundsCollection) {
   LoadDocument(String(GetParam().GetHTMLDocument()));
   HandlePointerDownEventOverTouchFallback();
-  OnStartStylusWriting(GetParam().GetFocusRectInWidget(GetDocument()));
+  OnStartStylusWriting(GetParam().GetFocusWidgetRectInDips(GetDocument()));
   if (!GetParam().IsStylusHandwritingWinEnabled()) {
     EXPECT_EQ(GetDocument().FocusedElement(), nullptr);
     EXPECT_EQ(GetLastProximateBounds(), nullptr);
@@ -1079,10 +1147,11 @@
   EXPECT_EQ(!GetLastProximateBounds(), GetParam().ExpectNullProximateBounds());
   if (!GetParam().ExpectNullProximateBounds()) {
     EXPECT_EQ(GetLastProximateBounds()->range, GetParam().GetExpectedRange());
-    EXPECT_TRUE(std::equal(GetLastProximateBounds()->bounds.begin(),
-                           GetLastProximateBounds()->bounds.end(),
-                           GetParam().GetExpectedBounds().begin(),
-                           GetParam().GetExpectedBounds().end()));
+    EXPECT_TRUE(
+        std::equal(GetLastProximateBounds()->widget_bounds_in_dips.begin(),
+                   GetLastProximateBounds()->widget_bounds_in_dips.end(),
+                   GetParam().GetExpectedBounds().begin(),
+                   GetParam().GetExpectedBounds().end()));
   }
 }
 #endif  // BUILDFLAG(IS_WIN)
diff --git a/third_party/blink/renderer/core/layout/box_fragment_builder.h b/third_party/blink/renderer/core/layout/box_fragment_builder.h
index 31d2817..d059f15 100644
--- a/third_party/blink/renderer/core/layout/box_fragment_builder.h
+++ b/third_party/blink/renderer/core/layout/box_fragment_builder.h
@@ -589,9 +589,8 @@
     use_last_baseline_for_inline_baseline_ = true;
   }
 
-  void SetGapGeometry(
-      std::unique_ptr<GapFragmentData::GapGeometry> gap_geometry) {
-    gap_geometry_ = std::move(gap_geometry);
+  void SetGapGeometry(GapFragmentData::GapGeometry* gap_geometry) {
+    gap_geometry_ = gap_geometry;
   }
 
   void SetTableGridRect(const LogicalRect& table_grid_rect) {
@@ -770,7 +769,7 @@
   std::optional<LayoutUnit> last_baseline_;
   LayoutUnit math_italic_correction_;
 
-  std::unique_ptr<GapFragmentData::GapGeometry> gap_geometry_;
+  GapFragmentData::GapGeometry* gap_geometry_ = nullptr;
 
   // Table specific types.
   std::optional<LogicalRect> table_grid_rect_;
diff --git a/third_party/blink/renderer/core/layout/gap_fragment_data.h b/third_party/blink/renderer/core/layout/gap_fragment_data.h
index 45efe94..9c7cd1c 100644
--- a/third_party/blink/renderer/core/layout/gap_fragment_data.h
+++ b/third_party/blink/renderer/core/layout/gap_fragment_data.h
@@ -46,9 +46,7 @@
   using GapBoundaries = HeapVector<GapBoundary>;
 
   // Gap locations are used for painting gap decorations.
-  struct GapGeometry {
-    USING_FAST_MALLOC(GapGeometry);
-
+  struct GapGeometry : public GarbageCollected<GapGeometry> {
    public:
     GapBoundaries columns;
     GapBoundaries rows;
diff --git a/third_party/blink/renderer/core/layout/physical_box_fragment.cc b/third_party/blink/renderer/core/layout/physical_box_fragment.cc
index 452f409..8226ed8 100644
--- a/third_party/blink/renderer/core/layout/physical_box_fragment.cc
+++ b/third_party/blink/renderer/core/layout/physical_box_fragment.cc
@@ -334,7 +334,7 @@
       has_scrollable_overflow + !!builder->frame_set_layout_data_ +
       !!builder->mathml_paint_info_ + !!builder->table_grid_rect_ +
       !!builder->table_collapsed_borders_ +
-      !!builder->table_collapsed_borders_geometry_ + !!builder->gap_geometry_ +
+      !!builder->table_collapsed_borders_geometry_ +
       !!builder->table_cell_column_index_ +
       (builder->table_section_row_offsets_.empty() ? 0 : 2) +
       !!builder->page_name_ + !!borders + !!scrollbar + !!padding +
diff --git a/third_party/blink/renderer/core/layout/physical_box_fragment.h b/third_party/blink/renderer/core/layout/physical_box_fragment.h
index 60ae7dfb..6ba66ae6 100644
--- a/third_party/blink/renderer/core/layout/physical_box_fragment.h
+++ b/third_party/blink/renderer/core/layout/physical_box_fragment.h
@@ -173,10 +173,7 @@
   }
 
   const GapFragmentData::GapGeometry* GapGeometry() const {
-    if (const auto* field = GetRareField(FieldId::kGapGeometry)) {
-      return field->gap_geometry.get();
-    }
-    return nullptr;
+    return rare_data_->gap_geometry_.Get();
   }
 
   LogicalRect TableGridRect() const {
diff --git a/third_party/blink/renderer/core/layout/physical_fragment_rare_data.cc b/third_party/blink/renderer/core/layout/physical_fragment_rare_data.cc
index 11297f4a..f132ab82 100644
--- a/third_party/blink/renderer/core/layout/physical_fragment_rare_data.cc
+++ b/third_party/blink/renderer/core/layout/physical_fragment_rare_data.cc
@@ -27,7 +27,8 @@
       reading_flow_nodes_(builder.reading_flow_nodes_.size()
                               ? MakeGarbageCollected<HeapVector<Member<Node>>>(
                                     builder.reading_flow_nodes_)
-                              : nullptr) {
+                              : nullptr),
+      gap_geometry_(builder.gap_geometry_) {
   field_list_.ReserveInitialCapacity(num_fields);
 
   // Each field should be processed in order of FieldId to avoid vector
@@ -53,10 +54,6 @@
     SetField(FieldId::kFrameSetLayoutData).frame_set_layout_data =
         std::move(builder.frame_set_layout_data_);
   }
-  if (builder.gap_geometry_) {
-    SetField(FieldId::kGapGeometry).gap_geometry =
-        std::move(builder.gap_geometry_);
-  }
   if (builder.table_grid_rect_) {
     SetField(FieldId::kTableGridRect).table_grid_rect =
         *builder.table_grid_rect_;
@@ -104,7 +101,8 @@
 PhysicalFragmentRareData::PhysicalFragmentRareData(
     const PhysicalFragmentRareData& other)
     : table_collapsed_borders_(other.table_collapsed_borders_),
-      table_column_geometries_(other.table_column_geometries_) {
+      table_column_geometries_(other.table_column_geometries_),
+      gap_geometry_(other.gap_geometry_) {
   field_list_.ReserveInitialCapacity(other.field_list_.capacity());
 
   // Each field should be processed in order of FieldId to avoid vector
@@ -116,7 +114,6 @@
   SET_IF_EXISTS(kPadding, padding, other);
   SET_IF_EXISTS(kInflowBounds, inflow_bounds, other);
   CLONE_IF_EXISTS(kFrameSetLayoutData, frame_set_layout_data, other);
-  CLONE_IF_EXISTS(kGapGeometry, gap_geometry, other);
   SET_IF_EXISTS(kTableGridRect, table_grid_rect, other);
   CLONE_IF_EXISTS(kTableCollapsedBordersGeometry,
                   table_collapsed_borders_geometry, other);
@@ -152,7 +149,6 @@
     FUNC(kTableSectionRowOffsets, table_section_row_offsets);               \
     FUNC(kPageName, page_name);                                             \
     FUNC(kMargins, margins);                                                \
-    FUNC(kGapGeometry, gap_geometry);                                       \
   }
 
 #define CONSTRUCT_UNION_MEMBER(id, name) \
diff --git a/third_party/blink/renderer/core/layout/physical_fragment_rare_data.h b/third_party/blink/renderer/core/layout/physical_fragment_rare_data.h
index adae6cd3..6846d1c 100644
--- a/third_party/blink/renderer/core/layout/physical_fragment_rare_data.h
+++ b/third_party/blink/renderer/core/layout/physical_fragment_rare_data.h
@@ -51,6 +51,7 @@
     visitor->Trace(table_column_geometries_);
     visitor->Trace(mathml_paint_info_);
     visitor->Trace(reading_flow_nodes_);
+    visitor->Trace(gap_geometry_);
   }
 
  private:
@@ -74,9 +75,8 @@
     kTableSectionRowOffsets,
     kPageName,
     kMargins,
-    kGapGeometry,
 
-    kMaxValue = kGapGeometry,
+    kMaxValue = kMargins,
   };
   static_assert(sizeof(RareBitFieldType) * CHAR_BIT >
                     static_cast<unsigned>(FieldId::kMaxValue),
@@ -94,7 +94,6 @@
       scoped_refptr<const TableBorders> table_collapsed_borders;
       std::unique_ptr<TableFragmentData::CollapsedBordersGeometry>
           table_collapsed_borders_geometry;
-      std::unique_ptr<GapFragmentData::GapGeometry> gap_geometry;
       wtf_size_t table_cell_column_index;
       wtf_size_t table_section_start_row_index;
       Vector<LayoutUnit> table_section_row_offsets;
@@ -167,6 +166,7 @@
   Member<const TableFragmentData::ColumnGeometries> table_column_geometries_;
   Member<const MathMLPaintInfo> mathml_paint_info_;
   Member<const HeapVector<Member<Node>>> reading_flow_nodes_;
+  Member<const GapFragmentData::GapGeometry> gap_geometry_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/resource/image_resource.cc b/third_party/blink/renderer/core/loader/resource/image_resource.cc
index 19ef3da..f00866c 100644
--- a/third_party/blink/renderer/core/loader/resource/image_resource.cc
+++ b/third_party/blink/renderer/core/loader/resource/image_resource.cc
@@ -609,6 +609,10 @@
   return GetContent()->PriorityFromObservers();
 }
 
+bool ImageResource::HasNonDegenerateSizeForDecode() const {
+  return GetContent()->HasNonDegenerateSizeForDecode();
+}
+
 void ImageResource::OnePartInMultipartReceived(
     const ResourceResponse& response) {
   DCHECK(multipart_parser_);
diff --git a/third_party/blink/renderer/core/loader/resource/image_resource.h b/third_party/blink/renderer/core/loader/resource/image_resource.h
index 08fe4d13..63cdf26 100644
--- a/third_party/blink/renderer/core/loader/resource/image_resource.h
+++ b/third_party/blink/renderer/core/loader/resource/image_resource.h
@@ -101,6 +101,7 @@
   void UpdateResourceInfoFromObservers() override;
   std::pair<ResourcePriority, ResourcePriority> PriorityFromObservers()
       const override;
+  bool HasNonDegenerateSizeForDecode() const override;
 
   // MultipartImageResourceParser::Client
   void OnePartInMultipartReceived(const ResourceResponse&) final;
diff --git a/third_party/blink/renderer/core/loader/resource/image_resource_content.h b/third_party/blink/renderer/core/loader/resource/image_resource_content.h
index 51c25b4..9496c56 100644
--- a/third_party/blink/renderer/core/loader/resource/image_resource_content.h
+++ b/third_party/blink/renderer/core/loader/resource/image_resource_content.h
@@ -210,6 +210,11 @@
     return !observers_.empty() || !finished_observers_.empty();
   }
   bool CanBeSpeculativelyDecoded() const;
+  bool HasNonDegenerateSizeForDecode() const {
+    // If an observer has 0x0 size, we will not consider it for speculative
+    // decode.
+    return !cached_info_.max_size_.IsZero();
+  }
   ImageDecoder::CompressionFormat GetCompressionFormat() const;
 
   // Returns the number of bytes of image data which should be used for entropy
diff --git a/third_party/blink/renderer/core/paint/box_painter_base.cc b/third_party/blink/renderer/core/paint/box_painter_base.cc
index cb89d57..7eb0513 100644
--- a/third_party/blink/renderer/core/paint/box_painter_base.cc
+++ b/third_party/blink/renderer/core/paint/box_painter_base.cc
@@ -109,23 +109,47 @@
   }
 }
 
-bool CanCompositeBackgroundColorAnimation(Node* node) {
+Animation* GetCompositableBackgroundColorAnimation(Node* node) {
   Element* element = DynamicTo<Element>(node);
-  if (!element)
-    return false;
+  if (!element) {
+    return nullptr;
+  }
 
   BackgroundColorPaintImageGenerator* generator =
       GetBackgroundColorPaintImageGenerator(node->GetDocument());
   // The generator can be null in testing environment.
-  if (!generator)
-    return false;
+  if (!generator) {
+    return nullptr;
+  }
 
   Animation* animation = generator->GetAnimationIfCompositable(element);
-  if (!animation)
-    return false;
+  if (!animation) {
+    return nullptr;
+  }
 
-  return animation->CheckCanStartAnimationOnCompositor(nullptr) ==
-         CompositorAnimations::kNoFailure;
+  if (animation->CheckCanStartAnimationOnCompositor(nullptr) !=
+      CompositorAnimations::kNoFailure) {
+    return nullptr;
+  }
+
+  return animation;
+}
+
+void DowngradeBackgroundColorAnimation(Node* node) {
+  Element* element = To<Element>(node);
+  ElementAnimations* element_animations = element->GetElementAnimations();
+  for (auto& entry : element_animations->Animations()) {
+    Animation& animation = *entry.key;
+    if (animation.GetNativePaintWorkletReasons() &
+        static_cast<Animation::NativePaintWorkletReasons>(
+            Animation::NativePaintWorkletProperties::
+                kBackgroundColorPaintWorklet)) {
+      if (animation.HasActiveAnimationsOnCompositor()) {
+        animation.SetCompositorPending(
+            Animation::CompositorPendingReason::kPendingDowngrade);
+      }
+    }
+  }
 }
 
 CompositedPaintStatus CompositedBackgroundColorStatus(Node* node) {
@@ -740,7 +764,7 @@
     return false;
 
   CompositedPaintStatus status = CompositedBackgroundColorStatus(node);
-
+  Animation* animation = nullptr;
   switch (status) {
     case CompositedPaintStatus::kNoAnimation:
     case CompositedPaintStatus::kNotComposited:
@@ -750,18 +774,32 @@
 
     case CompositedPaintStatus::kNeedsRepaint:
     case CompositedPaintStatus::kComposited:
-      if (CanCompositeBackgroundColorAnimation(node)) {
+      animation = GetCompositableBackgroundColorAnimation(node);
+      if (animation) {
         SetHasNativeBackgroundPainter(node, true);
       } else {
         SetHasNativeBackgroundPainter(node, false);
+        // Typically, this branch is only reached for the kNeedsRepaint case;
+        // however, it can occur if a blur filter is introduced to an ancestor
+        // of the element being animated, which breaks eligibility for
+        // compositing.
+        if (status == CompositedPaintStatus::kComposited) {
+          // TODO(kevers): Investigate if fallback to main in this degenerate
+          // case can occur too late to prevent a rendering glitch.
+          DowngradeBackgroundColorAnimation(node);
+        }
         return false;
       }
+      break;
   }
 
   scoped_refptr<Image> paint_worklet_image =
       GetBGColorPaintWorkletImage(document, node, dest_rect.Rect().size());
-  if (!paint_worklet_image)
-    return false;
+  // We can fail to create a paint worklet image if missing a generator, which
+  // is possible in a testing environment; however, in this case we won't have
+  // a compositable animation and would have bailed earlier. At this stage,
+  // image creation must succeed.
+  CHECK(paint_worklet_image) << "Failed to create paint worklet image";
   gfx::RectF src_rect(dest_rect.Rect().size());
   context.DrawImageRRect(
       *paint_worklet_image, Image::kSyncDecode, ImageAutoDarkMode::Disabled(),
@@ -769,6 +807,7 @@
           /* image_may_be_lcp_candidate */ false,
           /* report_paint_timing */ false),
       dest_rect, src_rect, SkBlendMode::kSrcOver, kRespectImageOrientation);
+  animation->OnPaintWorkletImageCreated();
   return true;
 }
 
diff --git a/third_party/blink/renderer/core/paint/clip_path_clipper.cc b/third_party/blink/renderer/core/paint/clip_path_clipper.cc
index 5c37098..16f6d8b 100644
--- a/third_party/blink/renderer/core/paint/clip_path_clipper.cc
+++ b/third_party/blink/renderer/core/paint/clip_path_clipper.cc
@@ -178,16 +178,24 @@
   }
 }
 
-bool CanCompositeClipPathAnimation(const LayoutObject& layout_object) {
+Animation* GetCompositableClipPathAnimation(const LayoutObject& layout_object) {
   ClipPathPaintImageGenerator* generator =
       layout_object.GetFrame()->GetClipPathPaintImageGenerator();
   CHECK(generator);
 
   const Element* element = To<Element>(layout_object.GetNode());
-  const Animation* animation = generator->GetAnimationIfCompositable(element);
+  Animation* animation = generator->GetAnimationIfCompositable(element);
 
-  return animation && (animation->CheckCanStartAnimationOnCompositor(nullptr) ==
-                       CompositorAnimations::kNoFailure);
+  if (!animation) {
+    return nullptr;
+  }
+
+  if (animation->CheckCanStartAnimationOnCompositor(nullptr) !=
+      CompositorAnimations::kNoFailure) {
+    return nullptr;
+  }
+
+  return animation;
 }
 
 void PaintWorkletBasedClip(GraphicsContext& context,
@@ -269,10 +277,9 @@
 
   CompositedPaintStatus status =
       CompositeClipPathStatus(layout_object.GetNode());
-
   switch (status) {
     case CompositedPaintStatus::kComposited:
-      DCHECK(CanCompositeClipPathAnimation(layout_object));
+      DCHECK(GetCompositableClipPathAnimation(layout_object));
       return true;
     case CompositedPaintStatus::kNoAnimation:
     case CompositedPaintStatus::kNotComposited:
@@ -335,8 +342,8 @@
     return;
   }
 
-  SetCompositeClipPathStatus(layout_object.GetNode(),
-                             CanCompositeClipPathAnimation(layout_object));
+  Animation* animation = GetCompositableClipPathAnimation(layout_object);
+  SetCompositeClipPathStatus(layout_object.GetNode(), animation);
 }
 
 gfx::RectF ClipPathClipper::LocalReferenceBox(const LayoutObject& object) {
@@ -596,6 +603,10 @@
     }
 
     PaintWorkletBasedClip(context, layout_object, reference_box, layout_object);
+
+    Animation* animation = GetCompositableClipPathAnimation(layout_object);
+    CHECK(animation) << "Unable to find composited clip path animation";
+    animation->OnPaintWorkletImageCreated();
   } else {
     gfx::RectF reference_box = LocalReferenceBox(layout_object);
     bool is_first = true;
diff --git a/third_party/blink/renderer/core/preferences/preference_object.cc b/third_party/blink/renderer/core/preferences/preference_object.cc
index fe17b69..89ba663 100644
--- a/third_party/blink/renderer/core/preferences/preference_object.cc
+++ b/third_party/blink/renderer/core/preferences/preference_object.cc
@@ -6,6 +6,7 @@
 
 #include "third_party/blink/renderer/bindings/core/v8/frozen_array.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
+#include "third_party/blink/renderer/core/css/media_feature_names.h"
 #include "third_party/blink/renderer/core/css/media_values.h"
 #include "third_party/blink/renderer/core/css/media_values_cached.h"
 #include "third_party/blink/renderer/core/css/media_values_dynamic.h"
diff --git a/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.cc b/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.cc
index d4d3ea4..3dac52a 100644
--- a/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.cc
+++ b/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.cc
@@ -24,6 +24,7 @@
 #include "third_party/blink/renderer/core/layout/table/layout_table_row.h"
 #include "third_party/blink/renderer/core/layout/table/layout_table_section.h"
 #include "third_party/blink/renderer/core/style/computed_style.h"
+#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
 #include "ui/gfx/geometry/rect_conversions.h"
 
 namespace blink {
@@ -415,25 +416,58 @@
   Supplement<Document>::Trace(visitor);
 }
 
-void AIPageContentAgent::GetAIPageContent(GetAIPageContentCallback callback) {
-  std::move(callback).Run(GetAIPageContentSync());
+void AIPageContentAgent::GetAIPageContent(
+    mojom::blink::AIPageContentOptionsPtr request,
+    GetAIPageContentCallback callback) {
+  if (request->on_critical_path) {
+    GetAIPageContentSync(std::move(request), std::move(callback),
+                         base::TimeTicks());
+    return;
+  }
+
+  // Note: We could maintain a set of all pending requests to batch process them
+  // when the idle task runs. But it's better for this to be handled in the
+  // browser process.
+  //
+  // TODO(crbug.com/389117697): Consider running this after a frame is
+  // committed, in case that happens before the idle task runs.
+  ThreadScheduler::Current()->PostIdleTask(
+      FROM_HERE, WTF::BindOnce(&AIPageContentAgent::GetAIPageContentSync,
+                               WrapWeakPersistent(this), std::move(request),
+                               std::move(callback)));
 }
 
-mojom::blink::AIPageContentPtr AIPageContentAgent::GetAIPageContentSync()
-    const {
+void AIPageContentAgent::GetAIPageContentSync(
+    mojom::blink::AIPageContentOptionsPtr request,
+    GetAIPageContentCallback callback,
+    base::TimeTicks deadline) const {
+  std::move(callback).Run(GetAIPageContentInternal(request->include_geometry));
+}
+
+mojom::blink::AIPageContentPtr AIPageContentAgent::GetAIPageContentInternal(
+    bool include_geometry) const {
   LocalFrame* frame = GetSupplementable()->GetFrame();
   if (!frame || !frame->GetDocument() || !frame->GetDocument()->View()) {
     return nullptr;
   }
 
-  auto& document = *frame->GetDocument();
+  ContentBuilder builder(include_geometry);
+  return builder.Build(*frame);
+}
+
+AIPageContentAgent::ContentBuilder::ContentBuilder(bool include_geometry)
+    : include_geometry_(include_geometry) {}
+
+AIPageContentAgent::ContentBuilder::~ContentBuilder() = default;
+
+mojom::blink::AIPageContentPtr AIPageContentAgent::ContentBuilder::Build(
+    LocalFrame& frame) {
+  auto& document = *frame.GetDocument();
+
   mojom::blink::AIPageContentPtr page_content =
       mojom::blink::AIPageContent::New();
   const auto start_time = base::TimeTicks::Now();
 
-  // Disallow throttling so content from offscreen frames is updated.
-  LocalFrameView::DisallowThrottlingScope disallow_throttling(*document.View());
-
   // Force activatable locks so content which is accessible via find-in-page is
   // styled/laid out and included when walking the tree below.
   //
@@ -467,7 +501,7 @@
   page_content->root_node = std::move(root_node);
 
   const auto latency = base::TimeTicks::Now() - start_time;
-  if (frame->IsOutermostMainFrame()) {
+  if (frame.IsOutermostMainFrame()) {
     UMA_HISTOGRAM_TIMES(
         "OptimizationGuide.AIPageContent.RendererLatency.MainFrame", latency);
   } else {
@@ -479,7 +513,7 @@
   return page_content;
 }
 
-bool AIPageContentAgent::WalkChildren(
+bool AIPageContentAgent::ContentBuilder::WalkChildren(
     const LayoutObject& object,
     mojom::blink::AIPageContentNode& content_node,
     const ComputedStyle& document_style) const {
@@ -520,7 +554,7 @@
   return has_visible_content;
 }
 
-void AIPageContentAgent::ProcessIframe(
+void AIPageContentAgent::ContentBuilder::ProcessIframe(
     const LayoutIFrame& object,
     mojom::blink::AIPageContentNode& content_node) const {
   CHECK(IsVisible(object));
@@ -554,7 +588,8 @@
   }
 }
 
-mojom::blink::AIPageContentNodePtr AIPageContentAgent::MaybeGenerateContentNode(
+mojom::blink::AIPageContentNodePtr
+AIPageContentAgent::ContentBuilder::MaybeGenerateContentNode(
     const LayoutObject& object,
     const ComputedStyle& document_style) const {
   auto content_node = mojom::blink::AIPageContentNode::New();
@@ -632,13 +667,12 @@
     attributes.common_ancestor_dom_node_id = *node_id;
   }
 
-  attributes.geometry = mojom::blink::AIPageContentGeometry::New();
-  AddNodeGeometry(object, *attributes.geometry);
+  AddNodeGeometry(object, attributes);
 
   return content_node;
 }
 
-std::optional<DOMNodeId> AIPageContentAgent::AddNodeId(
+std::optional<DOMNodeId> AIPageContentAgent::ContentBuilder::AddNodeId(
     const LayoutObject& object,
     mojom::blink::AIPageContentAttributes& attributes) const {
   if (auto node_id = GetNodeId(object)) {
@@ -649,9 +683,16 @@
   return std::nullopt;
 }
 
-void AIPageContentAgent::AddNodeGeometry(
+void AIPageContentAgent::ContentBuilder::AddNodeGeometry(
     const LayoutObject& object,
-    mojom::blink::AIPageContentGeometry& geometry) const {
+    mojom::blink::AIPageContentAttributes& attributes) const {
+  if (!include_geometry_) {
+    return;
+  }
+
+  attributes.geometry = mojom::blink::AIPageContentGeometry::New();
+  auto& geometry = *attributes.geometry;
+
   geometry.outer_bounding_box =
       object.AbsoluteBoundingBoxRect(kMapCoordinatesFlags);
 
diff --git a/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.h b/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.h
index b78f3d3..4343786 100644
--- a/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.h
+++ b/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent.h
@@ -43,28 +43,48 @@
   void Trace(Visitor* visitor) const override;
 
   // mojom::blink::AIPageContentAgent overrides.
-  void GetAIPageContent(GetAIPageContentCallback callback) override;
+  void GetAIPageContent(mojom::blink::AIPageContentOptionsPtr request,
+                        GetAIPageContentCallback callback) override;
 
-  mojom::blink::AIPageContentPtr GetAIPageContentSync() const;
+  // public for testing.
+  mojom::blink::AIPageContentPtr GetAIPageContentInternal(
+      bool include_geometry) const;
 
  private:
-  void Bind(mojo::PendingReceiver<mojom::blink::AIPageContentAgent> receiver);
+  void GetAIPageContentSync(mojom::blink::AIPageContentOptionsPtr request,
+                            GetAIPageContentCallback callback,
+                            base::TimeTicks deadline) const;
 
-  // Returns true if any descendant of `object` has a computed value of
-  // visible for `visibility`.
-  bool WalkChildren(const LayoutObject& object,
-                    mojom::blink::AIPageContentNode& content_node,
-                    const ComputedStyle& document_style) const;
-  void ProcessIframe(const LayoutIFrame& object,
-                     mojom::blink::AIPageContentNode& content_node) const;
-  mojom::blink::AIPageContentNodePtr MaybeGenerateContentNode(
-      const LayoutObject& object,
-      const ComputedStyle& document_style) const;
-  std::optional<DOMNodeId> AddNodeId(
-      const LayoutObject& object,
-      mojom::blink::AIPageContentAttributes& attributes) const;
-  void AddNodeGeometry(const LayoutObject& object,
-                       mojom::blink::AIPageContentGeometry& geometry) const;
+  // Synchronously services a single request.
+  class ContentBuilder {
+   public:
+    explicit ContentBuilder(bool include_geometry);
+    ~ContentBuilder();
+
+    mojom::blink::AIPageContentPtr Build(LocalFrame& frame);
+
+   private:
+    // Returns true if any descendant of `object` has a computed value of
+    // visible for `visibility`.
+    bool WalkChildren(const LayoutObject& object,
+                      mojom::blink::AIPageContentNode& content_node,
+                      const ComputedStyle& document_style) const;
+    void ProcessIframe(const LayoutIFrame& object,
+                       mojom::blink::AIPageContentNode& content_node) const;
+    mojom::blink::AIPageContentNodePtr MaybeGenerateContentNode(
+        const LayoutObject& object,
+        const ComputedStyle& document_style) const;
+    std::optional<DOMNodeId> AddNodeId(
+        const LayoutObject& object,
+        mojom::blink::AIPageContentAttributes& attributes) const;
+    void AddNodeGeometry(
+        const LayoutObject& object,
+        mojom::blink::AIPageContentAttributes& attributes) const;
+
+    const bool include_geometry_ = true;
+  };
+
+  void Bind(mojo::PendingReceiver<mojom::blink::AIPageContentAgent> receiver);
 
   HeapMojoReceiverSet<mojom::blink::AIPageContentAgent, AIPageContentAgent>
       receiver_set_;
diff --git a/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent_unittest.cc b/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent_unittest.cc
index be20f99..70ac7f96 100644
--- a/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent_unittest.cc
+++ b/third_party/blink/renderer/modules/content_extraction/ai_page_content_agent_unittest.cc
@@ -214,6 +214,15 @@
         << ", expected: " << expected.ToString();
   }
 
+  mojom::blink::AIPageContentPtr GetAIPageContent(
+      bool include_geometry = true) {
+    auto* agent = AIPageContentAgent::GetOrCreateForTesting(
+        *helper_.LocalMainFrame()->GetFrame()->GetDocument());
+    EXPECT_TRUE(agent);
+
+    return agent->GetAIPageContentInternal(include_geometry);
+  }
+
  protected:
   test::TaskEnvironment task_environment_;
   frame_test_helpers::WebViewHelper helper_;
@@ -234,11 +243,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -284,10 +289,7 @@
       ->item(0)
       ->setAttribute(html_names::kSrcAttr, AtomicString(kSmallImage));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(document);
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -316,14 +318,12 @@
                          "</body>",
                          kSmallImage),
       url_test_helpers::ToKURL("http://foobar.com"));
-  auto& document = *helper_.LocalMainFrame()->GetFrame()->GetDocument();
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(document);
-  auto page_content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
 
   mojom::blink::AIPageContentPtr output;
   ASSERT_TRUE(mojo::test::SerializeAndDeserialize<mojom::blink::AIPageContent>(
-      page_content, output));
+      content, output));
 }
 
 TEST_F(AIPageContentAgentTest, Headings) {
@@ -336,11 +336,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -384,11 +380,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -426,11 +418,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -480,11 +468,7 @@
 
   iframe_doc->body()->setInnerHTML("<body>inside iframe</body>");
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -509,11 +493,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -529,11 +509,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -553,11 +529,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -611,11 +583,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -684,11 +652,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -797,11 +761,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -900,11 +860,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -969,11 +925,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1055,11 +1007,7 @@
       "     </body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1149,11 +1097,7 @@
       "     </body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1228,11 +1172,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1265,11 +1205,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1308,11 +1244,7 @@
       "</html>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1343,11 +1275,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1376,11 +1304,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1412,11 +1336,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1476,11 +1396,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1524,11 +1440,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1571,11 +1483,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1609,11 +1517,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1637,11 +1541,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1670,11 +1570,7 @@
       "</body>",
       url_test_helpers::ToKURL("http://foobar.com"));
 
-  auto* agent = AIPageContentAgent::GetOrCreateForTesting(
-      *helper_.LocalMainFrame()->GetFrame()->GetDocument());
-  ASSERT_TRUE(agent);
-
-  auto content = agent->GetAIPageContentSync();
+  auto content = GetAIPageContent();
   ASSERT_TRUE(content);
   ASSERT_TRUE(content->root_node);
 
@@ -1682,5 +1578,24 @@
   EXPECT_EQ(root.children_nodes.size(), 0u);
 }
 
+TEST_F(AIPageContentAgentTest, NoGeometry) {
+  frame_test_helpers::LoadHTMLString(
+      helper_.LocalMainFrame(),
+      "<body>"
+      "  <div>text</div>"
+      "</body>",
+      url_test_helpers::ToKURL("http://foobar.com"));
+
+  auto content = GetAIPageContent(/*include_geometry=*/false);
+  ASSERT_TRUE(content);
+  ASSERT_TRUE(content->root_node);
+  EXPECT_FALSE(content->root_node->content_attributes->geometry);
+
+  EXPECT_EQ(content->root_node->children_nodes.size(), 1u);
+  const auto& text_node = *content->root_node->children_nodes[0];
+  CheckTextNode(text_node, "text");
+  EXPECT_FALSE(text_node.content_attributes->geometry);
+}
+
 }  // namespace
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/csspaint/nativepaint/background_color_paint_definition_test.cc b/third_party/blink/renderer/modules/csspaint/nativepaint/background_color_paint_definition_test.cc
index a9d78d6..11440fe 100644
--- a/third_party/blink/renderer/modules/csspaint/nativepaint/background_color_paint_definition_test.cc
+++ b/third_party/blink/renderer/modules/csspaint/nativepaint/background_color_paint_definition_test.cc
@@ -40,6 +40,8 @@
 
   scoped_refptr<Image> Paint(const gfx::SizeF& container_size,
                              const Node* node) override {
+    LayoutObject* layout_object = node->GetLayoutObject();
+    layout_object->GetMutableForPainting().FirstFragment().EnsureId();
     return BitmapImage::Create();
   }
 
@@ -217,6 +219,7 @@
   element_animations = element->GetElementAnimations();
   EXPECT_EQ(element_animations->CompositedBackgroundColorStatus(),
             ElementAnimations::CompositedPaintStatus::kNotComposited);
+  EXPECT_FALSE(animation->HasActiveAnimationsOnCompositor());
 
   // Reset.
   inline_style->setProperty(
@@ -235,6 +238,7 @@
   element_animations = element->GetElementAnimations();
   EXPECT_EQ(element_animations->CompositedBackgroundColorStatus(),
             ElementAnimations::CompositedPaintStatus::kComposited);
+  EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor());
 
   // Add blur to grandparent.
   Element* grandparent = GetElementById("grandparent");
@@ -249,6 +253,7 @@
   element_animations = element->GetElementAnimations();
   EXPECT_EQ(element_animations->CompositedBackgroundColorStatus(),
             ElementAnimations::CompositedPaintStatus::kNotComposited);
+  EXPECT_FALSE(animation->HasActiveAnimationsOnCompositor());
 }
 
 // Test the case when there is no animation attached to the element.
@@ -1008,4 +1013,42 @@
   RunPaintForTest(animated_colors, offsets, property_values);
 }
 
+TEST_F(BackgroundColorPaintDefinitionTest, OffscreenToOnScreen) {
+  SetBodyInnerHTML(R"HTML(
+    <style>
+      @keyframes colorize {
+        from { background-color: red }
+        to { background-color: green; }
+      }
+      #target {
+        animation: colorize 1s forwards;
+        height: 100px;
+        width: 100px;
+      }
+      #spacer {
+        height: 5000px;
+      }
+    }
+    </style>
+    <div id = "spacer"></div>
+    <div id ="target" style="width: 100px; height: 100px"></div>
+  )HTML");
+  UpdateAllLifecyclePhasesForTest();
+  Element* element = GetElementById("target");
+  EXPECT_TRUE(element->GetElementAnimations());
+  EXPECT_EQ(element->GetElementAnimations()->Animations().size(), 1u);
+  EXPECT_EQ(element->GetElementAnimations()->CompositedBackgroundColorStatus(),
+            ElementAnimations::CompositedPaintStatus::kNeedsRepaint);
+  Animation* animation =
+      element->GetElementAnimations()->Animations().begin()->key;
+  EXPECT_FALSE(animation->HasActiveAnimationsOnCompositor());
+
+  GetElementById("spacer")->remove();
+  UpdateAllLifecyclePhasesForTest();
+
+  EXPECT_EQ(element->GetElementAnimations()->CompositedBackgroundColorStatus(),
+            ElementAnimations::CompositedPaintStatus::kComposited);
+  EXPECT_TRUE(animation->HasActiveAnimationsOnCompositor());
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/csspaint/nativepaint/clip_path_paint_definition_test.cc b/third_party/blink/renderer/modules/csspaint/nativepaint/clip_path_paint_definition_test.cc
index bb9b068..d2be80c 100644
--- a/third_party/blink/renderer/modules/csspaint/nativepaint/clip_path_paint_definition_test.cc
+++ b/third_party/blink/renderer/modules/csspaint/nativepaint/clip_path_paint_definition_test.cc
@@ -18,6 +18,7 @@
 #include "third_party/blink/renderer/core/frame/settings.h"
 #include "third_party/blink/renderer/core/page/page_animator.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/platform/graphics/bitmap_image.h"
 #include "third_party/blink/renderer/platform/graphics/image.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 
@@ -25,6 +26,26 @@
 
 using CompositedPaintStatus = ElementAnimations::CompositedPaintStatus;
 
+class FakeClipPathPaintImageGenerator : public ClipPathPaintImageGenerator {
+ public:
+  FakeClipPathPaintImageGenerator() = default;
+
+  scoped_refptr<Image> Paint(float zoom,
+                             const gfx::RectF& reference_box,
+                             const gfx::SizeF& clip_area_size,
+                             const Node& node) override {
+    LayoutObject* layout_object = node.GetLayoutObject();
+    layout_object->GetMutableForPainting().FirstFragment().EnsureId();
+    return BitmapImage::Create();
+  }
+
+  Animation* GetAnimationIfCompositable(const Element* element) override {
+    return ClipPathPaintDefinition::GetAnimationIfCompositable(element);
+  }
+
+  void Shutdown() override {}
+};
+
 class ClipPathPaintDefinitionTest : public PageTestBase {
  public:
   ClipPathPaintDefinitionTest() = default;
@@ -38,6 +59,11 @@
         std::make_unique<ScopedCompositeBGColorAnimationForTest>(false);
     PageTestBase::SetUp();
     GetDocument().GetSettings()->SetAcceleratedCompositingEnabled(true);
+
+    FakeClipPathPaintImageGenerator* generator =
+        MakeGarbageCollected<FakeClipPathPaintImageGenerator>();
+    GetDocument().GetFrame()->SetClipPathPaintImageGeneratorForTesting(
+        generator);
   }
 
  private:
diff --git a/third_party/blink/renderer/platform/bindings/parkable_string.cc b/third_party/blink/renderer/platform/bindings/parkable_string.cc
index 1cdfcac..f1fdfa07 100644
--- a/third_party/blink/renderer/platform/bindings/parkable_string.cc
+++ b/third_party/blink/renderer/platform/bindings/parkable_string.cc
@@ -687,11 +687,11 @@
 
   switch (GetCompressionAlgorithm()) {
     case CompressionAlgorithm::kZlib: {
-      const auto uncompressed_string_piece = base::as_string_view(chars);
+      const auto uncompressed_span = chars;
       // If the buffer size is incorrect, then we have a corrupted data issue,
       // and in such case there is nothing else to do than crash.
       CHECK_EQ(compression::GetUncompressedSize(compressed_string_piece),
-               uncompressed_string_piece.size());
+               uncompressed_span.size());
       // If decompression fails, this is either because:
       // 1. Compressed data is corrupted
       // 2. Cannot allocate memory in zlib
@@ -699,11 +699,11 @@
       // (1) is data corruption, and (2) is OOM. In all cases, we cannot
       // recover the string we need, nothing else to do than to abort.
       if (!compression::GzipUncompress(compressed_string_piece,
-                                       uncompressed_string_piece)) {
+                                       uncompressed_span)) {
         // Since this is almost always OOM, report it as such. We don't have
         // certainty, but memory corruption should be much rarer, and could make
         // us crash anywhere else.
-        OOM_CRASH(uncompressed_string_piece.size());
+        OOM_CRASH(uncompressed_span.size());
       }
       break;
     }
diff --git a/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.filelist b/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.filelist
index 9354c828..7b23015 100644
--- a/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.filelist
+++ b/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.filelist
@@ -5,6 +5,13 @@
 #       If it requires updating, you should get a presubmit error with
 #       instructions on how to regenerate. Otherwise, do not edit.
 ../../web_tests/external/wpt/css/css-fonts/resources/COLR-palettes-test-font.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf
 ../../web_tests/external/wpt/fonts/AD.woff
 ../../web_tests/external/wpt/fonts/Ahem.ttf
 ../../web_tests/external/wpt/fonts/CanvasTest-ascent256.ttf
diff --git a/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.globlist b/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.globlist
index 8800a7e..9c040b7e 100644
--- a/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.globlist
+++ b/third_party/blink/renderer/platform/blink_platform_unittests_bundle_data.globlist
@@ -10,6 +10,7 @@
 # machinery to ../../webtests_external/wpt, we make an exception here.
 # push(ignore-relative)
 ../../web_tests/external/wpt/css/css-fonts/resources/COLR-palettes-test-font.ttf
+../../web_tests/external/wpt/css/css-fonts/resources/vs/*.ttf
 ../../web_tests/external/wpt/fonts/**/*.otf
 ../../web_tests/external/wpt/fonts/**/*.ttf
 # List TTF as well since globing is case sensitive on most platforms.
diff --git a/third_party/blink/renderer/platform/graphics/gpu/image_layer_bridge.h b/third_party/blink/renderer/platform/graphics/gpu/image_layer_bridge.h
index f46dae7..60894053 100644
--- a/third_party/blink/renderer/platform/graphics/gpu/image_layer_bridge.h
+++ b/third_party/blink/renderer/platform/graphics/gpu/image_layer_bridge.h
@@ -53,8 +53,6 @@
   void Trace(Visitor* visitor) const {}
 
  private:
-  // SharedMemory bitmap that was registered with SharedBitmapIdRegistrar. Used
-  // only with software compositing.
   struct RegisteredBitmap {
     RegisteredBitmap();
     RegisteredBitmap(RegisteredBitmap&& other);
diff --git a/third_party/blink/renderer/platform/graphics/web_graphics_context_3d_video_frame_pool.cc b/third_party/blink/renderer/platform/graphics/web_graphics_context_3d_video_frame_pool.cc
index 996ca62..63cdab1 100644
--- a/third_party/blink/renderer/platform/graphics/web_graphics_context_3d_video_frame_pool.cc
+++ b/third_party/blink/renderer/platform/graphics/web_graphics_context_3d_video_frame_pool.cc
@@ -369,7 +369,8 @@
 
 BASE_FEATURE(kGpuMemoryBufferReadbackFromTexture,
              "GpuMemoryBufferReadbackFromTexture",
-#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS) || \
+    BUILDFLAG(IS_LINUX)
              base::FEATURE_ENABLED_BY_DEFAULT
 #else
              base::FEATURE_DISABLED_BY_DEFAULT
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource.h b/third_party/blink/renderer/platform/loader/fetch/resource.h
index 017dc6b7..bba34a90 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource.h
+++ b/third_party/blink/renderer/platform/loader/fetch/resource.h
@@ -213,6 +213,8 @@
     return std::make_pair(ResourcePriority(), ResourcePriority());
   }
 
+  virtual bool HasNonDegenerateSizeForDecode() const { return false; }
+
   // If this Resource is already finished when AddClient is called, the
   // ResourceClient will be notified asynchronously by a task scheduled
   // on the given base::SingleThreadTaskRunner. Otherwise, the given
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
index a3fb7fa..f06d9b7 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
@@ -366,12 +366,13 @@
   return a.intra_priority_value - b.intra_priority_value;
 }
 
-Resource* PopHighestPriorityVisibleResource(
+Resource* PopHighestPriorityDecodableResource(
     HeapHashSet<WeakMember<Resource>>& resources) {
   Resource* result = nullptr;
   for (Resource* resource : resources) {
     const ResourcePriority& priority = resource->PriorityFromObservers().first;
-    if (priority.visibility != ResourcePriority::kVisible) {
+    if (priority.visibility != ResourcePriority::kVisible ||
+        !resource->HasNonDegenerateSizeForDecode()) {
       continue;
     }
     if (!result || CompareResourcePriorities(
@@ -3186,8 +3187,8 @@
   }
   // Find the highest priority image to decode.
   while (true) {
-    Resource* image_to_decode =
-        PopHighestPriorityVisibleResource(speculative_decode_candidate_images_);
+    Resource* image_to_decode = PopHighestPriorityDecodableResource(
+        speculative_decode_candidate_images_);
     if (!image_to_decode) {
       break;
     }
diff --git a/third_party/blink/renderer/platform/scheduler/common/features.cc b/third_party/blink/renderer/platform/scheduler/common/features.cc
index 91023b54..3d892f0 100644
--- a/third_party/blink/renderer/platform/scheduler/common/features.cc
+++ b/third_party/blink/renderer/platform/scheduler/common/features.cc
@@ -121,6 +121,9 @@
              "PrioritizeCompositingAfterDelayTrials",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kThrottleTimedOutIdleTasks,
+             "ThrottleTimedOutIdleTasks",
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 }  // namespace scheduler
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/scheduler/common/features.h b/third_party/blink/renderer/platform/scheduler/common/features.h
index 6cfca765..4382ffe 100644
--- a/third_party/blink/renderer/platform/scheduler/common/features.h
+++ b/third_party/blink/renderer/platform/scheduler/common/features.h
@@ -81,6 +81,9 @@
 // returns TimeDelta::Max().
 PLATFORM_EXPORT base::TimeDelta GetThreadedScrollRenderingStarvationThreshold();
 
+// Kill switch for throttling timed-out requestIdleCallback tasks.
+PLATFORM_EXPORT BASE_DECLARE_FEATURE(kThrottleTimedOutIdleTasks);
+
 }  // namespace scheduler
 }  // namespace blink
 
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
index 79fcaad2..740d5cc8 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
@@ -478,6 +478,18 @@
       return DeferrableTaskQueueTraits().SetPrioritisationType(
           QueueTraits::PrioritisationType::kJavaScriptTimer);
     }
+    case TaskType::kIdleTask:
+      // This type is used for timed-out idle tasks, which essentially become
+      // timers in the background after we stop running idle tasks or if the
+      // timeout is less than the idle period duration. These tasks should be
+      // throttled similar to other timers to prevent creating non-throttleable
+      // timers.
+      return DeferrableTaskQueueTraits()
+          .SetCanBeThrottled(
+              base::FeatureList::IsEnabled(kThrottleTimedOutIdleTasks))
+          .SetCanBeIntensivelyThrottled(
+              base::FeatureList::IsEnabled(kThrottleTimedOutIdleTasks) &&
+              IsIntensiveWakeUpThrottlingEnabled());
     case TaskType::kInternalLoading:
     case TaskType::kNetworking:
       return LoadingTaskQueueTraits();
@@ -517,7 +529,6 @@
     case TaskType::kPerformanceTimeline:
     case TaskType::kWebGL:
     case TaskType::kWebGPU:
-    case TaskType::kIdleTask:
     case TaskType::kInternalDefault:
     case TaskType::kMiscPlatformAPI:
     case TaskType::kFontLoading:
@@ -1358,7 +1369,8 @@
       frame_task_queue_controller_->NewWebSchedulingTaskQueue(
           DeferrableTaskQueueTraits()
               .SetCanBeThrottled(true)
-              .SetCanBeIntensivelyThrottled(true)
+              .SetCanBeIntensivelyThrottled(
+                  IsIntensiveWakeUpThrottlingEnabled())
               .SetCanBeDeferredForRendering(can_be_deferred_for_rendering),
           queue_type, priority);
   return std::make_unique<MainThreadWebSchedulingTaskQueueImpl>(
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc
index f237f1a..3d749a2 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc
@@ -1532,10 +1532,11 @@
                  << "TaskType is "
                  << TaskTypeNames::TaskTypeToString(task_type));
     switch (task_type) {
+      case TaskType::kIdleTask:
       case TaskType::kInternalContentCapture:
+      case TaskType::kInternalTranslation:
       case TaskType::kJavascriptTimerDelayedLowNesting:
       case TaskType::kJavascriptTimerDelayedHighNesting:
-      case TaskType::kInternalTranslation:
         EXPECT_TRUE(IsTaskTypeThrottled(task_type));
         break;
       default:
@@ -2896,6 +2897,9 @@
             /* task_type=*/TaskType::kJavascriptTimerDelayedHighNesting,
             /* is_intensive_throttling_expected=*/true},
         IntensiveWakeUpThrottlingTestParam{
+            /* task_type=*/TaskType::kIdleTask,
+            /* is_intensive_throttling_expected=*/true},
+        IntensiveWakeUpThrottlingTestParam{
             /* task_type=*/TaskType::kWebSchedulingPostedTask,
             /* is_intensive_throttling_expected=*/true}),
     [](const testing::TestParamInfo<IntensiveWakeUpThrottlingTestParam>& info) {
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc
index 6b0ee2c..1a89255 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc
@@ -642,6 +642,11 @@
         TaskQueue::InsertFencePosition::kNow);
   }
 
+  // The intensive throttling policy should affect all task queues, epecially
+  // anything web visible.
+  CHECK(!task_queue->CanBeIntensivelyThrottled() ||
+        IsIntensiveWakeUpThrottlingEnabled());
+
   return task_queue;
 }
 
diff --git a/third_party/blink/renderer/platform/widget/widget_base.cc b/third_party/blink/renderer/platform/widget/widget_base.cc
index e558cd72..a3ace153 100644
--- a/third_party/blink/renderer/platform/widget/widget_base.cc
+++ b/third_party/blink/renderer/platform/widget/widget_base.cc
@@ -504,9 +504,6 @@
   // Web tests can override the device scale factor in the renderer.
   if (auto scale_factor = client_->GetTestingDeviceScaleFactorOverride()) {
     screen_info.device_scale_factor = scale_factor;
-    visual_properties.compositor_viewport_pixel_rect =
-        gfx::Rect(gfx::ScaleToCeiledSize(visual_properties.new_size,
-                                         screen_info.device_scale_factor));
   }
 
   // Inform the rendering thread of the color space indicating the presence of
diff --git a/third_party/blink/renderer/platform/widget/widget_base.h b/third_party/blink/renderer/platform/widget/widget_base.h
index dbd53ba9..37bdfbd 100644
--- a/third_party/blink/renderer/platform/widget/widget_base.h
+++ b/third_party/blink/renderer/platform/widget/widget_base.h
@@ -405,6 +405,9 @@
 
   void OnDevToolsSessionConnectionChanged(bool attached);
 
+  // Helper to get the non-emulated device scale factor.
+  float GetOriginalDeviceScaleFactor() const;
+
  private:
   static void AssertAreCompatible(const WidgetBase& a, const WidgetBase& b);
 
@@ -431,9 +434,6 @@
   // Called after the delay given in `RequestAnimationAfterDelay()`.
   void RequestAnimationAfterDelayTimerFired(TimerBase*);
 
-  // Helper to get the non-emulated device scale factor.
-  float GetOriginalDeviceScaleFactor() const;
-
   // Finishes the call to RequestNewLayerTreeFrameSink() once the
   // |gpu_channel_host| is available.
   // TODO(crbug.com/1278147): Clean up these parameters using either a struct or
diff --git a/third_party/blink/web_tests/MSANExpectations b/third_party/blink/web_tests/MSANExpectations
index d8b3a1cc..0e1c76c 100644
--- a/third_party/blink/web_tests/MSANExpectations
+++ b/third_party/blink/web_tests/MSANExpectations
@@ -137,8 +137,8 @@
 
 # Gardener 2025-01-08
 crbug.com/388428511 [ Linux ] external/wpt/webrtc/protocol/crypto-suite.https.html [ Failure Pass ]
-crbug.com/388428511 [ Linux ] external/wpt/wasm/core/simd/simd_f32x4_pmin_pmax.wast.js.html [ Failure Pass ]
-crbug.com/388428511 [ Linux ] external/wpt/wasm/core/simd/simd_f64x2_pmin_pmax.wast.js.html [ Failure Pass ]
+crbug.com/388428511 [ Linux ] external/wpt/wasm/core/simd/simd_f32x4_pmin_pmax.wast.js.html [ Crash Failure Pass ]
+crbug.com/388428511 [ Linux ] external/wpt/wasm/core/simd/simd_f64x2_pmin_pmax.wast.js.html [ Crash Failure Pass ]
 
 # Gardener 2025-01-10
 # Very slow under MSAN. Similar to crbug.com/40948871.
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 4add0d8..5200f6ea 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -1424,20 +1424,6 @@
     "expires": "Jul 1, 2023"
   },
   {
-    "prefix": "fledge-two-seller-worklet-threads",
-    "platforms": ["Linux", "Mac", "Win"],
-    "owners": ["behamilton@google.com", "caraitto@chromium.org", "pauljensen@chromium.org", "mmenke@chromium.org", "morlovich@chromium.org", "qingxinwu@google.com"],
-    "bases": [
-      "http/tests/inspector-protocol/target/auto-attach-auction-worklet.js",
-      "http/tests/inspector-protocol/target/target-auction-worklet.js"
-    ],
-    "exclusive_tests": "ALL",
-    "args": [
-      "--enable-features=FledgeSellerWorkletThreadPool:seller_worklet_thread_pool_size/2"
-    ],
-    "expires": "Nov 15, 2024"
-  },
-  {
     "prefix": "fledge-kanon-status-below-threshold",
     "platforms": ["Linux", "Mac", "Win"],
     "owners": ["behamilton@google.com", "caraitto@chromium.org", "pauljensen@chromium.org", "mmenke@chromium.org", "morlovich@chromium.org", "qingxinwu@google.com"],
@@ -1469,7 +1455,7 @@
     "args": [
       "--enable-features=EnableBandATriggeredUpdates,FledgeBiddingAndAuctionServer:FledgeBiddingAndAuctionKeyConfig/{\"https%3A%2F%2Fweb-platform.test%22%3A%22https%3A%2F%2Fweb-platform.test%3A8444%2Ffledge%2Ftentative%2Fresources%2Fba-public-keys\"}"
     ],
-    "expires": "Jan 14, 2025"
+    "expires": "Mar 14, 2025"
   },
   {
     "prefix": "fledge-bidding-and-auction-kanonymity",
@@ -1490,7 +1476,7 @@
       "--enable-features=FledgeConsiderKAnonymity,FledgeEnforceKAnonymity,EnableBandAKAnonEnforcement,EnableBandADealSupport,EnableBandATriggeredUpdates,FledgeBiddingAndAuctionServer:FledgeBiddingAndAuctionKeyConfig/{\"https%3A%2F%2Fweb-platform.test%22%3A%22https%3A%2F%2Fweb-platform.test%3A8444%2Ffledge%2Ftentative%2Fresources%2Fba-public-keys\"}",
       "--disable-features=CookieDeprecationFacilitatedTesting"
     ],
-    "expires": "Jan 14, 2025"
+    "expires": "Mar 14, 2025"
   },
   {
     "prefix": "async-script-scheduling-disabled",
@@ -1653,7 +1639,7 @@
     ],
     "args": ["--enable-features=PrivacySandboxAdsAPIsOverride,Fledge,AdInterestGroupAPI,FencedFramesEnforceFocus,FencedFramesLocalUnpartitionedDataAccess,ExemptUrlFromNetworkRevocationForTesting,FencedFramesCrossOriginEventReportingAllTraffic,FencedFramesReportEventHeaderChanges,FencedFramesSrcPermissionsPolicy,FencedFramesCrossOriginAutomaticBeaconData",
              "--enable-blink-features=FencedFramesDefaultMode"],
-    "expires": "May 1, 2025"
+    "expires": "Jan 9, 2026"
   },
   "This is the same as the 'fenced-frame-mparch' test suite, but this suite",
   "exposes debug behavior that isn't implemented in WebDriver yet.",
@@ -1672,7 +1658,7 @@
       "wpt_internal/fenced_frame"
     ],
     "args": ["--enable-features=PrivacySandboxAdsAPIsOverride,Fledge,AdInterestGroupAPI,FencedFramesEnforceFocus,FencedFramesLocalUnpartitionedDataAccess,ExemptUrlFromNetworkRevocationForTesting,FencedFramesCrossOriginEventReportingAllTraffic,FencedFramesReportEventHeaderChanges,FencedFramesDefaultMode"],
-    "expires": "Jan 9, 2025"
+    "expires": "Jan 9, 2026"
   },
   {
     "prefix": "cors-non-wildcard-request-headers",
diff --git a/third_party/blink/web_tests/virtual/fledge-two-seller-worklet-threads/DIR_METADATA b/third_party/blink/web_tests/virtual/fledge-two-seller-worklet-threads/DIR_METADATA
deleted file mode 100644
index f5c5a69..0000000
--- a/third_party/blink/web_tests/virtual/fledge-two-seller-worklet-threads/DIR_METADATA
+++ /dev/null
@@ -1,6 +0,0 @@
-monorail: {
-  component: "Chromium > Blink > InterestGroups"
-}
-buganizer_public: {
-  component_id: 1456359
-}
\ No newline at end of file
diff --git a/third_party/blink/web_tests/virtual/fledge-two-seller-worklet-threads/README.md b/third_party/blink/web_tests/virtual/fledge-two-seller-worklet-threads/README.md
deleted file mode 100644
index c339a00..0000000
--- a/third_party/blink/web_tests/virtual/fledge-two-seller-worklet-threads/README.md
+++ /dev/null
@@ -1 +0,0 @@
-This directory is to test Protected Audience functions with two seller worklet threads.
diff --git a/third_party/boringssl/src b/third_party/boringssl/src
index 574fd72..7db3433 160000
--- a/third_party/boringssl/src
+++ b/third_party/boringssl/src
@@ -1 +1 @@
-Subproject commit 574fd72ebc0a15c4e54066e368c7834e6f86205d
+Subproject commit 7db3433bd4466b20ade77494cd3bb03396441aef
diff --git a/third_party/catapult b/third_party/catapult
index c4c972c..580dbb7 160000
--- a/third_party/catapult
+++ b/third_party/catapult
@@ -1 +1 @@
-Subproject commit c4c972c5f0bebc45d077172aa42d7ba98f3abbbb
+Subproject commit 580dbb72d8984e972a02874d06ace7b13295798a
diff --git a/third_party/chromium-variations b/third_party/chromium-variations
index 91db0dd..8010e7f 160000
--- a/third_party/chromium-variations
+++ b/third_party/chromium-variations
@@ -1 +1 @@
-Subproject commit 91db0dddd20500f927076d7309ab54d3b3909344
+Subproject commit 8010e7fb8e1d64f2c914da216cedf6e3cdfad093
diff --git a/third_party/depot_tools b/third_party/depot_tools
index a912cd2..b576ab3 160000
--- a/third_party/depot_tools
+++ b/third_party/depot_tools
@@ -1 +1 @@
-Subproject commit a912cd245b093ea57a187ca66665ca98090a82fd
+Subproject commit b576ab3b78a9d19c33060c821d4f11643397fa30
diff --git a/third_party/freetype/README.chromium b/third_party/freetype/README.chromium
index 1a218b4..e47638ce 100644
--- a/third_party/freetype/README.chromium
+++ b/third_party/freetype/README.chromium
@@ -1,7 +1,7 @@
 Name: FreeType
 URL: http://www.freetype.org/
-Version: VER-2-13-3-46-gf21999675
-Revision: f219996754c4df75b758983e18cabb401e5b93a1
+Version: VER-2-13-3-47-gee1310ab5
+Revision: ee1310ab5ca2897b760258b94f3d9230335cc2c0
 CPEPrefix: cpe:/a:freetype:freetype:2.13.3
 License: FTL
 License File: src/docs/FTL.TXT
diff --git a/third_party/freetype/src b/third_party/freetype/src
index f219996..ee1310ab 160000
--- a/third_party/freetype/src
+++ b/third_party/freetype/src
@@ -1 +1 @@
-Subproject commit f219996754c4df75b758983e18cabb401e5b93a1
+Subproject commit ee1310ab5ca2897b760258b94f3d9230335cc2c0
diff --git a/third_party/fuzztest/src b/third_party/fuzztest/src
index 87fffb7..ae6208f 160000
--- a/third_party/fuzztest/src
+++ b/third_party/fuzztest/src
@@ -1 +1 @@
-Subproject commit 87fffb7eaf974e55ec736f0100dcd9bdf3f91469
+Subproject commit ae6208fc45a09da94d9c0925e26cd9bbca92154b
diff --git a/third_party/pdfium b/third_party/pdfium
index 20b8b48..3cd0a26 160000
--- a/third_party/pdfium
+++ b/third_party/pdfium
@@ -1 +1 @@
-Subproject commit 20b8b48e460b742e34075cf5dd682e25dfea4dc3
+Subproject commit 3cd0a262ce17171069d69eb839a0ab9f284c329c
diff --git a/third_party/perfetto b/third_party/perfetto
index 0b3d3cc..38f6220 160000
--- a/third_party/perfetto
+++ b/third_party/perfetto
@@ -1 +1 @@
-Subproject commit 0b3d3cc75b911d55cb138252231d2c929e312dba
+Subproject commit 38f6220c882b08ed8bd8cbb47cff50cfa790e41b
diff --git a/third_party/vulkan-deps b/third_party/vulkan-deps
index 8d2a13c..ee1a15d 160000
--- a/third_party/vulkan-deps
+++ b/third_party/vulkan-deps
@@ -1 +1 @@
-Subproject commit 8d2a13cc437488e3dafc128371e0fb78cd1a216b
+Subproject commit ee1a15d510c031acaff02138a922ccdb6f85c2a7
diff --git a/third_party/vulkan-validation-layers/src b/third_party/vulkan-validation-layers/src
index ea05bf7..5cceb78 160000
--- a/third_party/vulkan-validation-layers/src
+++ b/third_party/vulkan-validation-layers/src
@@ -1 +1 @@
-Subproject commit ea05bf71f2948d64f7d36651ffcad59216401095
+Subproject commit 5cceb78082833556789a64f3237b04df7a826d93
diff --git a/third_party/webrtc b/third_party/webrtc
index 83861d5..da8a535a 160000
--- a/third_party/webrtc
+++ b/third_party/webrtc
@@ -1 +1 @@
-Subproject commit 83861d5649c83d1df82fafa622f469ac272f21aa
+Subproject commit da8a535ad4e41fbb587b38346d324a2892ba4183
diff --git a/third_party/widevine/cdm/linux b/third_party/widevine/cdm/linux
index 91eaf08b..8a12afc 160000
--- a/third_party/widevine/cdm/linux
+++ b/third_party/widevine/cdm/linux
@@ -1 +1 @@
-Subproject commit 91eaf08b6d45a593d8fa9a3aaff2ff0f6a97b422
+Subproject commit 8a12afc6ad470fac67ecb97bc9acf4bdbf9285e7
diff --git a/third_party/widevine/cdm/mac b/third_party/widevine/cdm/mac
index a0b43dc..8c2898c 160000
--- a/third_party/widevine/cdm/mac
+++ b/third_party/widevine/cdm/mac
@@ -1 +1 @@
-Subproject commit a0b43dc94b6519a2f0e4163801748e234fdfdaff
+Subproject commit 8c2898cf5e27669beeb7fc432a30f953f2541106
diff --git a/third_party/widevine/cdm/win b/third_party/widevine/cdm/win
index 7c1534e..33d580b2 160000
--- a/third_party/widevine/cdm/win
+++ b/third_party/widevine/cdm/win
@@ -1 +1 @@
-Subproject commit 7c1534e91a8bfdc4af28f8b7d6900343a845a8af
+Subproject commit 33d580b25178a85837950972b985f555c6d65fa9
diff --git a/third_party/zlib/google/compression_utils.cc b/third_party/zlib/google/compression_utils.cc
index 0ba3110..d50c969 100644
--- a/third_party/zlib/google/compression_utils.cc
+++ b/third_party/zlib/google/compression_utils.cc
@@ -89,19 +89,18 @@
   return false;
 }
 
-bool GzipUncompress(base::span<const char> input,
-                    base::span<const char> output) {
-  return GzipUncompress(base::as_bytes(input), base::as_bytes(output));
+bool GzipUncompress(base::span<const char> input, base::span<char> output) {
+  return GzipUncompress(base::as_bytes(input), base::as_writable_bytes(output));
 }
 
 bool GzipUncompress(base::span<const uint8_t> input,
-                    base::span<const uint8_t> output) {
+                    base::span<uint8_t> output) {
   uLongf uncompressed_size = GetUncompressedSize(input);
   if (uncompressed_size > output.size())
     return false;
   return zlib_internal::GzipUncompressHelper(
-             reinterpret_cast<Bytef*>(const_cast<uint8_t*>(output.data())),
-             &uncompressed_size, reinterpret_cast<const Bytef*>(input.data()),
+             reinterpret_cast<Bytef*>(output.data()), &uncompressed_size,
+             reinterpret_cast<const Bytef*>(input.data()),
              static_cast<uLongf>(input.size())) == Z_OK;
 }
 
diff --git a/third_party/zlib/google/compression_utils.h b/third_party/zlib/google/compression_utils.h
index ea39981..fd81153 100644
--- a/third_party/zlib/google/compression_utils.h
+++ b/third_party/zlib/google/compression_utils.h
@@ -43,12 +43,11 @@
 // needed. |output|'s size must be at least as large as the return value from
 // GetUncompressedSize.
 // Returns true for success.
-bool GzipUncompress(base::span<const char> input,
-                    base::span<const char> output);
+bool GzipUncompress(base::span<const char> input, base::span<char> output);
 
 // Like the above method, but using uint8_t instead.
 bool GzipUncompress(base::span<const uint8_t> input,
-                    base::span<const uint8_t> output);
+                    base::span<uint8_t> output);
 
 // Uncompresses the data in |input| using gzip, and writes the results to
 // |output|, which must NOT be the underlying string of |input|, and is resized
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index dabdf19..f2d9c4c 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -19730,6 +19730,7 @@
   <int value="-297225350" label="SensitiveContentWhileSwitchingTabs:disabled"/>
   <int value="-296762162" label="ExoOrdinalMotion:enabled"/>
   <int value="-296493265" label="ProjectorAppDebug:enabled"/>
+  <int value="-296285056" label="TabClosureMethodRefactor:enabled"/>
   <int value="-296208832" label="ServiceWorkerStaticRouter:disabled"/>
   <int value="-296203212"
       label="AutofillEnableMerchantDomainInUnmaskCardRequest:enabled"/>
@@ -20999,6 +21000,7 @@
   <int value="218317796" label="CastStreamingVp9:enabled"/>
   <int value="218890378" label="ManualSaving:disabled"/>
   <int value="219117936" label="AllowReaderForAccessibility:enabled"/>
+  <int value="219162425" label="TabClosureMethodRefactor:disabled"/>
   <int value="219384037" label="ESimPolicy:disabled"/>
   <int value="220182175" label="PipTuck:disabled"/>
   <int value="220654375" label="SearchResultInlineIcon:disabled"/>
diff --git a/tools/metrics/histograms/metadata/android/histograms.xml b/tools/metrics/histograms/metadata/android/histograms.xml
index c299592..fd433fef 100644
--- a/tools/metrics/histograms/metadata/android/histograms.xml
+++ b/tools/metrics/histograms/metadata/android/histograms.xml
@@ -911,7 +911,7 @@
 </histogram>
 
 <histogram name="Android.BindingManger.ConnectionsDroppedDueToMaxSize"
-    units="connections" expires_after="2025-02-23">
+    units="connections" expires_after="2025-08-23">
   <owner>ckitagawa@chromium.org</owner>
   <owner>yfriedman@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/apps/enums.xml b/tools/metrics/histograms/metadata/apps/enums.xml
index 2fead45..b92ba27 100644
--- a/tools/metrics/histograms/metadata/apps/enums.xml
+++ b/tools/metrics/histograms/metadata/apps/enums.xml
@@ -564,6 +564,8 @@
   <int value="0" label="Local file"/>
   <int value="1" label="Drive file"/>
   <int value="2" label="Unknown"/>
+  <int value="3" label="Help app - release notes"/>
+  <int value="4" label="Desks admin template"/>
 </enum>
 
 <!-- LINT.IfChange(LauncherSearchSessionConclusion) -->
diff --git a/tools/metrics/histograms/metadata/bluetooth/histograms.xml b/tools/metrics/histograms/metadata/bluetooth/histograms.xml
index 4c03b98d..02f1c94 100644
--- a/tools/metrics/histograms/metadata/bluetooth/histograms.xml
+++ b/tools/metrics/histograms/metadata/bluetooth/histograms.xml
@@ -97,7 +97,7 @@
 </histogram>
 
 <histogram name="Bluetooth.ChromeOS.DeviceDisconnect"
-    enum="BluetoothDeviceType" expires_after="2025-02-20">
+    enum="BluetoothDeviceType" expires_after="2025-11-20">
   <owner>khorimoto@chromium.org</owner>
   <owner>cros-device-enablement@google.com</owner>
   <summary>Emitted each time a Bluetooth device is disconnected.</summary>
@@ -105,7 +105,7 @@
 
 <histogram
     name="Bluetooth.ChromeOS.DeviceSelectionDuration{DeviceSelectionUISurfaces}"
-    units="ms" expires_after="2025-02-20">
+    units="ms" expires_after="2025-11-20">
   <owner>khorimoto@chromium.org</owner>
   <owner>cros-device-enablement@google.com</owner>
   <summary>
@@ -120,7 +120,7 @@
 
 <histogram
     name="Bluetooth.ChromeOS.DeviceSelectionDuration{DeviceSelectionUISurfaces}.NotPaired{BluetoothTransportTypes}"
-    units="ms" expires_after="2025-02-20">
+    units="ms" expires_after="2025-11-20">
   <owner>khorimoto@chromium.org</owner>
   <owner>cros-device-enablement@google.com</owner>
   <summary>
@@ -134,7 +134,7 @@
 
 <histogram
     name="Bluetooth.ChromeOS.DeviceSelectionDuration{DeviceSelectionUISurfaces}{BluetoothPairedStates}"
-    units="ms" expires_after="2025-02-20">
+    units="ms" expires_after="2025-11-20">
   <owner>khorimoto@chromium.org</owner>
   <owner>cros-device-enablement@google.com</owner>
   <summary>
@@ -1678,7 +1678,7 @@
 </histogram>
 
 <histogram name="Bluetooth.ChromeOS.Pairing.TransportType"
-    enum="BluetoothTransportType" expires_after="2025-02-20">
+    enum="BluetoothTransportType" expires_after="2025-11-20">
   <owner>khorimoto@chromium.org</owner>
   <owner>cros-device-enablement@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/compositing/histograms.xml b/tools/metrics/histograms/metadata/compositing/histograms.xml
index 91b3134f..11bd86c 100644
--- a/tools/metrics/histograms/metadata/compositing/histograms.xml
+++ b/tools/metrics/histograms/metadata/compositing/histograms.xml
@@ -463,6 +463,20 @@
   </summary>
 </histogram>
 
+<histogram
+    name="Compositing.Display.DrmThreadProxy.CheckOverlayCapabilitiesSyncOnDrmThreadUs"
+    units="microseconds" expires_after="2025-07-10">
+  <owner>harthuang@google.com</owner>
+  <owner>fangzhoug@chromium.org</owner>
+  <owner>petermcneeley@chromium.org</owner>
+  <owner>chromeos-gfx-compositor@chromium.org</owner>
+  <summary>
+    Logged zero or more times per frame, the time spent checking if a page flip
+    is succeeded or failed on DRM thread. Only reported for platforms supporting
+    high resolution clocks.
+  </summary>
+</histogram>
+
 <histogram name="Compositing.Display.FlattenedRenderPassCount" units="units"
     expires_after="2025-05-11">
   <owner>jonross@chromium.org</owner>
@@ -487,6 +501,21 @@
   </summary>
 </histogram>
 
+<histogram name="Compositing.Display.OverlayProcessorOzone.MaxPlanesSupported"
+    units="units" expires_after="2025-07-09">
+  <owner>harthuang@google.com</owner>
+  <owner>fangzhoug@chromium.org</owner>
+  <owner>petermcneeley@chromium.org</owner>
+  <owner>chromeos-gfx-compositor@chromium.org</owner>
+  <summary>
+    This is logged every time a valid HardwareCapabilites is received DRM thread
+    when display configuration may have changed. It records the number of
+    overlay planes we have available on this device including the primary plane.
+    Note: this metric was expired and removed on 2024-06-17 and re-enabled on
+    2025-01-09.
+  </summary>
+</histogram>
+
 <histogram
     name="Compositing.Display.OverlayProcessorUsingStrategy.FramesAttemptingRequiredOverlays"
     enum="Boolean" expires_after="2025-06-22">
@@ -557,6 +586,21 @@
 </histogram>
 
 <histogram
+    name="Compositing.Display.OverlayProcessorUsingStrategy.ShouldAttemptMultipleOverlays"
+    enum="AttemptingMultipleOverlays" expires_after="2025-07-09">
+  <owner>harthuang@google.com</owner>
+  <owner>fangzhoug@chromium.org</owner>
+  <owner>petermcneeley@chromium.org</owner>
+  <owner>chromeos-gfx-compositor@chromium.org</owner>
+  <summary>
+    Logged zero or one times per frame, whether we are using the
+    AttemptingMultipleOverlays code path this frame or we are not, and log the
+    reason we are not attempting multiple overlays. Note: this metric was
+    expired and removed on 2024-06-18 and re-enabled on 2025-01-09.
+  </summary>
+</histogram>
+
+<histogram
     name="Compositing.Display.OverlayProcessorUsingStrategy.ShouldPromoteCandidatesWithMasks"
     enum="PromotingMaskCandidates" expires_after="2025-06-29">
   <owner>zoraiznaeem@chromium.org</owner>
@@ -677,18 +721,6 @@
   </summary>
 </histogram>
 
-<histogram name="Compositing.Renderer.AnimationUpdateOnMissingPropertyNode"
-    enum="Boolean" expires_after="2024-12-02">
-  <owner>awogbemila@chromium.org</owner>
-  <owner>input-dev@chromium.org</owner>
-  <summary>
-    Records when we attempt to update an animation with a missing property node.
-    See crbug.com/1307498. We safely handle this case but until we find and fix
-    the root cause, this histogram allows us to keep track of the frequency at
-    this occurs.
-  </summary>
-</histogram>
-
 <histogram name="Compositing.Renderer.CALayerResult" enum="CALayerResult"
     expires_after="2025-05-11">
   <owner>ccameron@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/memory/histograms.xml b/tools/metrics/histograms/metadata/memory/histograms.xml
index 17347934..bdeb8c6 100644
--- a/tools/metrics/histograms/metadata/memory/histograms.xml
+++ b/tools/metrics/histograms/metadata/memory/histograms.xml
@@ -2070,7 +2070,7 @@
 </histogram>
 
 <histogram
-    name="Memory.SelfCompact{Process}.{Metric}.Diff.{Timing}.{Direction}"
+    name="Memory.SelfCompact2{Process}.{Metric}.Diff.{Timing}.{Direction}"
     units="MiB" expires_after="2025-10-06">
   <owner>thiabaud@google.com</owner>
   <owner>lizeb@google.com</owner>
@@ -2087,7 +2087,7 @@
   <token key="Direction" variants="DiffDirection"/>
 </histogram>
 
-<histogram name="Memory.SelfCompact{Process}.{Metric}.{Timing}" units="MiB"
+<histogram name="Memory.SelfCompact2{Process}.{Metric}.{Timing}" units="MiB"
     expires_after="2025-10-06">
   <owner>thiabaud@google.com</owner>
   <owner>lizeb@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/others/enums.xml b/tools/metrics/histograms/metadata/others/enums.xml
index f513fed..7090b5a 100644
--- a/tools/metrics/histograms/metadata/others/enums.xml
+++ b/tools/metrics/histograms/metadata/others/enums.xml
@@ -86,6 +86,36 @@
   <int value="2" label="Version v11"/>
 </enum>
 
+<enum name="FreedesktopSecretKeyProviderErrorDetail">
+  <summary>
+    Fine-grained error details for FreedesktopSecretKeyProvider.
+  </summary>
+  <int value="0" label="None"/>
+  <int value="1" label="DestructedBeforeComplete"/>
+  <int value="2" label="EmptyObjectPaths"/>
+  <int value="3" label="InvalidReplyFormat"/>
+  <int value="4" label="InvalidSignalFormat"/>
+  <int value="5" label="InvalidVariantFormat"/>
+  <int value="6" label="NoResponse"/>
+</enum>
+
+<enum name="FreedesktopSecretKeyProviderInitStatus">
+  <summary>
+    High-level error categories for FreedesktopSecretKeyProvider.
+  </summary>
+  <int value="0" label="Success"/>
+  <int value="1" label="CreateCollectionFailed"/>
+  <int value="2" label="CreateItemFailed"/>
+  <int value="3" label="EmptySecret"/>
+  <int value="4" label="GetSecretFailed"/>
+  <int value="5" label="GnomeKeyringDeadlock"/>
+  <int value="6" label="NoService"/>
+  <int value="7" label="ReadAliasFailed"/>
+  <int value="8" label="SearchItemsFailed"/>
+  <int value="9" label="SessionFailure"/>
+  <int value="10" label="UnlockFailed"/>
+</enum>
+
 <enum name="HappinessTrackingSurvey">
   <summary>Possible survey states and answers for each question.</summary>
   <int value="1" label="Survey Triggered"/>
diff --git a/tools/metrics/histograms/metadata/others/histograms.xml b/tools/metrics/histograms/metadata/others/histograms.xml
index 144dce0..ddd5db7 100644
--- a/tools/metrics/histograms/metadata/others/histograms.xml
+++ b/tools/metrics/histograms/metadata/others/histograms.xml
@@ -7070,6 +7070,39 @@
   </summary>
 </histogram>
 
+<histogram name="OSCrypt.FreedesktopSecretKeyProvider.InitStatus"
+    enum="FreedesktopSecretKeyProviderInitStatus" expires_after="2025-12-17">
+  <owner>thomasanderson@chromium.org</owner>
+  <owner>thestig@chromium.org</owner>
+  <summary>
+    The broad outcome of the org.freedesktop.secrets OSCrypt Async Key Provider
+    initialization, recorded once during browser startup (Linux only).
+  </summary>
+</histogram>
+
+<histogram name="OSCrypt.FreedesktopSecretKeyProvider.{InitStatus}.ErrorDetail"
+    enum="FreedesktopSecretKeyProviderErrorDetail" expires_after="2025-12-17">
+  <owner>thomasanderson@chromium.org</owner>
+  <owner>thestig@chromium.org</owner>
+  <summary>
+    The detailed error cause for each InitStatus for the org.freedesktop.secrets
+    OSCrypt Async Key Provider initialization, recorded once during browser
+    startup in case of failure (Linux only).
+  </summary>
+  <token key="InitStatus">
+    <variant name="CreateCollectionFailed"/>
+    <variant name="CreateItemFailed"/>
+    <variant name="EmptySecret"/>
+    <variant name="GetSecretFailed"/>
+    <variant name="GnomeKeyringDeadlock"/>
+    <variant name="NoService"/>
+    <variant name="ReadAliasFailed"/>
+    <variant name="SearchItemsFailed"/>
+    <variant name="SessionFailure"/>
+    <variant name="UnlockFailed"/>
+  </token>
+</histogram>
+
 <histogram name="OSCrypt.Linux.CanUseLibsecret" enum="BooleanAllowed"
     expires_after="2025-08-15">
   <owner>thomasanderson@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/page/histograms.xml b/tools/metrics/histograms/metadata/page/histograms.xml
index 4ec311f..9419e97 100644
--- a/tools/metrics/histograms/metadata/page/histograms.xml
+++ b/tools/metrics/histograms/metadata/page/histograms.xml
@@ -3857,7 +3857,7 @@
 
 <histogram
     name="PageLoad.PaintTiming.NavigationToFirstContentfulPaint.Background.HttpsOrDataOrFileScheme"
-    units="ms" expires_after="2025-02-21">
+    units="ms" expires_after="2026-02-21">
   <owner>sullivan@chromium.org</owner>
   <owner>speed-metrics-dev@chromium.org</owner>
   <owner>chrome-analysis-team@google.com</owner>
@@ -4016,7 +4016,7 @@
 
 <histogram
     name="PageLoad.PaintTiming.NavigationToLargestContentfulPaint2.Background.HttpsOrDataOrFileScheme"
-    units="ms" expires_after="2025-06-30">
+    units="ms" expires_after="2026-02-21">
   <owner>sullivan@chromium.org</owner>
   <owner>speed-metrics-dev@chromium.org</owner>
   <owner>chrome-analysis-team@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
index f8b50e5..d2465994 100644
--- a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
+++ b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
@@ -2481,22 +2481,6 @@
   </summary>
 </histogram>
 
-<histogram
-    name="SafeBrowsing.RT.EventUrlReferrerChainFetchSucceeded{UserCategory}"
-    enum="BooleanSuccess" expires_after="2025-02-23">
-  <owner>thefrog@chromium.org</owner>
-  <owner>chrome-counter-abuse-alerts@google.com</owner>
-  <summary>
-    Logged when fetching the referrer chain for a pending event URL is not able
-    to find a matching navigation event, so the referrer chain is fetched for
-    non-pending event URLs instead as a fallback. Logs true if the fallback
-    fetch returns a non-empty chain.
-
-    {UserCategory}
-  </summary>
-  <token key="UserCategory" variants="RealTimeUrlCheckUserCategory"/>
-</histogram>
-
 <histogram name="SafeBrowsing.RT.GetCache.FallbackThreatType"
     enum="SBThreatType" expires_after="2025-05-25">
   <owner>xinghuilu@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/user_education/histograms.xml b/tools/metrics/histograms/metadata/user_education/histograms.xml
index a22266a..1fb6982 100644
--- a/tools/metrics/histograms/metadata/user_education/histograms.xml
+++ b/tools/metrics/histograms/metadata/user_education/histograms.xml
@@ -102,7 +102,7 @@
 </variants>
 
 <histogram name="Tutorial.TabGroup.EditedTitle" enum="BooleanSuccess"
-    expires_after="2024-09-20">
+    expires_after="never">
   <owner>dpenning@chromium.org</owner>
   <owner>dfried@chromium.org</owner>
   <summary>
diff --git a/tools/perf/chrome_telemetry_build/BUILD.gn b/tools/perf/chrome_telemetry_build/BUILD.gn
index a3cc238..9bec0e6 100644
--- a/tools/perf/chrome_telemetry_build/BUILD.gn
+++ b/tools/perf/chrome_telemetry_build/BUILD.gn
@@ -39,14 +39,20 @@
 group("telemetry_chrome_test") {
   testonly = true
 
-  data_deps = [
-    ":telemetry_chrome_test_without_chrome",
-    "//third_party/perfetto/src/trace_processor:trace_processor_shell($host_toolchain)",
-  ]
+  data_deps = [ ":telemetry_chrome_test_without_chrome" ]
   data = []
 
-  if (is_android || is_fuchsia) {
-    data_deps += [ ":host_trace_processor_shell" ]
+  # Compile the host's version of trace_processor_shell for remote platforms.
+  if (is_android || is_fuchsia || is_chromeos) {
+    data_deps += [ "//third_party/perfetto/src/trace_processor:trace_processor_shell($host_toolchain)" ]
+
+    # Android and Fuchsia require a symlink for the host version to be included
+    # in the isolate.
+    if (is_android || is_fuchsia) {
+      data_deps += [ ":host_trace_processor_shell" ]
+    }
+  } else {
+    data_deps += [ "//third_party/perfetto/src/trace_processor:trace_processor_shell($default_toolchain)" ]
   }
 
   if (!is_fuchsia && !is_android && !is_castos && !is_ios) {
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 0f8a811..87cc92a 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -5,8 +5,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v49.0/linux-arm64/trace_processor_shell"
         },
         "win": {
-            "hash": "871809546a1fa0a0a74080b53bda9be32a50161d",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/0b3d3cc75b911d55cb138252231d2c929e312dba/trace_processor_shell.exe"
+            "hash": "81ac9b9e0950715b1cd7bfea1230606e5ef4357f",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/38f6220c882b08ed8bd8cbb47cff50cfa790e41b/trace_processor_shell.exe"
         },
         "linux_arm": {
             "hash": "a15d8362d80cfd7cd8d785cf6afc22586de688cd",
@@ -21,8 +21,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v49.0/mac-arm64/trace_processor_shell"
         },
         "linux": {
-            "hash": "dcc509ec51017ea9e1cf5f40916cb520ddc70611",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/0b3d3cc75b911d55cb138252231d2c929e312dba/trace_processor_shell"
+            "hash": "24fb6186c9dcbb4c77c7200803988100907b9730",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/38f6220c882b08ed8bd8cbb47cff50cfa790e41b/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/ui/android/event_forwarder.cc b/ui/android/event_forwarder.cc
index 34c80f23..a980682 100644
--- a/ui/android/event_forwarder.cc
+++ b/ui/android/event_forwarder.cc
@@ -209,7 +209,7 @@
                                         jlong time_ms,
                                         jfloat scale) {
   float dip_scale = view_->GetDipScale();
-  auto size = view_->GetSize();
+  auto size = view_->GetSizeDIPs();
   float x = size.width() / 2;
   float y = size.height() / 2;
   gfx::PointF root_location =
@@ -226,7 +226,7 @@
     const JavaParamRef<jobject>& motion_event,
     jlong event_time_ns,
     jlong down_time_ms) {
-  auto size = view_->GetSize();
+  auto size = view_->GetSizeDIPs();
   float x = size.width() / 2;
   float y = size.height() / 2;
   ui::MotionEventAndroid::Pointer pointer0(0, x, y, 0, 0, 0, 0, 0);
diff --git a/ui/android/javatests/src/org/chromium/ui/test/util/MockitoHelper.java b/ui/android/javatests/src/org/chromium/ui/test/util/MockitoHelper.java
index f5ba804d..2f306042 100644
--- a/ui/android/javatests/src/org/chromium/ui/test/util/MockitoHelper.java
+++ b/ui/android/javatests/src/org/chromium/ui/test/util/MockitoHelper.java
@@ -49,6 +49,14 @@
                         });
     }
 
+    /**
+     * Simplified version of {@link #doCallback(int, Callback)} when the same value is always given
+     * to the callback.
+     */
+    public static <T> Stubber runWithValue(int index, T value) {
+        return doCallback(index, (Callback<T> callback) -> callback.onResult(value));
+    }
+
     /** When no argument is needed. */
     public static Stubber doRunnable(Runnable runnable) {
         return Mockito.doAnswer(
diff --git a/ui/android/view_android.cc b/ui/android/view_android.cc
index ab9d83f..a955092 100644
--- a/ui/android/view_android.cc
+++ b/ui/android/view_android.cc
@@ -151,9 +151,9 @@
 
   // Empty view size also need not propagating down in order to prevent
   // spurious events with empty size from being sent down.
-  if (child->match_parent() && !bounds_.IsEmpty() &&
-      child->GetSize() != bounds_.size()) {
-    child->OnSizeChangedInternal(bounds_.size());
+  if (child->match_parent() && !bounds_device_px_.IsEmpty() &&
+      child->GetSizeDevicePx() != bounds_device_px_.size()) {
+    child->OnSizeChangedInternal(bounds_device_px_.size());
     child->DispatchOnSizeChanged();
   }
 
@@ -493,25 +493,32 @@
   // Match-parent view must not receive size events.
   DCHECK(!match_parent());
 
-  float scale = GetDipScale();
-  gfx::Size size(std::ceil(width / scale), std::ceil(height / scale));
-  if (bounds_.size() == size)
-    return;
+  gfx::Size size_device_px(width, height);
 
-  OnSizeChangedInternal(size);
+  if (bounds_device_px_.size() == size_device_px) {
+    return;
+  }
+
+  OnSizeChangedInternal(size_device_px);
 
   // Signal resize event after all the views in the tree get the updated size.
   DispatchOnSizeChanged();
 }
 
-void ViewAndroid::OnSizeChangedInternal(const gfx::Size& size) {
-  if (bounds_.size() == size)
+void ViewAndroid::OnSizeChangedInternal(const gfx::Size& size_device_px) {
+  if (bounds_device_px_.size() == size_device_px) {
     return;
+  }
 
-  bounds_.set_size(size);
+  bounds_device_px_.set_size(size_device_px);
+
+  float scale = GetDipScale();
+  bounds_dips_.set_size(gfx::Size(std::ceil(size_device_px.width() / scale),
+                                  std::ceil(size_device_px.height() / scale)));
+
   for (ViewAndroid* child : children_) {
     if (child->match_parent())
-      child->OnSizeChangedInternal(size);
+      child->OnSizeChangedInternal(size_device_px);
   }
 }
 
@@ -554,8 +561,12 @@
   return physical_size_;
 }
 
-gfx::Size ViewAndroid::GetSize() const {
-  return bounds_.size();
+gfx::Size ViewAndroid::GetSizeDIPs() const {
+  return bounds_dips_.size();
+}
+
+gfx::Size ViewAndroid::GetSizeDevicePx() const {
+  return bounds_device_px_.size();
 }
 
 bool ViewAndroid::OnDragEvent(const DragEventAndroid& event) {
@@ -685,7 +696,7 @@
                           const E& event,
                           const gfx::PointF& point) {
   if (event_handler_) {
-    if (bounds_.origin().IsOrigin()) {  // (x, y) == (0, 0)
+    if (bounds_dips_.origin().IsOrigin()) {  // (x, y) == (0, 0)
       if (handler_callback.Run(event_handler_.get(), event))
         return true;
     } else {
@@ -697,14 +708,14 @@
 
   if (!children_.empty()) {
     gfx::PointF offset_point(point);
-    offset_point.Offset(-bounds_.x(), -bounds_.y());
+    offset_point.Offset(-bounds_dips_.x(), -bounds_dips_.y());
     gfx::Point int_point = gfx::ToFlooredPoint(offset_point);
 
     // Match from back to front for hit testing.
     for (ViewAndroid* child : base::Reversed(children_)) {
       bool matched = child->match_parent();
       if (!matched)
-        matched = child->bounds_.Contains(int_point);
+        matched = child->bounds_dips_.Contains(int_point);
       if (matched && child->HitTest(handler_callback, event, offset_point))
         return true;
     }
@@ -713,7 +724,8 @@
 }
 
 void ViewAndroid::SetLayoutForTesting(int x, int y, int width, int height) {
-  bounds_.SetRect(x, y, width, height);
+  bounds_dips_.SetRect(x, y, width, height);
+  bounds_device_px_.SetRect(x, y, width, height);
 }
 
 size_t ViewAndroid::GetChildrenCountForTesting() const {
diff --git a/ui/android/view_android.h b/ui/android/view_android.h
index 78876253..314f89e 100644
--- a/ui/android/view_android.h
+++ b/ui/android/view_android.h
@@ -168,8 +168,10 @@
                         jint drag_obj_rect_height);
 
   gfx::Size GetPhysicalBackingSize() const;
-  gfx::Size GetSize() const;
+  gfx::Size GetSizeDIPs() const;
+  gfx::Size GetSizeDevicePx() const;
 
+  // |width| and |height| are in device pixels.
   void OnSizeChanged(int width, int height);
   // |deadline_override| if not nullopt will be used as the cc::DeadlinePolicy
   // timeout for this resize.
@@ -302,7 +304,7 @@
   // each leaf of subtree.
   static bool SubtreeHasEventForwarder(ViewAndroid* view);
 
-  void OnSizeChangedInternal(const gfx::Size& size);
+  void OnSizeChangedInternal(const gfx::Size& size_device_px);
   void DispatchOnSizeChanged();
 
   // Returns the Java delegate for this view. This is used to delegate work
@@ -320,7 +322,11 @@
 
   // Basic view layout information. Used to do hit testing deciding whether
   // the passed events should be processed by the view. Unit in DIP.
-  gfx::Rect bounds_;
+  gfx::Rect bounds_dips_;
+
+  // Same as above, but before dividing by the device scale factor.
+  gfx::Rect bounds_device_px_;
+
   const LayoutType layout_type_;
 
   // In physical pixel.
diff --git a/ui/android/view_android_unittest.cc b/ui/android/view_android_unittest.cc
index bbe3a4e..e3d3202 100644
--- a/ui/android/view_android_unittest.cc
+++ b/ui/android/view_android_unittest.cc
@@ -214,7 +214,7 @@
 
   Reset();
 
-  // Match-parent view should not receivee size events in the first place.
+  // Match-parent view should not receive size events in the first place.
   EXPECT_DCHECK_DEATH(viewm_.OnSizeChanged(100, 200));
   EXPECT_FALSE(handlerm_.OnSizeCalled());
   EXPECT_FALSE(handler3_.OnSizeCalled());
diff --git a/ui/aura/window.cc b/ui/aura/window.cc
index 10d798d..09f237bc 100644
--- a/ui/aura/window.cc
+++ b/ui/aura/window.cc
@@ -62,6 +62,7 @@
 #include "ui/compositor/compositor.h"
 #include "ui/compositor/layer.h"
 #include "ui/compositor/layer_animator.h"
+#include "ui/compositor/layer_type.h"
 #include "ui/display/display.h"
 #include "ui/display/screen.h"
 #include "ui/events/event_target_iterator.h"
@@ -333,7 +334,9 @@
     return;
   transparent_ = transparent;
 
-  layer()->SetFillsBoundsOpaquely(!transparent_);
+  if (layer()->type() != ui::LAYER_SOLID_COLOR) {
+    layer()->SetFillsBoundsOpaquely(!transparent_);
+  }
   TriggerChangedCallback(&transparent_);
 }
 
diff --git a/ui/base/ime/dummy_text_input_client.cc b/ui/base/ime/dummy_text_input_client.cc
index e2d434e4..e9acec7 100644
--- a/ui/base/ime/dummy_text_input_client.cc
+++ b/ui/base/ime/dummy_text_input_client.cc
@@ -92,7 +92,7 @@
 }
 
 std::optional<size_t> DummyTextInputClient::GetProximateCharacterIndexFromPoint(
-    const gfx::Point& point,
+    const gfx::Point& screen_point_in_dips,
     IndexFromPointFlags flags) const {
   return std::nullopt;
 }
diff --git a/ui/base/ime/dummy_text_input_client.h b/ui/base/ime/dummy_text_input_client.h
index f4466c1..1045dfba 100644
--- a/ui/base/ime/dummy_text_input_client.h
+++ b/ui/base/ime/dummy_text_input_client.h
@@ -47,7 +47,7 @@
   std::optional<gfx::Rect> GetProximateCharacterBounds(
       const gfx::Range& range) const override;
   std::optional<size_t> GetProximateCharacterIndexFromPoint(
-      const gfx::Point& point,
+      const gfx::Point& screen_point_in_dips,
       IndexFromPointFlags flags) const override;
 #endif  // BUILDFLAG(IS_WIN)
   bool GetCompositionCharacterBounds(size_t index,
diff --git a/ui/base/ime/fake_text_input_client.cc b/ui/base/ime/fake_text_input_client.cc
index 322c435..4221e6c66 100644
--- a/ui/base/ime/fake_text_input_client.cc
+++ b/ui/base/ime/fake_text_input_client.cc
@@ -147,7 +147,7 @@
 }
 
 std::optional<size_t> FakeTextInputClient::GetProximateCharacterIndexFromPoint(
-    const gfx::Point& point,
+    const gfx::Point& screen_point_in_dips,
     IndexFromPointFlags flags) const {
   return std::nullopt;
 }
diff --git a/ui/base/ime/fake_text_input_client.h b/ui/base/ime/fake_text_input_client.h
index 5a9bc91..62d1d5b 100644
--- a/ui/base/ime/fake_text_input_client.h
+++ b/ui/base/ime/fake_text_input_client.h
@@ -82,7 +82,7 @@
   std::optional<gfx::Rect> GetProximateCharacterBounds(
       const gfx::Range& range) const override;
   std::optional<size_t> GetProximateCharacterIndexFromPoint(
-      const gfx::Point& point,
+      const gfx::Point& screen_point_in_dips,
       IndexFromPointFlags flags) const override;
 #endif  // BUILDFLAG(IS_WIN)
   bool GetCompositionCharacterBounds(size_t index,
diff --git a/ui/base/ime/text_input_client.h b/ui/base/ime/text_input_client.h
index cb24560..f9bf583 100644
--- a/ui/base/ime/text_input_client.h
+++ b/ui/base/ime/text_input_client.h
@@ -197,26 +197,28 @@
 
   // For StylusHandwritingWin gesture support, this method mirrors the
   // expectations of ITextStoreACP::GetACPFromPoint. Depending on which `flags`
-  // are provided, returns an appropriate character offset relative to `point`.
-  // See comments around IndexFromPointFlags and its values for details.
+  // are provided, returns an appropriate character offset relative to
+  // `screen_point_in_dips`. See comments around IndexFromPointFlags and its
+  // values for details.
   //
   // For renderer content, "ProximateCharacterBounds" uses a cached subset of
-  // the actual character bounding boxes, so requests for a `point` that's
+  // the actual character bounding boxes, so requests for `screen_point_in_dips`
   // contained by a character bounding box may not be considered "hit" by this
   // method if that character falls outside the cached range, or what's
   // considered "nearest" may be technically incorrect based on this fact. If
-  // no `flags` are provided and `point` isn't contained by any cached character
-  // bounds, regardless of whether the point is technically valid for the
-  // content, std::nullopt is returned. If either or both `flags` are provided,
-  // this is guaranteed to return *some* character offset, even if it's not the
-  // most appropriate offset based on the actual content.
+  // no `flags` are provided and `screen_point_in_dips` isn't contained by any
+  // cached character bounds, regardless of whether `screen_point_in_dips` is
+  // technically valid for the content, std::nullopt is returned. If either or
+  // both `flags` are provided, this is guaranteed to return *some* character
+  // offset, even if it's not the most appropriate offset based on the actual
+  // content.
   //
   // For views content, it's possible to retrieve accurate results for
   // "ProximateCharacterBounds" since the data is readily available. The caching
   // mechanism is to mitigate performance costs (CPU and memory) when processing
   // very large documents.
   virtual std::optional<size_t> GetProximateCharacterIndexFromPoint(
-      const gfx::Point& point,
+      const gfx::Point& screen_point_in_dips,
       IndexFromPointFlags flags) const = 0;
 #endif  // BUILDFLAG(IS_WIN)
 
diff --git a/ui/base/ime/win/tsf_text_store.cc b/ui/base/ime/win/tsf_text_store.cc
index 91c3cb1..99f1ef2 100644
--- a/ui/base/ime/win/tsf_text_store.cc
+++ b/ui/base/ime/win/tsf_text_store.cc
@@ -177,9 +177,12 @@
     if (flags & GXFPF_ROUND_NEAREST) {
       index_flags |= IndexFromPointFlags::kNearestToContainedPoint;
     }
+    const gfx::Point screen_point_in_dips =
+        gfx::ToFlooredPoint(display::win::ScreenWin::ScreenToDIPPoint(
+            gfx::PointF(gfx::Point(*point))));
     const std::optional<size_t> index =
         text_input_client_->GetProximateCharacterIndexFromPoint(
-            gfx::Point(*point), index_flags);
+            screen_point_in_dips, index_flags);
     if (!index.has_value()) {
       return TS_E_INVALIDPOINT;
     }
diff --git a/ui/compositor/layer.cc b/ui/compositor/layer.cc
index 75ffc37..4cc0de6 100644
--- a/ui/compositor/layer.cc
+++ b/ui/compositor/layer.cc
@@ -307,12 +307,14 @@
   clone->SetVisible(GetTargetVisibility());
   clone->SetClipRect(GetTargetClipRect());
   clone->SetAcceptEvents(accept_events());
-  clone->SetFillsBoundsOpaquely(fills_bounds_opaquely_);
   clone->SetFillsBoundsCompletely(fills_bounds_completely_);
   clone->SetRoundedCornerRadius(GetTargetRoundedCornerRadius());
   clone->SetGradientMask(gradient_mask());
   clone->SetIsFastRoundedCorner(is_fast_rounded_corner());
   clone->SetName(name_);
+  if (type() != LAYER_SOLID_COLOR) {
+    clone->SetFillsBoundsOpaquely(fills_bounds_opaquely_);
+  }
 
   // the |damaged_region_| will be sent to cc later in SendDamagedRects().
   clone->damaged_region_ = damaged_region_;
@@ -896,6 +898,7 @@
 }
 
 void Layer::SetFillsBoundsOpaquely(bool fills_bounds_opaquely) {
+  CHECK_NE(type_, LayerType::LAYER_SOLID_COLOR);
   SetFillsBoundsOpaquelyWithReason(fills_bounds_opaquely,
                                    PropertyChangeReason::NOT_FROM_ANIMATION);
 }
diff --git a/ui/compositor/layer.h b/ui/compositor/layer.h
index 729a8f2..b0d78219 100644
--- a/ui/compositor/layer.h
+++ b/ui/compositor/layer.h
@@ -414,6 +414,8 @@
   // Note: Setting a layer non-opaque has significant performance impact,
   // especially on low-end Chrome OS devices. Please ensure you are not
   // adding unnecessary overdraw. When in doubt, talk to the graphics team.
+  // NOTE: Opacity of SOLID_COLOR layer is determined by the color's alpha
+  // channel. Calling this on SOLID_COLOR results in check failure.
   void SetFillsBoundsOpaquely(bool fills_bounds_opaquely);
   bool fills_bounds_opaquely() const { return fills_bounds_opaquely_; }
 
diff --git a/ui/compositor/layer_unittest.cc b/ui/compositor/layer_unittest.cc
index 6bf2754..ecc6b84 100644
--- a/ui/compositor/layer_unittest.cc
+++ b/ui/compositor/layer_unittest.cc
@@ -865,7 +865,7 @@
 
   constexpr SkColor kTransparent = SK_ColorTRANSPARENT;
   layer->SetColor(kTransparent);
-  layer->SetFillsBoundsOpaquely(false);
+
   // Color and opaqueness targets should be preserved during cloning, even after
   // switching away from solid color content.
   ASSERT_TRUE(layer->SwitchCCLayerForTest());
@@ -882,23 +882,6 @@
   EXPECT_FALSE(clone->LayerHasCustomColorMatrix());
   EXPECT_FALSE(clone->fills_bounds_opaquely());
 
-  // A solid color layer with transparent color can be marked as opaque. The
-  // clone should retain this state.
-  layer = CreateLayer(LAYER_SOLID_COLOR);
-  layer->SetColor(kTransparent);
-  layer->SetFillsBoundsOpaquely(true);
-
-  clone = layer->Clone();
-  EXPECT_TRUE(clone->GetTargetTransform().IsIdentity());
-  EXPECT_EQ(kTransparent, clone->background_color());
-  EXPECT_EQ(kTransparent, clone->GetTargetColor());
-  EXPECT_FALSE(clone->layer_inverted());
-  // Sepia and hue rotation should be off by default.
-  EXPECT_FLOAT_EQ(0, layer->layer_sepia());
-  EXPECT_FLOAT_EQ(0, clone->layer_hue_rotation());
-  EXPECT_FALSE(clone->LayerHasCustomColorMatrix());
-  EXPECT_TRUE(clone->fills_bounds_opaquely());
-
   layer = CreateLayer(LAYER_SOLID_COLOR);
   layer->SetVisible(true);
   layer->SetOpacity(1.0f);
@@ -1143,7 +1126,6 @@
 
 TEST_F(LayerWithNullDelegateTest, SwitchLayerPreservesCCLayerState) {
   std::unique_ptr<Layer> l1 = CreateLayer(LAYER_SOLID_COLOR);
-  l1->SetFillsBoundsOpaquely(true);
   l1->SetVisible(false);
   l1->SetBounds(gfx::Rect(4, 5));
 
@@ -2690,7 +2672,6 @@
   SkColor transparent = SK_ColorTRANSPARENT;
   std::unique_ptr<Layer> root = CreateLayer(LAYER_SOLID_COLOR);
   GetCompositor()->SetRootLayer(root.get());
-  root->SetFillsBoundsOpaquely(false);
   root->SetColor(transparent);
 
   EXPECT_FALSE(root->fills_bounds_opaquely());
@@ -2727,7 +2708,6 @@
   {
     ui::ScopedLayerAnimationSettings animation(root->GetAnimator());
     animation.SetTransitionDuration(base::Milliseconds(1000));
-    root->SetFillsBoundsOpaquely(false);
     root->SetColor(transparent);
   }
 
diff --git a/ui/latency/latency_info.h b/ui/latency/latency_info.h
index 657f1fe..05c9278 100644
--- a/ui/latency/latency_info.h
+++ b/ui/latency/latency_info.h
@@ -13,7 +13,6 @@
 #include "base/tracing/protos/chrome_track_event.pbzero.h"
 #include "build/blink_buildflags.h"
 #include "build/build_config.h"
-#include "third_party/perfetto/include/perfetto/tracing/event_context.h"
 #include "third_party/perfetto/protos/perfetto/trace/track_event/chrome_latency_info.pbzero.h"
 
 #if BUILDFLAG(USE_BLINK)
diff --git a/ui/ozone/platform/drm/gpu/drm_display.cc b/ui/ozone/platform/drm/gpu/drm_display.cc
index 95ef5dd..9853b25 100644
--- a/ui/ozone/platform/drm/gpu/drm_display.cc
+++ b/ui/ozone/platform/drm/gpu/drm_display.cc
@@ -479,6 +479,11 @@
       return false;
     }
 
+    if (hdr_output_metadata_property->count_blobs == 0) {
+      // HDR metadata property was never set
+      return true;
+    }
+
     // TODO(b/342617770): Atomically set connector properties across all
     // connectors owned by DrmDisplay to prevent scenarios where SetProperty()
     // succeeds for a subset of the connectors and creates inconsistencies.
@@ -581,20 +586,21 @@
 
     ScopedDrmPropertyPtr color_space_property(
         drm_->GetProperty(crtc_connector_pair.connector.get(), kColorSpace));
-    if (!color_space_property) {
+    if (!color_space_property || !color_space_property->prop_id) {
       PLOG(INFO) << "'" << kColorSpace << "' property doesn't exist.";
       return false;
     }
 
+    const uint64_t enum_value_for_colorspace =
+        GetEnumValueForName(*drm_, color_space_property->prop_id,
+                            GetNameForColorspace(color_space));
+
     // TODO(b/342617770): Atomically set connector properties across all
     // connectors owned by DrmDisplay to prevent scenarios where SetProperty()
     // succeeds for a subset of the connectors and creates inconsistencies.
-    if (!color_space_property->prop_id ||
-        !drm_->SetProperty(
-            crtc_connector_pair.connector->connector_id,
-            color_space_property->prop_id,
-            GetEnumValueForName(*drm_, color_space_property->prop_id,
-                                GetNameForColorspace(color_space)))) {
+    if (!drm_->SetProperty(crtc_connector_pair.connector->connector_id,
+                           color_space_property->prop_id,
+                           enum_value_for_colorspace)) {
       PLOG(ERROR) << "Cannot set '" << GetNameForColorspace(color_space)
                   << "' to '" << kColorSpace << "' property for connector "
                   << crtc_connector_pair.connector->connector_id;
diff --git a/ui/ozone/platform/drm/gpu/drm_thread_proxy.cc b/ui/ozone/platform/drm/gpu/drm_thread_proxy.cc
index dba0a23..cd6973b 100644
--- a/ui/ozone/platform/drm/gpu/drm_thread_proxy.cc
+++ b/ui/ozone/platform/drm/gpu/drm_thread_proxy.cc
@@ -8,8 +8,11 @@
 #include <utility>
 
 #include "base/functional/bind.h"
+#include "base/metrics/histogram_macros.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/threading/thread_restrictions.h"
+#include "base/time/time.h"
+#include "base/timer/elapsed_timer.h"
 #include "base/trace_event/trace_event.h"
 #include "ui/gfx/linux/gbm_wrapper.h"
 #include "ui/ozone/platform/drm/gpu/drm_device.h"
@@ -169,9 +172,21 @@
   base::OnceClosure task = base::BindOnce(
       &DrmThread::CheckOverlayCapabilitiesSync, base::Unretained(&drm_thread_),
       widget, candidates, &result);
+
+  base::ElapsedTimer timer;
   PostSyncTask(drm_thread_.task_runner(),
                base::BindOnce(&DrmThread::RunTaskAfterDeviceReady,
                               base::Unretained(&drm_thread_), std::move(task)));
+  base::TimeDelta time = timer.Elapsed();
+
+  static constexpr base::TimeDelta kMinTime = base::Microseconds(1);
+  static constexpr base::TimeDelta kMaxTime = base::Milliseconds(10);
+  static constexpr int kTimeBuckets = 50;
+  UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
+      "Compositing.Display.DrmThreaedProxy."
+      "CheckOverlayCapabilitiesSyncOnDrmThreadUs",
+      time, kMinTime, kMaxTime, kTimeBuckets);
+
   return result;
 }
 
diff --git a/ui/touch_selection/touch_selection_magnifier_aura.cc b/ui/touch_selection/touch_selection_magnifier_aura.cc
index ff95688..dba189ca 100644
--- a/ui/touch_selection/touch_selection_magnifier_aura.cc
+++ b/ui/touch_selection/touch_selection_magnifier_aura.cc
@@ -300,7 +300,6 @@
   // Create the zoom layer, which will show the zoomed contents.
   zoom_layer_ = std::make_unique<Layer>(LAYER_SOLID_COLOR);
   zoom_layer_->SetBackgroundZoom(kMagnifierScale, 0);
-  zoom_layer_->SetFillsBoundsOpaquely(false);
   // BackdropFilterBounds applies after the backdrop filter (the zoom effect)
   // but before anything else, meaning its clipping effect is transformed by
   // the layer_offset() filter operation. SetRoundedCornerRadius() applies too
diff --git a/ui/views/controls/prefix_selector.cc b/ui/views/controls/prefix_selector.cc
index 4e9cfe2..675213c 100644
--- a/ui/views/controls/prefix_selector.cc
+++ b/ui/views/controls/prefix_selector.cc
@@ -104,7 +104,7 @@
 }
 
 std::optional<size_t> PrefixSelector::GetProximateCharacterIndexFromPoint(
-    const gfx::Point& point,
+    const gfx::Point& screen_point_in_dips,
     ui::IndexFromPointFlags flags) const {
   NOTIMPLEMENTED_LOG_ONCE();
   return std::nullopt;
diff --git a/ui/views/controls/prefix_selector.h b/ui/views/controls/prefix_selector.h
index ca774dbaeb..098c09dc 100644
--- a/ui/views/controls/prefix_selector.h
+++ b/ui/views/controls/prefix_selector.h
@@ -64,7 +64,7 @@
   std::optional<gfx::Rect> GetProximateCharacterBounds(
       const gfx::Range& range) const override;
   std::optional<size_t> GetProximateCharacterIndexFromPoint(
-      const gfx::Point& point,
+      const gfx::Point& screen_point_in_dips,
       ui::IndexFromPointFlags flags) const override;
 #endif  // BUILDFLAG(IS_WIN)
   bool GetCompositionCharacterBounds(size_t index,
diff --git a/ui/views/controls/textfield/textfield.cc b/ui/views/controls/textfield/textfield.cc
index 33afc532..2536a20d 100644
--- a/ui/views/controls/textfield/textfield.cc
+++ b/ui/views/controls/textfield/textfield.cc
@@ -1780,7 +1780,7 @@
 }
 
 std::optional<size_t> Textfield::GetProximateCharacterIndexFromPoint(
-    const gfx::Point& point,
+    const gfx::Point& screen_point_in_dips,
     ui::IndexFromPointFlags flags) const {
   NOTIMPLEMENTED_LOG_ONCE();
   return std::nullopt;
diff --git a/ui/views/controls/textfield/textfield.h b/ui/views/controls/textfield/textfield.h
index a808b73..cd996cd 100644
--- a/ui/views/controls/textfield/textfield.h
+++ b/ui/views/controls/textfield/textfield.h
@@ -443,7 +443,7 @@
   std::optional<gfx::Rect> GetProximateCharacterBounds(
       const gfx::Range& range) const override;
   std::optional<size_t> GetProximateCharacterIndexFromPoint(
-      const gfx::Point& point,
+      const gfx::Point& screen_point_in_dips,
       ui::IndexFromPointFlags flags) const override;
 #endif  // BUILDFLAG(IS_WIN)
   bool GetCompositionCharacterBounds(size_t index,
diff --git a/ui/views/view.cc b/ui/views/view.cc
index 87be2461..6f9417d6 100644
--- a/ui/views/view.cc
+++ b/ui/views/view.cc
@@ -61,6 +61,7 @@
 #include "ui/compositor/clip_recorder.h"
 #include "ui/compositor/compositor.h"
 #include "ui/compositor/layer.h"
+#include "ui/compositor/layer_type.h"
 #include "ui/compositor/paint_context.h"
 #include "ui/compositor/paint_recorder.h"
 #include "ui/compositor/transform_recorder.h"
@@ -2554,7 +2555,9 @@
 
   CreateOrDestroyLayer();
 
-  layer()->SetFillsBoundsOpaquely(false);
+  if (layer()->type() != ui::LAYER_SOLID_COLOR) {
+    layer()->SetFillsBoundsOpaquely(false);
+  }
 }
 
 void View::SetLayerParent(ui::Layer* parent_layer) {
diff --git a/ui/webui/resources/cr_components/certificate_manager/certificate_manager_v2.mojom b/ui/webui/resources/cr_components/certificate_manager/certificate_manager_v2.mojom
index d07b786..b4a6a41 100644
--- a/ui/webui/resources/cr_components/certificate_manager/certificate_manager_v2.mojom
+++ b/ui/webui/resources/cr_components/certificate_manager/certificate_manager_v2.mojom
@@ -165,4 +165,7 @@
 
   // Triggers a reload of the certificates for the given sources.
   TriggerReload(array<CertificateSource> sources);
+
+  // Trigger an update to refetch the Cert Metadata
+  TriggerMetadataUpdate();
 };
diff --git a/ui/webui/resources/cr_components/certificate_manager/local_certs_section_v2.ts b/ui/webui/resources/cr_components/certificate_manager/local_certs_section_v2.ts
index 764f90d1..6777b40f 100644
--- a/ui/webui/resources/cr_components/certificate_manager/local_certs_section_v2.ts
+++ b/ui/webui/resources/cr_components/certificate_manager/local_certs_section_v2.ts
@@ -105,6 +105,13 @@
 
   override ready() {
     super.ready();
+    this.onMetadataRefresh_();
+    const proxy = CertificatesV2BrowserProxy.getInstance();
+    proxy.callbackRouter.triggerMetadataUpdate.addListener(
+        this.onMetadataRefresh_.bind(this));
+  }
+
+  private onMetadataRefresh_() {
     const proxy = CertificatesV2BrowserProxy.getInstance();
     proxy.handler.getCertManagementMetadata().then(
         (results: {metadata: CertManagementMetadata}) => {
diff --git a/ui/webui/resources/cr_components/searchbox/searchbox.ts b/ui/webui/resources/cr_components/searchbox/searchbox.ts
index 9871b755..3ef5719 100644
--- a/ui/webui/resources/cr_components/searchbox/searchbox.ts
+++ b/ui/webui/resources/cr_components/searchbox/searchbox.ts
@@ -855,6 +855,11 @@
         this.isDeletingInput_ || this.pastedInInput_ || caretNotAtEnd;
     this.pageHandler_.queryAutocomplete(
         mojoString16(input), preventInlineAutocomplete);
+
+    this.dispatchEvent(new CustomEvent('query-autocomplete', {
+      bubbles: true,
+      composed: true,
+    }));
   }
 
   /**